Developing workflows
A workflow package contains a workflow.yml definition and any scripts, modules, or resources it references. Start with a small local definition, exercise success and failure paths, then package a versioned release.
Package layout
my-workflow/
├── workflow.yml
├── README.md
├── LICENSE
├── CHANGELOG.md
└── scripts/
A bare YAML file is enough for a simple workflow. Directory and archive installation preserve companion files; a raw YAML URL does not download adjacent repository files. Archives can contain the package at their root or inside one top-level directory.
Resolve companion files with context.workflow_dir or the shell's SPECKIT_WORKFLOW_DIR, not the caller's working directory.
Bundled SDD example
Workflows are defined in YAML files. Here is the built-in Full SDD Cycle workflow that ships with Spec Kit:
schema_version: "1.0"
workflow:
id: "speckit"
name: "Full SDD Cycle"
version: "1.0.1"
author: "GitHub"
description: "Runs specify → plan → tasks → implement with review gates"
requires:
speckit_version: ">=0.8.5"
integrations:
any:
- "alquimia"
- "claude"
- "copilot"
- "gemini"
- "opencode"
inputs:
spec:
type: string
required: true
prompt: "Describe what you want to build"
integration:
type: string
default: "auto"
prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)"
steps:
- id: specify
command: speckit.specify
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
- id: review-spec
type: gate
message: "Review the generated spec before planning."
options: [approve, reject]
on_reject: abort
- id: plan
command: speckit.plan
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
- id: review-plan
type: gate
message: "Review the plan before generating tasks."
options: [approve, reject]
on_reject: abort
- id: tasks
command: speckit.tasks
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
- id: implement
command: speckit.implement
integration: "{{ inputs.integration }}"
input:
args: "{{ inputs.spec }}"
This produces the following execution flow:
flowchart TB
A["specify<br/>(command)"] --> B{"review-spec<br/>(gate)"}
B -- approve --> C["plan<br/>(command)"]
B -- reject --> X1["⏹ Abort"]
C --> D{"review-plan<br/>(gate)"}
D -- approve --> E["tasks<br/>(command)"]
D -- reject --> X2["⏹ Abort"]
E --> F["implement<br/>(command)"]
style A fill:#49a,color:#fff
style B fill:#a94,color:#fff
style C fill:#49a,color:#fff
style D fill:#a94,color:#fff
style E fill:#49a,color:#fff
style F fill:#49a,color:#fff
style X1 fill:#999,color:#fff
style X2 fill:#999,color:#fff
Run it with:
specify workflow run speckit -i spec="Build a kanban board with drag-and-drop task management"
Test locally
specify workflow run ./workflow.yml --input key=value
specify workflow status <run_id>
specify workflow resume <run_id>
specify workflow add ./my-workflow --dev
specify workflow info my-workflow
run validates before execution and then executes the definition; it is not a dry-run validator. Use a disposable initialized project for examples that can modify files or invoke agents. There is no specify workflow validate command. info displays metadata and steps, not a complete safety review.
Cover valid and invalid inputs, gate verdicts and rejection policies, step failures, interrupted/failed resume, nested calls, and concurrent items when applicable. A completed step is not repeated on resume, but an uncheckpointed side effect can repeat; make external writes safe to retry.
Custom step packages
Custom step types are installed with specify workflow step. A step is a
directory package containing metadata and executable Python:
my-step/
├── step.yml # required, at the package root
├── __init__.py # required, at the package root
└── helpers.py # optional nested modules and data files
To prepare a public package for community catalog intake, see Community Workflow Step Types.
step.yml declares the step's identity. step.type_key must exactly match the
<step_id> passed on the command line — the ID is never inferred from package
content:
step:
type_key: my-step
name: My Step
version: 0.1.0
author: you
description: What this step does
__init__.py must define a StepBase subclass whose type_key matches:
from specify_cli.workflows.base import StepBase, StepResult
class MyStep(StepBase):
type_key = "my-step"
def execute(self, config, context):
return StepResult(output={"ok": True})
Package validation
Every source is validated identically before anything is committed:
step.ymland__init__.pymust be regular, non-symlink files at the package root.- The package tree is copied recursively (relative imports, nested helper modules, and data files are supported). A symlinked package root, any descendant symlink, and any filesystem object that is not a regular file or directory are rejected.
.git,__pycache__, and.DS_Storeentries are skipped without being inspected: they are not copied, excluded directories are not entered, and they do not count toward any limit.- The installed-package policy permits at most 512 retained entries (files and directories combined), at most 32 levels of directory nesting, and 50 MiB of retained content.
- Archive URLs also pass transport/extraction safety limits before package validation: at most 512 archive entries, 50 MiB downloaded or extracted, and 10 MiB per archive member. Catalog files have a 50 MiB per-response bound.
- Installation validates and copies the package but does not import or
execute
__init__.py. Installed custom step modules are loaded during startup ofworkflow add,workflow run, andworkflow resume, before any particular custom step necessarily executes.
Security note: Loading a custom step runs its Python with your privileges. Only install and retain step packages from sources you trust.
Implement a custom step
Subclass StepBase, give the package and implementation the same type_key, and return explicit status, outputs, and errors. Override validate() and guard invalid configuration in execute() too: execution does not automatically call the validator.
The registry shares a single instance of each type. Concurrent fan-out can call it from several threads, so keep execution stateless and thread-safe; store per-run data in config and context, not self. Let the engine own checkpoints and lifecycle transitions.
Use specify workflow step add my-step --dev ./my-step for local iteration and --force after editing. See the step installation reference for source validation and replacement failure behavior.
Bundle-local limitation
A bundle's provides.steps still resolves only through the active step
catalogs. Bundle-local steps/<id>/ payloads and relative
provides.steps[].source overrides are not resolved in this release, so
such steps are not installable offline. See the bundle offline limits.
Authoring practices
- Keep steps focused and place gates at meaningful decision points.
- Give inputs descriptive prompts, appropriate types, sensible defaults, and enums for fixed choices.
- Declare integration defaults and allow per-step overrides.
requires.integrationsdocuments expectations, not enforced permissions. - Read prior-step outputs only after those steps have run;
defaulthandles null or empty text, not every possible expression error. - Prefer strict shell steps with environment bindings for dynamic data. Document destructive operations, credentials, network calls, and other side effects. Gates do not make untrusted shell text safe.