Conditional Execution
Sometimes status-based gating isn't enough. You need to skip a step based on the value
of a previous step's output or based on the trigger payload — not just because a
predecessor failed. FlowOrchestrator supports this via the When clause on a RunAfter
entry.
A step whose When evaluates to false is recorded as Skipped (not Failed),
and the dashboard surfaces the evaluation trace so you can see exactly why.
Operator Reference
| Operator | Semantics | Example |
|---|---|---|
== |
Equality | @steps('x').output.status == 'approved' |
!= |
Inequality | @triggerBody()?.region != 'EU' |
> < >= <= |
Numeric / ordinal compare | @steps('x').output.amount >= 1000 |
&& |
Logical AND (short-circuits) | a > 5 && b == 'foo' |
\|\| |
Logical OR (short-circuits) | a > 5 \|\| b == 'foo' |
! |
Negation | !@steps('x').output.flag |
( ) |
Grouping | (a > 5 \|\| b > 10) && c == 'ok' |
Right-hand side literals: numbers, strings (single or double quoted), true, false, null.
Left-hand side: any @steps('key').output.path, @steps('key').status, @steps('key').error,
@triggerBody(), or @triggerHeaders()['Header-Name'] expression.
Type Coercion
Comparison rules are intentionally strict — the evaluator does not silently coerce between types:
- number ↔ number → compared as
decimal - string ↔ string → ordinal compare
- bool ↔ bool → equality only
nullis equal/not-equal tonullonly; comparingnullwith any other operator throws.- Mismatched types (e.g. comparing a string to a number) throw
FlowExpressionExceptionand the step is recorded asSkippedwith the error message in the trace.
Worked Examples
1 — Status-only (legacy syntax — still supported)
RunAfter = new RunAfterCollection
{
["validate"] = [StepStatus.Succeeded]
}
2 — When-only
Step runs only if the trigger payload includes a priority field equal to 'high':
RunAfter = new RunAfterCollection
{
[""] = new RunAfterCondition
{
When = "@triggerBody()?.priority == 'high'"
}
}
The empty key "" is a synthetic entry-trigger marker — useful when you want to
attach a When clause to an entry step that has no real predecessor.
3 — Combined status + when
RunAfter = new RunAfterCollection
{
["fetch_order"] = new RunAfterCondition
{
Statuses = [StepStatus.Succeeded],
When = "@steps('fetch_order').output.amount > 1000"
}
}
Step runs only when fetch_order Succeeded AND the resolved amount is greater than 1000.
4 — Branching (the canonical pattern)
["high_value_approve"] = new StepMetadata
{
Type = "LogMessage",
RunAfter = new RunAfterCollection
{
["start"] = new RunAfterCondition
{
Statuses = [StepStatus.Succeeded],
When = "@triggerBody().amount > 1000"
}
},
...
},
["auto_approve"] = new StepMetadata
{
Type = "LogMessage",
RunAfter = new RunAfterCollection
{
["start"] = new RunAfterCondition
{
Statuses = [StepStatus.Succeeded],
When = "@triggerBody().amount <= 1000"
}
},
...
},
["complete"] = new StepMetadata
{
Type = "LogMessage",
RunAfter = new RunAfterCollection
{
["high_value_approve"] = [StepStatus.Succeeded, StepStatus.Skipped],
["auto_approve"] = [StepStatus.Succeeded, StepStatus.Skipped]
},
...
}
complete accepts both Succeeded and Skipped from each branch, so the run always
finishes regardless of which branch took effect. See
AmountThresholdFlow
for the runnable sample.
5 — Complex boolean
["escalate"] = new StepMetadata
{
RunAfter = new RunAfterCollection
{
["risk_check"] = new RunAfterCondition
{
Statuses = [StepStatus.Succeeded],
When = "(@steps('risk_check').output.score >= 0.8 || @triggerBody()?.priority == 'high') && @triggerBody().region != 'EU'"
}
}
}
Why was my step skipped?
When a step is skipped because its When clause evaluated to false, the dashboard
shows a "Why skipped" panel under the step in the run timeline. The panel displays:
- The original expression text from the manifest
- A rewrite with each LHS replaced by its resolved runtime value
(e.g.
500 > 1000) - The boolean result (always
false, since that's why it was skipped)
This makes "why didn't this step run?" answerable from the dashboard alone — no log diving required.
Anti-pattern: throwing exceptions to skip a step
Some authors fake conditional execution by throwing an exception inside a step handler, hoping the failure cascade will cause downstream steps to be skipped. Don't do this:
- The run history shows a
Failedstatus, polluting metrics and alerting. - The exception message is opaque — readers can't tell whether it was an intentional branch or an actual bug.
- Retry logic will retry the "fake failure" repeatedly.
A When clause cleanly expresses intent (this branch is intentionally not running),
records a clean Skipped status, and surfaces the decision in the dashboard.