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.planFor 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 speckitRunning 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
When a workflow declares more than one input, pass each with a separate --input flag. For example, a custom workflow that gates steps on a scope selector:
specify workflow run ./my-workflow.yml \
--input spec="Build a user authentication system with OAuth support" \
--input scope="backend-only"The bundled speckit workflow only declares spec (and optional integration); it does not take a scope input.
Step Types
Workflows support 12 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 modelCLI integrations can expose per-step positional arguments and named runtime options. For example, Docker Agent accepts an agent reference plus validated agent and safety options. Use the command step's top-level model field for model selection:
- id: specify-with-docker-agent
command: speckit.specify
integration: docker-agent
integration_args:
- "{{ inputs.agent_config }}"
integration_options:
agent: root
safety: balanced
model: "openai/gpt-5"
input:
args: "{{ inputs.spec }}"integration_args must be an ordered list of strings. Expressions are resolved one element at a time. integration_options must be a mapping with string keys; its values are likewise expression-resolved and validated by the selected integration. Non-empty runtime configuration is rejected when an integration does not support it. Resolved values are stored in workflow run state for audit and recovery. On resume, the complete dispatch configuration (integration, model, integration_args, and integration_options) is re-resolved from the current workflow inputs; without updated inputs this reproduces the prior values.
When Docker Agent integration_args supplies an agent reference for a command step, it takes precedence over SPECKIT_INTEGRATION_DOCKER_AGENT_EXTRA_ARGS; the entire legacy environment value is ignored for that step. Without a per-step agent reference, the legacy environment behavior is unchanged.
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: claudeShell Steps
Run a shell command and capture output:
- id: run-tests
type: shell
run: "npm test" # runs from the project root; no interpolation needed
timeout: 1800 # Optional: max seconds before the command is killed (default 300)⚠️ Constrain interpolated values in
runfields. Arunfield is executed by the system shell, and <code v-pre>{{ ... }}</code> expressions are substituted as raw text with no automatic quoting or escaping. Aninputs.*value or prior-step output (including AI-generatedpromptoutput) is parsed as shell syntax and can change the command that runs. The only reliable control is to restrict such a value at the source with anenum/allowlist, or to keep values you cannot constrain out ofrunentirely. Quoting a substitution helps a trusted value survive word-splitting but is not a security boundary — there is no shell-escaping filter, and a value containing the matching quote can still break out. See Interpolation and shell safety.
timeout is the maximum time in seconds the command may run before it is killed and the step fails; it must be a positive number and defaults to 300 (five minutes) when omitted. Raise it for long-running gates such as full builds, linter aggregators, or integration-test targets.
Init Steps
Bootstrap a project the same way specify init does — scaffolding templates, scripts, shared infrastructure, and the selected coding agent integration. Runs non-interactively (defaults to --ignore-agent-tools) and resolves the integration from the step config or the workflow default:
- id: bootstrap
type: init
here: true # or: project: my-project
integration: copilot # Optional: defaults to workflow integration
integration_options: "--skills" # Optional: extra options for the integration
script: sh # Optional: sh, ps, or py
force: true # Optional: required when target directory already exists
preset: healthcare-compliance # Optional preset IDWorkflow Slots
Declare a named workflow slot that downstream projects can fill with a workflow overlay. The slot is skipped when unfilled; its id is the overlay anchor and name is a required human-readable label:
- id: post-implement
type: slot
name: "Post-implementation checks"Use an overlay replace edit anchored on post-implement to fill the slot. Keep the same id when downstream expressions or fan-in steps reference it, and preserve any output keys they consume. Slot steps are invalid inside fan-out.step templates because those runtime-multiplied templates cannot be targeted by overlays.
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: abortIf/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: trueSwitch 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.implementDo-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.specifyFan-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.implementFan-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 returns StepResult(status=StepStatus.FAILED, ...) at runtime halts the entire run — most commonly a shell or command step exiting non-zero. 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 a downstream if or switch can branch on it (or a gate can surface it to the operator via <code v-pre>{{ }}</code> interpolation in message):
- 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 }}). Approve to run the recovery path, or reject to leave the failure recorded and move on."
on_reject: skip
- id: recover
type: if
condition: "{{ steps.review.output.choice == 'approve' }}"
then:
- id: rerun
command: speckit.recovery
else:
- id: next-thing
command: speckit.next-thingA few things worth knowing about that example:
- Both gate options (
approve,reject) returnStepStatus.COMPLETED;on_reject: skipcontrols only whether the engine aborts on reject (it doesn't, withskip) — it does not auto-skip subsequent sibling steps in thethen:list. Downstream branching is the workflow author's responsibility: read