Skip to content
Beta. VectorStep is in beta.Let us know if something breaks.

Flow control

The runner controls all step execution and flow decisions. Agents never decide what happens next — they only report findings and score confidence. For each step:

  1. Evaluate optional when: condition — if false, skip the step entirely
  2. Parse and validate the LLM response as LLMOutput via Pydantic
  3. Run verifier if configured and trigger fires
  4. Check effective_confidence against confidence_threshold
  5. If below threshold, apply on_low_confidence action (escalate | abort | proceed)
  6. If confidence passes, check proceed:
    • proceed: false → pipeline stops with status stopped
    • proceed: true → continue to next step
  7. If the executor raised an error (step status = failed), check on_failure:
    • on_failure: abort (default) → abort the pipeline
    • on_failure: continue → log and continue to next step
    • If on_failure.webhook is set, fire that callback regardless of policy
  8. Record step result to SQLite before proceeding

Step statuses:

Status Set when Counts as failure?
completed Step ran successfully and proceed: true No
stopped Step ran successfully and returned proceed: false No
escalated Confidence below threshold and on_low_confidence: escalate No
aborted Confidence below threshold and on_low_confidence: abort No
failed Executor exception, timeout, bad JSON, or schema validation error Yes

For agent success rate calculations, failed is the only status that counts against a model.

Pipeline run statuses:

Status Meaning
completed All steps ran to completion
stopped A step returned proceed: false — clean intentional stop
escalated A step was escalated — run halted, human notified
aborted A step aborted due to low confidence
failed A step raised an unhandled error
running Currently in progress
interrupted Service restarted/crashed mid-run — set by the startup sweep, not the runner. A pipeline with durable: set resumes instead of landing here, unless the guard conditions in Durability & resume rule it out

Any sequential step or parallel group can have an optional when: field containing a Jinja2-compatible boolean expression. A false result skips the step cleanly — no DB row, no executor call, invisible to subsequent steps.

steps:
- name: triage
executor: openclaw
executor_config:
agent: sre-triage
prompt_template: |
Analyse the alert. Return JSON with an "action" field: "remediate" | "escalate_human" | "ignore"
- name: auto-remediation
when: "steps.triage.action == 'remediate'"
executor: openclaw
executor_config:
agent: sre-remediator
prompt_template: |
Triage says: {{steps.triage.summary}}. Attempt remediation.
- name: page-oncall
when: "steps.triage.action == 'escalate_human'"
executor: human
prompt_template: |
<b>Page oncall?</b> Triage: {{steps.triage.summary}}

when: vs proceed: false:

  • when: — pipeline author decides in advance which steps are relevant given prior step outputs
  • proceed: false — the agent signals the pipeline is complete and no further steps are warranted

loop_until re-runs a step until its effective confidence reaches a target, giving the agent a chance to correct itself:

- name: draft-summary
executor: gateway
executor_config: { agent: writer }
loop_until:
confidence: 0.85 # stop once effective confidence is at least this
max_iterations: 3 # default 3; the loop never runs more than this
prompt_template: |
Summarise the incident.
{% if loop.iteration > 1 %}
Your previous answer scored {{ loop.prior_confidence }}. Previous output:
{{ loop.prior_output.summary }}
Fix what was weak.
{% endif %}

Each iteration’s template gets a loop object:

Variable Value
loop.iteration 1, 2, 3 …
loop.max_iterations the configured limit
loop.prior_confidence the previous iteration’s effective confidence (after verifier and grounding), or None on the first iteration
loop.prior_output the previous iteration’s output as a dict (summary, next_step_context, reasoning, any extra fields), or None on the first iteration

The loop stops as soon as the effective confidence meets confidence, or after max_iterations. Only the last iteration’s output is saved and passed on. Every iteration costs a full agent call, and the run log records loop_iteration, then loop_converged or loop_exhausted. If it is exhausted, the step carries on with its last result and is then judged against the step’s own confidence_threshold like any other step. Guard loop.prior_* with loop.iteration > 1, since they are None on the first pass.

Later steps read earlier outputs as steps.<name>.<field>. A step name with a hyphen cannot be written with dot notation (Jinja reads - as subtraction), so every step is available under both its original name and a copy with hyphens turned into underscores:

{{ steps.slice_inputs.summary }} {# hyphen → underscore #}
{{ steps['slice-inputs'].summary }} {# original name, bracket form #}

Branches of a parallel group are addressed by their own names. Branches of a fan_out are named <fan_out name>/<index> and need the bracket form: {{ steps['check-upstreams/0'].summary }}. The group as a whole is not a step output, so use the branches.

By default a failed step (executor exception, timeout) aborts the pipeline. For non-critical steps — enrichment lookups, external API calls, notifications — you can allow failures to pass through:

- name: enrich-from-cmdb
executor: webhook
on_failure: continue # pipeline keeps running if this step fails
executor_config:
url: "https://cmdb.internal/api/enrich"

The step is still recorded in run history with status: failed and the error message is available in step_outputs[name].summary for downstream when: conditions or prompt templates.

A step that fails with policy: continue still counts toward the budget — its spend is added and the budget is checked straight after, so a pipeline whose steps keep failing and continuing can still be stopped by budget.max_usd.

on_failure only applies to executor errors. Low-confidence results that trigger on_low_confidence: abort are LLM-quality decisions and are always pipeline-stopping regardless of this setting.

Attach a webhook to any step that fires when that step fails, without adding a separate notify step to the pipeline. The callback fires before the pipeline decides whether to abort or continue, so it always goes out regardless of the policy.

- name: triage
executor: gateway
executor_config:
agent: sre-triage
on_failure:
policy: continue # pipeline continues even if this step fails
webhook:
url: "${PAGERDUTY_URL}"
headers:
Authorization: "Token ${PAGERDUTY_TOKEN}"
payload:
summary: "Triage step failed: {{step_failure.summary}}"
severity: critical

on_failure as a block:

Field Default Description
policy abort abort or continue — what the pipeline does after the failure
webhook.url required Outbound URL (${ENV_VAR} expansion supported)
webhook.method POST HTTP method
webhook.headers {} Header dict; values support ${ENV_VAR} expansion
webhook.payload {} JSON body dict; string values are Jinja2 templates
webhook.timeout_seconds 30 Request timeout

String shorthand (on_failure: continue or on_failure: abort) is equivalent to setting policy only with no webhook.

The Jinja2 context for the webhook payload includes all standard step context variables plus step_failure.step, step_failure.summary, and step_failure.status. Webhook delivery failures are logged as a step_failure_webhook_failed run-log event and never abort the pipeline.

Optional retry: block on any step. Retries wrap the executor call only — low-confidence results are valid outputs and are never retried.

retry:
attempts: 3
backoff: exponential # fixed | exponential
delay_seconds: 2.0
Field Default Description
attempts 3 Total attempts, including the first — attempts: 3 means up to 2 retries.
backoff exponential fixed (always delay_seconds) or exponential (delay_seconds doubling each attempt).
delay_seconds 1.0 Base delay between attempts — the example above raises it to 2.0.

Two details worth knowing:

  • A reply cut off at the model’s output-token limit is not retried. The same request would truncate the same way, so retrying only pays for it again; the step fails straight away with an output truncated error.
  • Failed attempts are recorded and costed. Every attempt that reached the model adds its tokens and cost to the one step row, and the last attempt’s trace and prompt are kept, so a step that failed after three tries shows what all three cost. An attempt that never reached the model (a timeout or connection error) has no tokens to count.