Decision node
Ask a decision model typed questions about your data and get values you can branch on
Purpose
The Decision node asks a decision model a set of typed questions about your data, and writes one output field per question. It generates no text.
Each question has a type, and the type decides the shape of the answer:
- probability gives you a number from 0 to 1 for a yes/no question
- choice gives you exactly one of the named options you defined
- score gives you a position on an ordered scale you defined
Use it for classification, routing, triage, scoring and sentiment. Anywhere you currently ask an agent for a label and then parse its reply, a Decision node gives you the value directly.
The Decision node is linear. It does not branch.
It has one outgoing connection, like an Agent node. To route on an answer, follow it with a Condition node reading the field the Decision node wrote.
This is deliberate. One Decision node can answer several questions at once, so a branching node would need a branch set per question. Separating the two keeps the answer and the routing independently readable.
How it differs from the other nodes
| Agent node | Guardrail node | Decision node | |
|---|---|---|---|
| Returns | Text, and tool calls | A pass or a fail | Typed values |
| Branches | No | Yes, two fixed | No |
| Questions per call | One prompt | One policy | Up to 32 |
| Model | Any chat model | The workflow default | A decision-capable configuration |
| Output shape | Free text you parse | _guardrail | One field per question |
Configuration
A decision-capable LLM configuration, meaning one with Supports Decisions turned on and a decision endpoint set.
Chat-only configurations are refused at run time, not silently used. See Decision models for how to set one up.
The text describing what is being decided about. It supports {{variables}}:
Customer message: {{input.message}}
Account tier: {{input.tier}}The model sees only this text. Anything the questions depend on has to be in here, so a question about the account tier needs the tier in the state.
Maximum 20,000 characters.
From 1 to 32 questions. Each has a key, a type, instructions, and criteria.
The key becomes the output field name, so it must be lowercase snake_case: start with a lowercase letter, then lowercase letters, digits or underscores, up to 64 characters.
See Question types below.
The display label for the node on the canvas.
Question types
probability
A yes/no question answered as a number. The criteria say what a yes and a no mean.
{
"is_urgent": {
"type": "probability",
"instructions": "Does this message convey urgency?",
"criteria": {
"true": "Explicitly time-sensitive, or the customer is escalating",
"false": "No urgency expressed"
}
}
}Output: <node-id>.is_urgent.probability, a number from 0 to 1.
choice
Pick exactly one option. The criteria map each option key to what that option means. You need 2 to 255 options.
{
"intent": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments, refunds, invoices",
"technical": "Bugs, outages, integration errors",
"sales": "Pricing questions and new business"
}
}
}Outputs:
| Field | Contains |
|---|---|
<node-id>.intent.choice | One of your option keys |
<node-id>.intent.probabilities | The probability of every option |
<node-id>.intent.confidence | How confident the model is |
score
Place the state on an ordered scale. The criteria are the scale labels, lowest to highest. You need 2 to 255 labels.
{
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer sound?",
"criteria": ["Calm", "Mildly annoyed", "Frustrated", "Very angry"]
}
}Outputs:
| Field | Contains |
|---|---|
<node-id>.frustration.score | A fractional index into the scale, where 0 is the first label |
<node-id>.frustration.legend | The index-to-label mapping |
<node-id>.frustration.probabilities | The probability of each label |
<node-id>.frustration.confidence | How confident the model is |
Limits
| Limit | Value |
|---|---|
| Questions per node | 32 |
Options per choice, labels per score | 2 to 255 |
| Option key length | 64 characters |
| Instructions length | 2,000 characters |
| One criterion length | 1,000 characters |
| State length | 20,000 characters |
Routing on an answer
Follow the Decision node with a Condition node in variable mode.
Branch on a choice
Set variable_path to <node-id>.<question>.choice with the equals operator.
{
"id": "route-by-team",
"type": "conditional",
"config": {
"condition_type": "variable",
"variable_path": "route.intent.choice",
"branches": [
{ "operator": "equals", "compare_value": "billing", "goto": "billing-agent" },
{ "operator": "equals", "compare_value": "technical", "goto": "tech-agent" },
{ "operator": "equals", "compare_value": "sales", "goto": "sales-agent" }
]
}
}Branch on a probability
Use a numeric operator and a threshold.
{
"condition_type": "variable",
"variable_path": "route.is_urgent.probability",
"branches": [
{ "operator": "greater_than", "compare_value": 0.7, "goto": "fast-track" }
],
"default_goto": "normal-queue"
}Branch on a score
A score is a fractional index, so compare it against an index, not a label. With the scale ["Calm", "Mildly annoyed", "Frustrated", "Very angry"], a score above 2 means at least "Frustrated".
{
"condition_type": "variable",
"variable_path": "triage.frustration.score",
"branches": [
{ "operator": "greater_than", "compare_value": 2, "goto": "escalate" }
],
"default_goto": "standard-reply"
}Add a default branch to the Condition node. A decision model always returns one of your options, but a default branch protects you if someone later edits the option keys on the Decision node and forgets the Condition node.
Metadata
Alongside the answers, the node writes a _metadata object holding the model used, a decision ID, the latency in milliseconds, and the number of attempts. It appears in the execution timeline, which makes a slow or retried decision easy to spot. See Execution artifacts.
Retries
The node makes one bounded retry of its own, with a short backoff, rather than leaving it to the workflow engine.
Only failures classed as transient are retried: a 429, a 5xx, a timeout or a connection error. These fail immediately with no retry:
- A bad configuration
- A model that is not decision-capable
- A project that is denied access to the model
- An exceeded token quota
Examples
Example 1: Support triage in one call
One node answers three questions, and the Condition nodes after it route on two of them.
{
"id": "triage",
"type": "decision",
"config": {
"llm_config_id": "<a decision-capable configuration>",
"state": "Customer message: {{input.message}}\nAccount tier: {{input.tier}}",
"questions": {
"intent": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Payments, refunds, invoices",
"technical": "Bugs, outages, integration errors",
"sales": "Pricing questions and new business"
}
},
"is_urgent": {
"type": "probability",
"instructions": "Does this message convey urgency?",
"criteria": {
"true": "Explicitly time-sensitive or escalating",
"false": "No urgency expressed"
}
},
"frustration": {
"type": "score",
"instructions": "How frustrated does the customer sound?",
"criteria": ["Calm", "Mildly annoyed", "Frustrated", "Very angry"]
}
}
},
"next": "check-urgency"
}Asking all three in one call costs one round trip. Three separate nodes would cost three.
Example 2: Score a document and route on the result
{
"id": "score-cv",
"type": "decision",
"config": {
"llm_config_id": "<a decision-capable configuration>",
"state": "Role: {{input.role}}\n\nCandidate CV:\n{{cv-text.content}}",
"questions": {
"fit": {
"type": "score",
"instructions": "How well does this candidate match the role?",
"criteria": ["No match", "Weak match", "Possible match", "Strong match"]
},
"has_required_certification": {
"type": "probability",
"instructions": "Does the CV evidence the certification the role requires?",
"criteria": {
"true": "The certification is named and current",
"false": "Not named, or expired"
}
}
}
},
"next": "shortlist-check"
}Best practices
A node can answer up to 32 questions in a single call. Splitting them across nodes multiplies the round trips and the cost for no benefit.
The model sees only the state text. A question about a field that is not in the state gets a guess.
The most common cause of an unstable choice is two options whose meanings overlap. Make each criterion name what belongs to it and, where it helps, what does not.
The key becomes the output path, so name it for how you will read it. intent reads better than q1 in every Condition node downstream.
For a choice, route on choice. Use confidence to decide whether to send a borderline case to a human instead.
"Is this urgent" as a probability gives you a threshold you can tune. The same question as a choice between yes and no gives you one bit.
Troubleshooting
Cause: A question key is not lowercase snake_case, a choice has fewer than 2 options, or a field exceeds one of the length limits.
Fix: The error names the field. The same rules apply at save time and at run time, so a node that saves is a node the API accepts.
Cause: The configuration selected on the node does not have Supports Decisions turned on.
Fix: Turn on the capability and add a decision endpoint, or pick a different configuration. See Decision models.
Cause: The decision endpoint on the configuration points somewhere that is not a decisions route, often the chat completions URL.
Fix: Correct Decision endpoint on the LLM configuration.
Cause: The model returned a choice outside your option keys. MagOneAI fails the call rather than passing it through, because a Condition node downstream would silently fall to its default branch.
Fix: Make the option keys and their criteria more distinct.
Causes to check:
- The
variable_pathis missing the answer field. It isroute.intent.choice, notroute.intent. - The node ID in the path does not match the Decision node's ID.
- You are comparing a score against a label instead of an index.
Cause: The failure is not transient. A bad configuration, a wrong model kind, a denied project and an exceeded quota all fail on the first attempt by design.
Fix: Read the error. A retry would not have helped.