Branching Conditions
How branching conditions alter plan execution flow through safe expression evaluation and typed actions (skip_to, add_steps, fail, continue).
Branching conditions let research plans adapt their execution flow based on step results. After each step completes, submit_step_result evaluates any conditions attached to that step and executes the resulting action. This enables plans to skip unnecessary steps when confidence is high, fail early when data quality is low, or dynamically add steps when deeper investigation is needed.
How Branching Conditions Are Created
Branching conditions are defined at plan creation time via the branchingConditions parameter of create_research_plan. Each condition specifies:
| Field | Type | Purpose |
|---|---|---|
afterStepOrder | number | Which step this condition applies to (1-based order) |
conditionExpression | string | The expression to evaluate against step results |
ifTrueAction | string | The action to take when the condition is true |
actionParams | Record<string, unknown> | Optional parameters for the action (e.g., target step ID) |
During plan creation, afterStepOrder values are resolved to actual step IDs. If a condition references a step order beyond the plan's step count, plan creation fails with INVALID_STEP_REFERENCE.
The conditions are stored in the plan_branching_condition table, linked to both the plan and the specific step they follow. See Data & Storage -- Schema Design for the table definition.
Expression Evaluation
The expression evaluator in packages/mcp/src/plan-engine/branching.ts uses safe string parsing -- no eval(), no Function() constructor. It supports a limited grammar: a left-side dot-path, a comparison operator, and a right-side literal value.
Supported Operators
The evaluator recognizes six comparison operators, checked in this order:
| Operator | Behavior |
|---|---|
=== | Strict equality (type-aware) |
!== | Strict inequality |
>= | Greater than or equal (numeric) |
<= | Less than or equal (numeric) |
> | Greater than (numeric) |
< | Less than (numeric) |
Operator precedence matters because the evaluator finds the first matching operator by scanning the expression string. >= and <= are checked before > and < to avoid partial matches.
Left-side: Dot-path resolution
The left side of the expression is a dot-separated path into the context object. The resolvePath function walks the path segments, returning undefined if any segment is missing:
"confidence" --> context.confidence
"result.hasData" --> context.result.hasData
"result.metrics.accuracy" --> context.result.metrics.accuracy
Right-side: Literal parsing
The right side is parsed into a typed JavaScript value:
| Raw string | Parsed as |
|---|---|
true | boolean true |
false | boolean false |
null | null |
"completed" or 'completed' | string (quotes stripped) |
0.8 | number |
| Anything else | Left as raw string |
Example Expressions
confidence > 0.8 -- numeric comparison against step confidence
result.hasData === true -- boolean check on nested result field
status === "completed" -- string equality
result.metrics.accuracy > 0.9 -- deeply nested numeric check
status !== "failed" -- negative string comparison
score >= 80 -- numeric threshold
Safety guarantees
The evaluator returns false for any expression it cannot parse. Missing dot-path segments return undefined, and comparisons involving undefined evaluate to false. There is no way to execute arbitrary code -- the evaluator only does property lookup and comparison.
Branch Actions
When a condition evaluates to true, its ifTrueAction string is resolved into a typed BranchAction by resolveBranchAction:
skip_to
Skips all pending steps between the current step and the target step. The target step ID is provided in actionParams.stepId. Steps that are already completed or failed are not affected -- only pending steps are transitioned to skipped.
The skipStepsTo function in submit-step-result.ts walks the step list in order, skipping pending steps between the current and target:
async function skipStepsTo(tx, planId, currentStepId, targetStepId) {
// Walk steps in order
// After finding currentStepId, skip all pending steps
// Stop when reaching targetStepId
}
add_steps
Signals that additional steps should be added to the plan. The current implementation resolves this action type but the AI platform is responsible for calling modify_plan with add_steps to actually insert the new steps.
fail
Immediately fails the plan. Updates research_plan.status to failed. This is one of only two ways a plan can fail (the other is a user reject decision).
continue
The default action for any unrecognized action string. Execution proceeds normally to the next pending step.
Evaluation Context
When submit_step_result evaluates branching conditions, it builds the context object from the completed step's data:
const context = {
confidence: confidence ?? 0, // step's self-assessed confidence (0-1)
result: result, // the structured result submitted by the AI
status: "completed", // always "completed" at evaluation time
};
This means branching expressions can reference:
confidence-- the numeric confidence scoreresult.*-- any field in the step's result objectstatus-- always"completed"(branching only runs after successful completion)
Evaluation Flow in submit_step_result
After a step is marked as completed, evaluateBranchingConditions runs:
- Query
plan_branching_conditionfor conditions whereafter_step_idmatches the completed step - For each condition, evaluate the expression against the context
- If the condition is true, resolve and execute the action
- If any action is
fail, return{ planFailed: true }immediately -- no further conditions are checked - If no conditions match or all actions are
continue/skip_to, proceed normally
After branching evaluation, derivePlanStatus recalculates the plan status from all step states and updates the research_plan row.
Related Pages
- State Machines -- the state transitions that branching actions trigger
- Execution Loop -- where branching fits in the end-to-end flow
- Data & Storage -- Schema Design -- the
plan_branching_conditiontable
Plan and Step State Machines
The two interlocking finite state machines that govern research plan and step lifecycles, including every valid state, transition, and the derivePlanStatus function.
Stall Detection
How the plan engine identifies steps stuck in in_progress beyond a configurable threshold, and how stall information surfaces to users and administrators.