Skip to content

Add a type: terminate step that ends the workflow with a reason and status #219

@jrob5756

Description

@jrob5756

Summary

Add a step type that explicitly ends the workflow with a structured reason and an overall status (success / failure). Today the only way to end a workflow from inside is to route to $end, which carries no information about why it ended.

Motivation

Real workflows have multiple legitimate end states beyond "the last agent finished":

  • Early success — "the document is already up to date; no work needed"
  • Soft abort — "no matching issues found, nothing to do"
  • Hard failure with reason — "the upstream service returned data we cannot process; stop and surface the cause"
  • Pre-condition not met — "this PR is from a fork; refusing to run the publish step"

With only $end, all of these collapse into "workflow completed" in the dashboard, JSONL log, and exit code. Downstream tooling (CI, dashboards, notifications) can't distinguish a clean no-op from a real failure.

Proposed shape

agents:
  - name: precheck
    model: claude-sonnet-4-5
    prompt: "Is the input safe to process? Return {\"safe\": bool, \"reason\": string}"
    output:
      safe: { type: boolean }
      reason: { type: string }
    routes:
      - when: "not precheck.output.safe"
        to: abort_unsafe
      - to: main_pipeline

  - name: abort_unsafe
    type: terminate
    status: failed       # success | failed
    reason: "{{ precheck.output.reason }}"
    output_template: |   # optional
      {
        "aborted": true,
        "stage": "precheck",
        "reason": "{{ precheck.output.reason }}"
      }

  - name: noop_exit
    type: terminate
    status: success
    reason: "Document already up to date; no edits needed."

Behavior

  • Reaching a terminate step ends the workflow immediately (no routes evaluated after).
  • status: success — exit code 0; dashboard shows ✅; JSONL emits workflow_completed with termination_reason.
  • status: failed — exit code non-zero; dashboard shows ❌; JSONL emits workflow_failed with termination_reason and is_explicit: true (to distinguish from uncaught exceptions).
  • output_template: (optional) — Jinja2 expression that becomes the final output: block. If omitted, the workflow's top-level output: mapping is rendered as usual.
  • Terminate steps don't have routes: — schema validator should reject them.

Why now

  • Tiny change, big ergonomic improvement for workflows with multiple end states.
  • Surfaces in every observability surface we already have (CLI exit code, dashboard, event log, checkpoint metadata).
  • Lives in the engine dispatch loop next to the existing $end handling — couple dozen lines plus schema + tests.

Open questions

  • Should multiple terminate steps with the same name be allowed across branches, or must each be unique? Suggest unique names like every other step (consistency with parallel/for-each routing targets).
  • Should status: allow custom strings beyond success / failed? Suggest no — keep it binary for clean integration with exit codes and dashboards.
  • Interaction with sub-workflows: a terminate inside a sub-workflow ends just that sub-workflow (parent sees it as the sub-workflow's result). Document this explicitly.

Acceptance criteria

  • type: terminate accepted by the YAML schema with status, reason, optional output_template
  • Schema rejects routes: / tools: / output: / validator: on terminate steps
  • Reaching a terminate step ends the workflow without evaluating subsequent routes
  • CLI exit code matches status: (0 vs non-zero)
  • workflow_completed / workflow_failed event includes termination_reason and terminated_by: <step_name>
  • Dashboard shows terminate steps distinctly (e.g. red/green pill with reason text)
  • Sub-workflow terminate ends just the sub-workflow; parent continues normally
  • Example under examples/
  • Tests for: success termination, failure termination, terminate inside sub-workflow, terminate inside parallel branch, terminate inside for_each

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:configYAML schema, loader, validatorarea:engineWorkflow engine, routing, context, limitsenhancementNew feature or requestideaSpeculative feature proposal — not yet committed

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions