Audience: project users adapting or composing custom templates from real-world patterns. Prereq: you've read the quickstart and know the schema basics from the reference.
Every recipe below is grounded in one of the built-in templates (
cp workflow show <name>). Use them as starting points:cp workflow new my-flow --from <built-in>.
Each recipe answers three questions:
- When to use it.
- What the YAML looks like.
- What to watch out for.
When: Most "small task" workflows. Discuss scope with the user first; only then do the work.
YAML:
workflow: small-task
version: 1
binds_to: quick
supervised: true
params:
- name: slug_with_date
phases:
- phase:
id: design
description: |
Agree on Approach + Done-When with the user before any
implementation. Mark status=ready in DESIGN.md only AFTER
explicit user confirmation.
STOP. Do not start implementation. The user's brief is a
starting point, NOT a green light. Ask follow-ups until both
Approach and Done-When are unambiguous.
role: tech-writer
skill: plan
outputs:
- ".planning/quick/{{slug_with_date}}/DESIGN.md"
prompt: |
Open DESIGN.md and work with the user to fill in Approach +
Done-When. Wait for explicit user confirmation before marking
complete.
- phase:
id: execute
description: Implement the change agreed in DESIGN.md.
depends_on: [design]
role: developer
skill: execute
prompt: |
Implement the change agreed in DESIGN.md. Commit atomically
per task; append progress notes to STATE.md.Watch out:
skill: planandskill: executeare routing keys — the engine resolves them through your active provider's skills map (see reference §Canonical routing keys).- The STOP language goes in both
description:andprompt:—description:is what the user sees incp workflow inspect;prompt:is what the harness reads. - The
outputs:field is a hard contract undersupervised: true: the sub-agent supervisor (lib/supervisor.js:261-264) rejects writes outside the declared paths, and the checkpoint logic uses the same list for backup/rollback. Declare every path the phase will write, and only those paths — see reference §outputs:for full semantics. binds_to: quickauto-injects thefinalizephase. You can omit it.
See: built-in quick (cp workflow show quick).
When: A planner phase decides how many sub-tasks are needed (1 –10); the runtime spawns one child phase per item.
YAML:
- phase:
id: plan
description: Decompose the milestone into 1-10 sub-features.
role: planner
skill: plan
max_children: 10
min_children: 1
materialize: inline # default; "roadmap-phases" for milestone
prompt: |
Return JSON:
{ "optimizable": false,
"items": [ { "id": "slug", "title": "..." } ] }
- phase:
id: child-plan
description: Plan one sub-feature in detail.
parent: plan # marks this as a child of `plan`
role: planner
skill: plan
prompt: |
Produce a detailed plan for the assigned sub-feature.
- phase:
id: child-execute
description: Execute one sub-feature.
parent: plan
after: [child-plan] # sequence siblings within the fan-out
role: implementer
skill: execute
prompt: |
Execute the planned sub-feature. Commit atomically per task.Watch out:
- Children must declare
parent:; without it, they're not part of the fan-out. - Children do not inherit
role:orskill:— spell them on every child. - The parent's JSON output drives spawning. Item count must be within
[min_children, max_children]or the runtime errors. after: [child-plan]sequences siblings within the same parent's fan-out (not across waves).
See: built-in dev (cp workflow show dev) and docs.
When: A value the supervisor (e.g., the slash-skill driver) knows at run-time but the template author doesn't.
YAML:
params:
# Defaults: routes you control at template-author time.
- name: design_skill
default: "plan"
# Supervisor-supplied: no default. Injected per run.
- name: task_description
- name: slug_with_date
phases:
- phase:
id: setup
description: Scaffold the quick directory.
kind: scaffold
command: "cp quick-setup --task '{{task_description}}'"
- phase:
id: design
description: Discuss the task with the user.
depends_on: [setup]
role: tech-writer
skill: "{{design_skill}}"
outputs:
- ".planning/quick/{{slug_with_date}}/DESIGN.md"
prompt: |
Open .planning/quick/{{slug_with_date}}/DESIGN.md and work with
the user on Approach + Done-When.Watch out:
- Declaring a
params:entry withoutdefault:is the only way to keep leftover{{name}}tokens after the expand pass. Any other unresolved token is an error. - The supervisor (your slash skill or driver) is responsible for injecting these values per-phase at run-time; the template just declares the contract.
See: built-in quick (params for slug_with_date and
task_description).
When: You want the template to work for any provider (Superpowers,
manual, etc.) without hard-coding skill paths, and you want users to
override skills per run via --param.
Pattern: param indirection. ${config.…} only resolves inside
params: default: values — never directly in phase fields. Declare
a param, default it from config, reference it via {{...}} in
phases:
params:
- name: plan_skill
default: "${config.provider.plan_skill}"
- name: review_skill
default: "${config.provider.review_skill}"
phases:
- phase:
id: plan
description: Plan the change.
role: planner
skill: "{{plan_skill}}"
prompt: |
Produce a detailed plan.
- phase:
id: review
description: Review the proposed change.
depends_on: [plan]
role: reviewer
skill: "{{review_skill}}"
prompt: |
Review for correctness, scope creep, and missed edge cases.Don't write skill: ${config.provider.plan_skill} directly in a
phase — the runtime's resolvePhaseSkill()
(lib/runtime.js:257–297) does not expand ${config.…}. The value
will pass through literally and the contract block will print a
warning like "Unknown skill ${config.provider.plan_skill}".
Watch out:
${config.path}references resolve at param-default time, populated fromlib/workflow-template-expand.js:CONFIG_FALLBACKSplus yourcp doctorconfig tree.- For a simpler one-off (no per-run override), use the bare routing
key directly:
skill: plan. The engine still resolves it through the active provider's skills map. ${config.…}is distinct from{{…}}: dotted tokens only work through${config.…}.{{provider.plan_skill}}is forbidden.
See: built-in quick and docs (both declare provider-skill
params at the top and reference them as {{plan_skill}},
{{review_skill}}, etc.).
When: You want cp to do the deterministic filesystem work
(scaffold a directory, write SUMMARY.md, tick a plan) while the
harness handles the open-ended thinking.
YAML:
phases:
- phase:
id: setup
description: Create the run directory.
kind: scaffold
command: "cp quick-setup {{slug_with_date}}"
- phase:
id: think
description: Plan + discuss.
depends_on: [setup]
role: planner
skill: plan
prompt: |
Draft the plan; check it with the user.
- phase:
id: finalize
description: Write SUMMARY.md and flip status=complete.
depends_on: [think]
kind: scaffold
command: "cp quick-finalize {{slug_with_date}}"Watch out:
kind: scaffoldrequirescommand:. Noprompt:, noskill:, norole:(the runtime runs the command itself).- The contract block for a scaffold phase prints
skill: (none)— there's nothing for the harness to do beyondmark-complete. - You can omit your own
finalizeand let the runtime auto-inject one (see Reference §binds_to).
See: built-in quick and milestone.
When: You want a human-readable persona label that's distinct from the routing key.
Anti-pattern:
- phase:
id: plan
role: plan # ← validator warns: 'plan' is a routing key
prompt: |
Plan the change.The validator (v1.5 rule) warns: "role 'plan' looks like a routing
key — role is persona only; use skill: plan instead and set role to
a persona (e.g., developer, tech-writer)."
Pattern:
- phase:
id: plan
role: tech-writer # persona
skill: plan # routing key
prompt: |
Plan the change.
- phase:
id: investigate
role: analyst # custom persona
skill: brainstorm # explicit routing
prompt: |
Investigate the cause before proposing a fix.Watch out:
role:is persona-only; the v1.6 contract block shows it for the human reader but doesn't use it to route work.- If both
role:andskill:are routing keys and they disagree, the validator fails (role 'plan' and skill 'execute' both as routing keys — they must agree). - Use canonical routing keys in
skill:; use free personas inrole:.
See: reference §Canonical routing keys
for the full list (brainstorm, plan, execute, review,
verify, debug, worktree, tdd, finish, …).
When: Most authors want supervised: true. Reach for false
only for short fully-automated chains.
Supervised (default for built-ins):
workflow: review-pass
version: 1
binds_to: quick
supervised: true # cp run prints one wave at a time
phases:
- phase:
id: review
description: ...
role: reviewer
skill: review
prompt: |
Review the diff.cp run review-pass prints wave 1 with the contract block, then
exits. You drive forward with
cp run mark-complete <slug> review, which prints wave 2 (or
finalize), and so on.
Unsupervised:
supervised: false # engine drives all waves itselfNo mark-complete calls; the engine fires every phase in sequence
without waiting for human acknowledgement.
Watch out:
supervised: falseskips the v1.6 contract block — there is no per-wave checkpoint for a human to read.- All built-ins use
supervised: true. There is rarely a good reason for a custom template to be unsupervised.
When: Your planner phase knows the exact dependency graph between items and you want the runtime to execute independent items in parallel.
Default (sequential):
The planner returns:
{
"optimizable": false,
"items": [
{ "id": "a", "title": "..." },
{ "id": "b", "title": "..." },
{ "id": "c", "title": "..." }
]
}Runtime executes a → b → c in list order. Any depends_on on the
items is ignored.
Opt-in DAG:
The planner returns:
{
"optimizable": true,
"items": [
{ "id": "schema", "title": "...", "depends_on": [] },
{ "id": "migrate", "title": "...", "depends_on": ["schema"] },
{ "id": "seed", "title": "...", "depends_on": ["schema"] },
{ "id": "verify", "title": "...", "depends_on": ["migrate", "seed"] }
]
}Runtime runs schema, then migrate + seed in parallel, then
verify. The graph is computed from depends_on.
Watch out:
optimizable: trueis only meaningful if every item declaresdepends_on(use[]for no deps). One missing entry and the runtime falls back to sequential.- If you're unsure about any dependency, set
optimizable: falseand list items in safe order. Mistaken parallelisation is harder to debug than slow sequential execution. - This flag is in the planner's JSON output, not in the template.
See: built-in dev (cp workflow show dev) and docs.
For the milestone variant that pairs a planner with an implementer per proposed roadmap phase (the v1.8 default), see Recipe 9.
When: A propose-phases planner emits a list of roadmap
phases, and you want each one driven through plan + execute
automatically by the supervisor.
This is what the built-in milestone workflow ships in v1.8.
YAML (excerpted from templates/workflows/milestone.yaml):
- phase:
id: propose-phases
description: Propose 1-N roadmap phases for this milestone.
role: planner
skill: plan
max_children: 12
min_children: 1
materialize: roadmap-phases # write children to ROADMAP.md
prompt: |
Return JSON:
{ "items": [ { "id": "phase-slug", "title": "..." } ] }
- phase:
id: child-plan
description: Plan one roadmap phase in detail.
parent: propose-phases
role: planner
skill: plan
prompt: |
Produce a detailed PLAN.md for the assigned phase.
- phase:
id: child-execute
description: Execute one roadmap phase.
parent: propose-phases
after: [child-plan]
role: implementer
skill: execute
prompt: |
Execute the planned phase. Commit atomically per task.What materialize: roadmap-phases does (vs. inline):
- Writes each item into
.planning/ROADMAP.mdunder the active milestone as a real planned phase entry, so the milestone's on-disk roadmap reflects what the supervisor will execute. - Hard-fails on malformed or empty input — no silent zero-child drift (v1.8 behavior; previously, bad input produced zero children quietly).
Watch out:
- Children are hidden from the supervisor-facing wave block until
the parent is
mark-completed (v1.8 behavior). This is by design — the planner decides how many children exist, so they don't appear in scaffolding output ahead of time. - The planner-emitted item count must be within
[min_children, max_children], same as inline fan-out. - An optional review phase between
propose-phasesand child materialization is supported in v1.8's built-in milestone workflow; useful when the planner's first roadmap pass deserves a sanity check before execution.
See: built-in milestone (cp workflow show milestone), and
the v1.8 entry in CHANGELOG.md for the design rationale.