Speculative fan-out
Ask every question a flow might need in one call — Jev answers them in parallel — and let AND rules and your code decide which answers matter. Built on the Support Ticket Triage template.
Ask every question the flow might need in one call — Jev answers them in parallel — and let rules and your code decide which answers matter. Instead of asking for a ticket's category first and its bug severity in a follow-up call, ask both at once: if the ticket isn't a bug report, you simply ignore the severity.
Benefits: cost and speed. TypeSafe's guide: Speculative fan-out.
Why it works
- One round trip. Jev reads the state once and evaluates every question against it independently and in parallel, so extra questions add little latency. In TypeSafe's parallel questions cookbook, batching 13 questions into one call was about 12× cheaper and 10× faster than one call per question, with the same answers.
- One billable decision. Dcision bills per successful call, not per question: a decision with five questions is one decision on your plan. The engine cost grows only with the input tokens of the extra questions — see
metrics.input_tokens. - No hidden coupling. Each answer is computed on its own; one question's answer never becomes context for another. Your rules combine them afterwards.
When to use it
Triage where later steps depend on earlier answers — category → bug severity → refund — and any flow that would otherwise chain calls ("if A, then ask B"). A decision holds up to 64 questions, within Jev's token budget: about 32,000 tokens for the state plus the longest question, 64,000 for the state plus all questions.
How it maps to Dcision
| In TypeSafe's guide | In Dcision |
|---|---|
| All questions in one request | All questions in one decision — one POST /v1/decisions/{slug} |
| Speculative questions | Questions that only matter in some branches, such as bug_severity |
if category == "bug_report" and severity > … in your code | A rule with and conditions: category = bug_report AND bug_severity ≥ 4 → escalate |
frustration.score > 1.5 (levels counted from 0) | A rule on the weighted level: frustration (weighted level) ≥ 3.5 → escalate (levels counted from 1) |
Rules hold the routing that must be consistent and auditable; your code still reads every answer in result.
Build it
In the app
Open Templates, find Support Ticket Triage (fan-out) — under All templates, or Try on the Speculative fan-out card — and choose it. In Name it, call the decision Ticket Triage: the slug, your endpoint, comes from the name and becomes ticket-triage. (The template's full name would give support-ticket-triage-fan-out; you can also edit the slug in the editor until the first deploy.) Click Create decision.
The State is a JSON object with subject (string) and body (string, required).
The Questions are the whole decision tree at once:
| Question | Type | Answers | Matters when |
|---|---|---|---|
category | choice | bug_report, billing, feature_request, how_to, other | always |
bug_severity | score | cosmetic, minor, major, critical | bug reports (speculative) |
has_reproducible_steps | probability | the ticket includes steps to reproduce the problem | bug reports (speculative) |
refund_requested | probability | the customer explicitly asks for a refund or credit | billing (speculative) |
frustration | score | calm, annoyed, frustrated, very angry | always |
The critical level is a structured level — { "label": "critical", "rubric": "outage, data loss, security issue or money at risk" } — so the engine reads the rubric while the API returns the label critical.
In Policies, each rule reads IF condition [AND condition…] THEN action:
- IF
category· answer · = ·bug_report, then + ANDbug_severity· answer · ≥ · 4 · critical — THENescalate. - IF
refund_requested· answer · ≥ · 0.8 — THENescalate. - IF
frustration· weighted level · ≥ · 3.5 — THENescalate.
The fallback action stays escalate, so a ticket that fits no category (other) also reaches a person.
Run the sample ticket in the Playground, check the answers and the action, then Deploy v1. The editor's Patterns panel now marks Speculative fan-out and Intent routing as in use.
The decision schema
The questions and rules, as the editor's Decision schema view shows them (the reserved other option is added to category automatically):
{
"questions": [
{
"key": "category",
"type": "choice",
"instructions": "What kind of support ticket is this?",
"options": [
{ "value": "bug_report", "description": "something in the product is broken or behaves incorrectly" },
{ "value": "billing", "description": "charges, invoices, refunds, plans, payment methods" },
{ "value": "feature_request", "description": "asks for something the product doesn't do yet" },
{ "value": "how_to", "description": "asks how to do something that already works" }
]
},
{
"key": "bug_severity",
"type": "score",
"instructions": "If this describes a bug, how severe is it for the customer?",
"scale": ["cosmetic", "minor", "major", { "label": "critical", "rubric": "outage, data loss, security issue or money at risk" }]
},
{ "key": "has_reproducible_steps", "type": "probability", "instructions": "Does the ticket include steps to reproduce the problem?" },
{ "key": "refund_requested", "type": "probability", "instructions": "Does the customer explicitly ask for a refund or credit?" },
{ "key": "frustration", "type": "score", "instructions": "How frustrated is the customer?", "scale": ["calm", "annoyed", "frustrated", "very angry"] }
],
"policies": [
{
"field": "category",
"on": "output",
"operator": "eq",
"value": "bug_report",
"and": [{ "field": "bug_severity", "on": "output", "operator": "gte", "value": 4 }],
"action": "escalate"
},
{ "field": "refund_requested", "on": "output", "operator": "gte", "value": 0.8, "action": "escalate" },
{ "field": "frustration", "on": "score", "operator": "gte", "value": 3.5, "action": "escalate" }
],
"settings": { "fallbackAction": "escalate" }
}With the CLI, dcision init triage.json --template ticket-triage writes the complete schema to a file.
Call it
curl -X POST https://api.dcision.io/v1/decisions/ticket-triage \
-H "Authorization: Bearer $DCISION_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ticket-90412" \
-d '{
"state": {
"subject": "Payouts failing",
"body": "Our payouts have been failing for 3 days and customers are complaining. Steps: open Payouts, click Retry, error 500. Please fix ASAP."
}
}'{
"decision_id": "dec_Tq4Wm8Lz2Xv6Rk1Np9Cs",
"execution_id": "exec_Jd7Hs2Qa5Wn8Lx3Kv1Mz",
"schema": "ticket-triage",
"version": 1,
"result": {
"category": "bug_report",
"bug_severity": "critical",
"has_reproducible_steps": 0.91,
"refund_requested": 0.04,
"frustration": "frustrated"
},
"confidence": {
"category": 0.9125,
"bug_severity": 0.56,
"has_reproducible_steps": 0.82,
"refund_requested": 0.92,
"frustration": 0.59
},
"scores": { "bug_severity": 3.56, "frustration": 2.97 },
"action": "escalate",
"action_reason": { "type": "policy", "rule": 0 },
"metrics": {
"latency_ms": 368,
"engine": "jev",
"model": "jev-1.13.0",
"estimated_cost_usd": 0.00002184,
"input_tokens": 520,
"output_tokens": 58
}
}Five answers from one call. Rule 0 matched: the ticket is a bug_report and its most likely severity is critical (level 4). scores holds the weighted level of each score question — bug_severity sits at 3.56, between major and critical.
Act on it
The rules decided whether a person must look now; your code reads the speculative answers only in the branch where they matter:
const decision = await response.json();
const { result } = decision;
if (decision.action === "escalate") {
return onCall.page(ticket, { reason: decision.action_reason, executionId: decision.execution_id });
}
switch (result.category) {
case "bug_report":
return result.has_reproducible_steps >= 0.6
? engineering.triage(ticket, { severity: result.bug_severity })
: bugBacklog.add(ticket, { severity: result.bug_severity });
case "billing":
return billing.assign(ticket, { refundLikely: result.refund_requested >= 0.7 });
case "feature_request":
return roadmap.log(ticket);
case "how_to":
return autoReply.withDocs(ticket);
}frustration is useful in every branch — for example, raise the priority when decision.scores.frustration is 3 or more, even below the escalation threshold.
Tune it
- Add speculative questions freely. Each one costs its input tokens, not a round trip. Stay within 64 questions and the token budget.
- Keep each question atomic. One judgment per question, read literally — see Writing good questions.
- Gate with
and, not with longer instructions. "If this describes a bug, how severe…" stays a simple question; the rule decides when its answer counts. - Threshold between levels with
on: "score":frustration ≥ 3.5triggers when the weight sits betweenfrustrationandvery angry, not only whenvery angrywins. - Order rules from specific to general: the first match wins.
- Send only the ticket. Long threads and unrelated fields dilute accuracy and use the token budget — filter them in your code.
Related: Policies and actions · Questions · Support routing
Patterns overview
TypeSafe's four architectural patterns — speculative fan-out, confidence-gated routing, composite scoring and intent routing — and how each maps to Dcision features and templates.
Confidence-gated routing
Use confidence as a second axis — the answer says what, confidence says whether to act — with a minimum confidence per question and per-action AND rules. Built on the Voice Banking template.