Adds an optional `continue_on_error: bool` field on every step.
When set to `true` and the step fails, the engine records the
result (`exit_code`, `stderr` on `steps.<id>.output` plus `status`
as a sibling key on `steps.<id>`) and continues to the next sibling
step instead of halting the run. Downstream `if`, `switch`, or
`gate` steps can then branch on
`{{ steps.<id>.output.exit_code }}` to route the recovery path.
Engine details
--------------
`WorkflowEngine._execute_steps` now consults the step config when a
step returns `StepStatus.FAILED`:
- Gate aborts (`output.aborted`) always halt the run — operator
decisions take precedence over the flag.
- Otherwise, if `continue_on_error` is the literal `True`, log a
`step_continue_on_error` event and proceed to the next sibling.
The runtime check uses identity comparison (`is True`) rather
than truthiness, so truthy non-bool values like the string
`"true"` cannot silently change run semantics even if a caller
bypasses `validate_workflow()`.
- Otherwise, behave as before: log `step_failed`, set
`RunStatus.FAILED`, and return.
Validation
----------
`_validate_steps` rejects non-bool values for `continue_on_error`.
Coerced strings like `"true"` are not accepted so authoring
mistakes surface at validation time rather than silently changing
run semantics.
Tests
-----
`TestContinueOnError` in `tests/test_workflows.py` (8 tests):
- `test_undeclared_failure_halts_run` — default halt behaviour.
- `test_declared_and_fired_continues_run` — flag + fail → continue.
- `test_declared_but_step_succeeded_is_noop` — flag + success → no-op.
- `test_if_branch_routes_around_failure` — end-to-end recovery.
- `test_gate_abort_still_halts_with_continue_on_error` — abort
always halts.
- `test_validation_rejects_non_bool_continue_on_error` — `"true"`
rejected at validation.
- `test_validation_accepts_bool_continue_on_error` — `true`/`false`
pass cleanly.
- `test_engine_ignores_truthy_non_bool_continue_on_error` —
defense-in-depth: engine ignores string `"true"` even when
validation is bypassed.
Rebased onto current upstream/main (post #2664 merge); the new
`TestContinueOnError` class sits immediately after upstream's
`TestContextRunId` so the two feature suites coexist cleanly.
Closes #2591.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.6 KiB
Workflows
Workflows are multi-step, resumable automation pipelines defined in YAML. They orchestrate Spec Kit commands across integrations, evaluate control flow, and pause at human review gates — enabling end-to-end Spec-Driven Development cycles without manual step-by-step invocation.
How It Works
A workflow definition declares a sequence of steps. The engine executes them in order, dispatching commands to AI integrations, running shell commands, evaluating conditions for branching, and pausing at gates for human review. State is persisted after each step, so workflows can be resumed after interruption.
steps:
- id: specify
command: speckit.specify
input:
args: "{{ inputs.spec }}"
- id: review
type: gate
message: "Review the spec before planning."
options: [approve, reject]
on_reject: abort
- id: plan
command: speckit.plan
For detailed architecture and internals, see ARCHITECTURE.md.
Quick Start
# Search available workflows
specify workflow search
# Install the built-in SDD workflow
specify workflow add speckit
# Or run directly from a local YAML file
specify workflow run ./workflow.yml --input spec="Build a user authentication system with OAuth support"
# Run an installed workflow with inputs
specify workflow run speckit --input spec="Build a user authentication system with OAuth support"
# Check run status
specify workflow status
# Resume after a gate pause
specify workflow resume <run_id>
# Get detailed workflow info
specify workflow info speckit
# Remove a workflow
specify workflow remove speckit
Running Workflows
From an Installed Workflow
specify workflow add speckit
specify workflow run speckit --input spec="Build a user authentication system with OAuth support"
From a Local YAML File
specify workflow run ./my-workflow.yml --input spec="Build a user authentication system with OAuth support"
Multiple Inputs
specify workflow run speckit \
--input spec="Build a user authentication system with OAuth support" \
--input scope="backend-only"
Step Types
Workflows support 10 built-in step types:
Command Steps (default)
Invoke an installed Spec Kit command by name via the integration CLI:
- id: specify
command: speckit.specify
input:
args: "{{ inputs.spec }}"
integration: claude # Optional: override workflow default
model: "claude-sonnet-4-20250514" # Optional: override model
Prompt Steps
Send an arbitrary inline prompt to an integration CLI (no command file needed):
- id: security-review
type: prompt
prompt: "Review {{ inputs.file }} for security vulnerabilities"
integration: claude
Shell Steps
Run a shell command and capture output:
- id: run-tests
type: shell
run: "cd {{ inputs.project_dir }} && npm test"
Gate Steps
Pause for human review. The workflow resumes when specify workflow resume is called:
- id: review-spec
type: gate
message: "Review the generated spec before planning."
options: [approve, edit, reject]
on_reject: abort
If/Then/Else Steps
Conditional branching based on an expression:
- id: check-scope
type: if
condition: "{{ inputs.scope == 'full' }}"
then:
- id: full-plan
command: speckit.plan
else:
- id: quick-plan
command: speckit.plan
options:
quick: true
Switch Steps
Multi-branch dispatch on an expression value:
- id: route
type: switch
expression: "{{ steps.review.output.choice }}"
cases:
approve:
- id: plan
command: speckit.plan
reject:
- id: log
type: shell
run: "echo 'Rejected'"
default:
- id: fallback
type: gate
message: "Unexpected choice"
While Loop Steps
Repeat steps while a condition is truthy:
- id: retry
type: while
condition: "{{ steps.run-tests.output.exit_code != 0 }}"
max_iterations: 5
steps:
- id: fix
command: speckit.implement
Do-While Loop Steps
Execute steps at least once, then repeat while condition holds:
- id: refine
type: do-while
condition: "{{ steps.review.output.choice == 'edit' }}"
max_iterations: 3
steps:
- id: revise
command: speckit.specify
Fan-Out Steps
Dispatch a step template for each item in a collection (sequential):
- id: parallel-impl
type: fan-out
items: "{{ steps.tasks.output.task_list }}"
max_concurrency: 3
step:
id: impl
command: speckit.implement
Fan-In Steps
Aggregate results from fan-out steps:
- id: collect
type: fan-in
wait_for: [parallel-impl]
output: {}
Error Handling
By default, any step that ends in StepStatus.FAILED at runtime halts
the entire run — most commonly a shell or command step exiting
non-zero, but also any other runtime error raised during step
execution. (Invalid workflow definitions are rejected up-front by
specify workflow run before the run even starts, so structural
validation failures never reach this code path.) Set
continue_on_error: true on a step to record its result and continue
to the next sibling step instead. When the failure was a non-zero
exit, the exit code remains available on
steps.<id>.output.exit_code so downstream if, switch, or gate
steps can branch on it:
- id: heavy-thing
type: command
integration: claude
command: speckit.heavy-thing
continue_on_error: true
- id: check-result
type: if
condition: "{{ steps.heavy-thing.output.exit_code != 0 }}"
then:
- id: review
type: gate
message: "Step failed (exit {{ steps.heavy-thing.output.exit_code }}). Retry or skip?"
on_reject: skip
else:
- id: next-thing
command: speckit.next-thing
Notes:
- The field must be a literal boolean (
true/false); coerced strings like"true"are rejected at validation time. - Gate aborts (
on_reject: abortchosen by the operator) always halt the run —continue_on_errordoes not override them. The flag is for transient/expected step failures, not for overriding deliberate operator decisions. - When the flag is omitted, behaviour is byte-equivalent to before this feature.
Expressions
Workflow definitions use {{ expression }} syntax for dynamic values:
# Access inputs
args: "{{ inputs.spec }}"
# Access previous step outputs
args: "{{ steps.specify.output.file }}"
# Comparisons
condition: "{{ steps.run-tests.output.exit_code != 0 }}"
# Filters
message: "{{ status | default('pending') }}"
Supported filters: default, join, contains, map.
Input Types
Workflow inputs are type-checked and coerced from CLI string values:
inputs:
spec:
type: string
required: true
prompt: "Describe what you want to build"
task_count:
type: number
default: 5
dry_run:
type: boolean
default: false
scope:
type: string
default: "full"
enum: ["full", "backend-only", "frontend-only"]
| Type | Accepts | Example |
|---|---|---|
string |
Any string | "user-auth" |
number |
Numeric strings → int/float | "42" → 42 |
boolean |
true/1/yes → True, false/0/no → False |
"true" → True |
State and Resume
Every workflow run persists state to .specify/workflows/runs/<run_id>/:
# List all runs with status
specify workflow status
# Check a specific run
specify workflow status <run_id>
# Resume a paused run (after approving a gate)
specify workflow resume <run_id>
# Resume a failed run (retries from the failed step)
specify workflow resume <run_id>
Run states: created → running → completed | paused | failed | aborted
Catalog Management
Workflows are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
Note
Community workflows are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting and structure, but they do not review, audit, endorse, or support the workflow definitions themselves. Review workflow source before installation and use at your own discretion.
# List active catalogs
specify workflow catalog list
# Add a custom catalog
specify workflow catalog add https://example.com/catalog.json --name my-org
# Remove a catalog
specify workflow catalog remove <index>
Creating a Workflow
- Create a
workflow.ymlfollowing the schema above - Test locally with
specify workflow run ./workflow.yml --input key=value - Verify with
specify workflow info ./workflow.yml - See PUBLISHING.md to submit to the catalog
Environment Variables
| Variable | Description |
|---|---|
SPECKIT_WORKFLOW_CATALOG_URL |
Override the catalog URL (replaces all defaults) |
Configuration Files
| File | Scope | Description |
|---|---|---|
.specify/workflow-catalogs.yml |
Project | Custom catalog stack for this project |
~/.specify/workflow-catalogs.yml |
User | Custom catalog stack for all projects |
Repository Layout
workflows/
├── ARCHITECTURE.md # Internal architecture documentation
├── PUBLISHING.md # Guide for submitting workflows to the catalog
├── README.md # This file
├── catalog.json # Official workflow catalog
├── catalog.community.json # Community workflow catalog
└── speckit/ # Built-in SDD cycle workflow
└── workflow.yml