Custom workflows
A workflow is a manifest: an ordered list of phases, each declaring what must be true (gates) before the run advances. Project workflows live at {repo}/.paintedwolf/workflows/<id>/workflow.yaml and surface in the app exactly like the bundled ones. The engine contract is workflows.md in the product repository.
A complete manifest
# {repo}/.paintedwolf/workflows/triage/workflow.yaml
id: triage
version: 1.0.0
name: Triage
description: Fan out read-only scouts, then report — no code changes.
trigger: /triage
phases:
- id: investigate
coordinator_surface: orchestrate_plan
surface_template: agents/coordinator-surface-orchestrate.md
mode_refs: [plan-research]
complete_when: gates_satisfied
gates: [human_approval]
next: done
- id: done
coordinator_surface: await_user
surface_template: agents/coordinator-surface-plan.md
mode_refs: [await-user-input]
terminal: true
complete_when: orchestration_complete
Every phase in a project workflow — the terminal one included — sets all three surface fields. That is the one thing overlays are stricter about than the shipped manifests, and it is the most common first validation failure.
Build your first pack walks one manifest end to end.
Phase anatomy
| Key | Role |
|---|
complete_when | The gate expression that lets the phase finish |
gates | Leaves that must all be met when complete_when: gates_satisfied |
next / terminal | The spine — where the run goes, or where it ends |
coordinator_surface, surface_template, mode_refs | Which tools and prompt the coordinator gets — explicit and required in project overlays |
transitions | Optional off-spine choices, fired by the human or the coordinator |
Terminal phases must complete with orchestration_complete. A phase that declares evidence_passed:verify requires the project’s test command to pass; see Verification.
Coordinator surfaces
coordinator_surface must name one of the shipped surfaces. The surface names the phase’s tools. Naming a surface that isn’t in this list is a load error, and a phase that sets coordinator_surface must also set surface_template.
| Surface | What the coordinator is doing | Tools |
|---|
implement_routing | Deciding what to dispatch next — a lean set with no file mutation, so routing turns stay cheap. | task worker_cancel answer_decision extend_worker_budget update_progress promote_overlay pack_board list_dir web_search fetch_url wait scan_query scan_summary scan_list scan_compare ask_user surface_note request_tools |
implement_dispatch | Workers are in flight; the coordinator dispatches, cancels, answers decisions, and records progress. | task worker_cancel answer_decision extend_worker_budget update_progress promote_overlay reject_overlay wait ask_user submit_verdict surface_note |
implement_synthesis | The wrap-up turn — read and search the tree to write a grounded report; no dispatch, no writes. | read grep find jq list_dir survey_repo summarize scan_pack web_search fetch_url render_view update_progress scan_query scan_summary scan_list scan_compare request_tools |
orchestrate_plan | Planning a run — read, search, and scan the project, then fan out a plan or advance the workflow. | read grep find jq list_dir survey_repo summarize web_search fetch_url scan_pack scan_query scan_summary scan_list scan_compare fanout_plan workflow_advance ask_user surface_note request_tools |
implement_overlay_promote | Promote-eligible write overlays are queued; the coordinator inspects, promotes, or rejects them before starting new work. | promote_overlay reject_overlay preview_overlay worker_cancel answer_decision extend_worker_budget update_progress task pack_board read grep jq list_dir survey_repo summarize web_search fetch_url command verify wait scan_query scan_summary scan_list render_view capture_page measure_page page_open page_act page_snapshot page_close terminal_open terminal_send terminal_read terminal_snapshot terminal_close ask_user surface_note request_tools |
implement_investigate | The coordinator works the tree itself — read, edit, run, and verify — with task available when a leg is worth parallelizing. | read grep summarize survey_repo skills_read diff find jq list_dir stat wc write edit replace_lines code_rewrite chmod delete git_status git_diff git_log git_show git_blame git_ref git_branches git_commit git_restore web_search fetch_url command verify task update_progress worker_cancel answer_decision wait pack_board ask_user surface_note scan_pack scan_query scan_summary scan_list scan_compare render_view capture_page measure_page page_open page_act page_snapshot page_close terminal_open terminal_send terminal_read terminal_snapshot terminal_close request_tools |
implement_park | Sibling work is still running; lifecycle tools only while the coordinator waits. | wait worker_cancel answer_decision extend_worker_budget update_progress ask_user surface_note |
workflow_compose | Composing a workflow manifest from the conversation and persisting it to the catalog. | workflow_compose workflow_compose_from_template workflow_catalog_summaries workflow_persist ask_user surface_note |
plan_stub | Drafting the first blueprint stub — write the document, record progress, look things up. | read write edit replace_lines update_progress grep web_search fetch_url surface_note |
plan_research | Researching the blueprint and dispatching research workers, then advancing when the picture is complete. | read write edit replace_lines update_progress grep list_dir survey_repo summarize web_search fetch_url pack_board task worker_cancel workflow_advance surface_note |
plan_approve | Finishing the blueprint for the human’s approval — revise the document, no worker dispatch. | read write edit replace_lines update_progress grep web_search fetch_url surface_note |
plan_review | The critique loop — revise the blueprint, dispatch the reviewer, and record the verdict that ends the phase. | read grep list_dir survey_repo summarize write edit replace_lines update_progress pack_board task worker_cancel wait submit_verdict surface_note |
await_user | A human answer is outstanding; the coordinator may ask, wait, and read, but not act on the project. | ask_user wait workflow_advance read grep list_dir |
plan_build | Turning an approved blueprint into dispatched legs, including workflow rally departures. | read write edit replace_lines update_progress grep web_search fetch_url pack_board task worker_cancel delegate_dispatch surface_note |
no_folder_allowlist | No folder is open — an allow-list intersected with whatever surface the phase names, so filesystem tools never appear. | web_search fetch_url wait update_progress answer_decision task worker_cancel extend_worker_budget ask_user surface_note |
Posture
Every run carries a session posture: a coordinator approval bucket that decides which tool profile and rule set the coordinator gets. This is a different axis from the approval posture in Settings — see Approvals.
The shipped set is closed — overlays may retune these, but cannot add a fifth:
| Posture | What the coordinator may do | Tool profile |
|---|
spec | Coordinator specifies (plans/docs); no delegation dispatch | coordinator |
build | Implementation + delegation closeout | coordinator |
orchestrate | Multi-worker topology sessions | coordinator |
vet | Readonly coordinator; security proof required at closeout | worker_readonly |
initial_posture: spec
phases:
- id: draft
next: implement
- id: implement
on_enter:
set_posture: build
A run records the session’s posture before it begins and restores it when it ends. To retune one for a project, add {repo}/.paintedwolf/postures.yaml. Each of the four ids accepts tool_profile, rules, description, and label.
The gate kit
Project workflows compose from the public gate kit. This vocabulary is generated from the engine — it is the exact set the host evaluates:
Show the full workflow gate kit
Static leaves
| Gate | Met when |
|---|
gates_satisfied | The phase completes once every leaf in its gates: list is met. The workhorse complete_when for proof-style phases. |
orchestration_complete | The run’s orchestration is closed out — required complete_when on every terminal phase. |
child_run_complete | A child workflow started with invoke_workflow finished successfully. |
child_run_failed | The invoked child workflow run ended in failure — pair with a recovery phase. |
hitl_approved | The human-in-the-loop consult ended in approval. |
human_approval | The human granted this phase’s approval request, declared under the phase’s human_approval: block. |
Parameterized leaves
Append an argument after the prefix — evidence_passed:verify, hitl_consulted:approve.
| Prefix | Met when |
|---|
var_equals: | A named run variable equals the expected value. |
var_truthy: | A named run variable is set and truthy. |
user_decision_received: | The user answered the named decision request. |
user_feedback_received: | User feedback arrived for the named request. |
user_decision: | The user’s recorded decision matches the named option. |
hitl_consulted: | The human was consulted during the named phase. |
evidence_passed: | The named evidence check recorded a pass — for example evidence_passed:verify. |
phase_is: | The run is currently in the named phase. |
phase_skipped: | The named phase was skipped. |
gate_passed: | The named gate leaf already passed earlier in the run. |
Bundled-only leaves
These appear in the shipped workflows but are rejected in project overlays (domain_leaf_on_overlay) — they depend on host internals the generic kit doesn't expose:
plan_stub_valid · research_satisfied · topology_stage_complete · delegation_closeout_complete · closeout_gates_passed · recon_or_board_ready · worker_cycle_ready · topology_report_delivered · fanout_planned · parallel_stages_complete · implement_workflow_ready
Choice transitions
A phase may offer named exits off the next: spine. Transitions render as buttons in the app and as a tool on the coordinator surface.
transitions:
- id: critique
to: review
actors: [human, coordinator]
label: Run critique
Manifest reference
Generated from the loader — every field it parses, and nothing it doesn’t:
Show every workflow manifest field (133)
Top level
| Field | Type | Meaning |
|---|
id | string | Stable workflow id — the catalog key, the own/disabled unit id (workflows/<id>), and what a child invoke_workflow names. |
version | string | Manifest version, required. Registry keys are id@version, so two versions of a workflow can be loaded side by side. |
attach | block | Whether this workflow attaches to a session on its own rather than waiting for its trigger. |
extends | string | Inherit another manifest in the same directory and override selected keys; lists merge, scalars replace. Resolved before validation. |
name | string | Display name in the workflow catalog and the run header. |
description | string | One-line summary shown beside the name in the catalog. |
trigger | string | The slash command that starts the run (/triage). Routing is by exact command — the host never infers a workflow from prose. |
initial_posture | string | The session posture the run starts in; the run restores the session’s previous posture when it ends. |
icon | string | Catalog icon name for the app’s workflow list. |
featured | bool | Surfaces the workflow in the app’s featured list rather than only the full catalog. |
requires_repo | bool | The workflow refuses to start with no folder open — set it when every phase assumes a project tree. |
coordinator_profile | string | Tool profile the coordinator runs under for the whole run, when the workflow needs something other than the posture default. |
surface_profile | string | A named surface profile supplying per-phase surface defaults. Project overlays must still set the surface fields explicitly. |
agents | []block | Agent bindings this run may dispatch, including the tool ceiling each agent receives. |
gates | []string | Run-level gate leaves that must be met before the run can close out, in addition to each phase’s own gates. |
rules | []string | Rule unit paths this workflow activates for the length of the run — validated at load, so a missing rule file fails the manifest. |
topology | string | The fan-out topology this run binds to; phases then bind to its stages with bind_topology_stage. |
controls | block | Run-wide behavior switches — who advances phases, what happens on pause, stop, and decision rejection. |
phases | []block | The ordered phase list — the run’s spine. Each entry declares what must be true before the run leaves it. |
parameters | map[string]block | Typed run parameters with defaults. Values land in run variables under params.<name> and can be read by gates. |
blueprint | block | Declares the run’s governing document — the plan the workflow writes and the human approves. |
presets | []block | Named parameter recipes for the same manifest — one workflow, several ways to start it. |
injects | []block | Workflow-tier inform bindings — prompt content rendered at a lifecycle anchor while this run is active. |
attach
| Field | Type | Meaning |
|---|
policy | string | Only session_create is accepted — the workflow attaches when a session is created (this is how the ambient implement workflow runs). Omit it for ordinary slash-command workflows. |
agents
| Field | Type | Meaning |
|---|
id | string | Agent id available to the workflow; any id not listed here is rejected at dispatch. |
tools | string | Tool profile or explicit tool ceiling applied when this workflow dispatches the agent. |
spawn | bool | Whether this agent may dispatch its own child agents while working for the workflow. |
controls
| Field | Type | Meaning |
|---|
phase_advance | string | host hands phase advance to the host instead of the coordinator; omit it to let the coordinator advance with workflow_advance. |
default_execution_mode | string | Default coordinator execution mode for the run: investigate (work the tree directly), orchestrate (dispatch), or state_derived (follow the run state). |
on_decision_reject | block | What the run does when the human rejects a decision it asked for. |
on_pause | block | What happens to work in flight when the run pauses. |
on_stop | block | What a stop tears down. |
content_review | block | Run-wide human review of edits — which tools and paths must be shown to the human before they apply. |
report | block | End-of-run report behavior. |
controls.on_decision_reject
| Field | Type | Meaning |
|---|
pause | bool | Pause the run on rejection so the human can redirect it. |
cancel | bool | Cancel the run outright on rejection. |
controls.on_pause
| Field | Type | Meaning |
|---|
hold_pending | bool | Hold queued work instead of discarding it, so a resume picks up where it stopped. |
cancel_running | bool | Cancel workers that are already running rather than letting them finish. |
controls.on_stop
| Field | Type | Meaning |
|---|
cancel_workers | bool | Cancel every worker the run dispatched. |
abort_delegation | bool | Abort the delegation batch rather than letting queued legs drain. |
session_abort | bool | Abort the session’s current turn as well as the run. |
controls.content_review
| Field | Type | Meaning |
|---|
tools | []string | Tool names whose edits require review (typically the write tools). |
paths | []string | Path globs whose edits require review. |
controls.report
| Field | Type | Meaning |
|---|
enabled | bool | Produce the run’s closing report when the run completes. |
phases
| Field | Type | Meaning |
|---|
id | string | Phase id, unique within the manifest; next:, transitions.to, and every gate that names a phase use it. |
activity_label | string | Short user-facing label shown while this phase is active. |
complete_when | string | The condition that lets the phase finish — gates_satisfied for proof-style phases, orchestration_complete on terminal phases. |
entry_when | string | Optional condition the run must satisfy to enter the phase at all; an unmet entry condition skips it. Must be a known gate leaf. |
invoke_workflow | block | Run another workflow as a child from this phase. The phase must use complete_when gates_satisfied with child_run_complete in its gates. |
invoke_trigger | string | When the child starts — phase_enter (default) or coordinator_tool, letting the coordinator decide the moment. |
next | string | The phase the run advances to on the spine. Omit it only on a terminal phase. |
skippable | bool | The phase may be skipped by the host when its work is not needed — for example when a depth parameter resolves to none. |
on_enter | block | Effects applied once as the run enters the phase. |
on_reenter | block | Effects applied when the run comes back to a phase it already visited — a returning worker, a loop, a rejected review. |
controls | block | Phase-scoped overrides of the run-wide controls: block. |
scan_on_enter | []string | Scanner categories to run as the phase opens; results are available to the phase’s gates and guidance. |
scan_on_exit | []string | Scanner categories to run as the phase closes — the usual place for a security sweep before hand-off. |
gates | []string | The leaves that must all be met when complete_when is gates_satisfied. Compose them from the gate kit above. |
require_hitl | bool | The phase cannot complete without a human-in-the-loop consult. Cannot be combined with request_user_feedback. |
bind_topology_stage | string | Bind the phase to a named stage of the manifest’s topology, so the stage’s fan-out belongs to this phase. |
bind_parallel_group | []string | Parallel group names whose legs this phase owns — how one phase waits on several concurrent legs. |
parallel_task | block | Concurrency ceiling for workers dispatched from this phase. |
touch | block | The paths this phase’s delegated work is expected to touch. |
terminal | bool | Marks the end of the spine. Terminal phases must complete with orchestration_complete. |
coordinator_surface | string | The capability surface the coordinator holds during this phase — one of the shipped surface ids. Required, with surface_template, in project overlays. |
surface_template | string | The coordinator prompt file for the surface. Required whenever coordinator_surface is set. |
mode_refs | []string | Additional coordinator mode templates layered into the phase prompt. |
advance | block | Who advances once the phase’s gate is met. |
loop | block | Where the phase goes when its loop iterates. |
intake | []string | Intake keys collected from the human in this phase; each answer lands in run variables under intake.<key> and can gate the phase. |
human_approval | block | Declares this phase’s approval request — the human checkpoint the app renders as a button. |
review_loop | block | The reviewer-verdict convention — dispatch a reviewer, record a structured verdict as evidence, iterate up to a cap. |
depth_param | string | Names a depth-typed parameter that scales this phase — none skips it, light and thorough widen the fan-out. |
transitions | []block | Named exits off the next: spine, rendered as buttons and offered to the coordinator as a tool. |
phases.invoke_workflow
| Field | Type | Meaning |
|---|
workflow_id | string | The child workflow’s id. |
version | string | The child manifest version to run — pinned, so a new version of the child cannot change this run silently. |
phases.on_enter
| Field | Type | Meaning |
|---|
set_posture | string | Move the session posture on entry — this is what keeps a planning phase from starting to edit files. |
set_execution_mode | string | Force the coordinator’s execution mode for this phase instead of deriving it from run state. |
inject_kick | string | Rejected on load — author phase-entry guidance as a top-level injects: entry on phase.entered instead. |
prompt_coordinator | bool | Prompt the coordinator immediately on entry rather than waiting for the next user turn. |
request_user_feedback | block | Ask the human a question as the phase opens; the answer lands in run variables and can gate the phase. |
phases.on_enter.request_user_feedback
| Field | Type | Meaning |
|---|
prompt | string | The question text shown to the human. Required. |
response_type | string | text (default), single_choice, or multi_choice. Choice types require at least two options. |
options | []string | The offered choices — only valid with a choice response type. |
allow_other | bool | Let the human answer outside the offered options. |
phases.on_reenter
| Field | Type | Meaning |
|---|
inject_kick | string | Guidance template rendered to the coordinator on re-entry. |
reenter_leg | string | The delegation leg to resume on re-entry. |
phases.controls
| Field | Type | Meaning |
|---|
content_review | block | Human review of edits for this phase only. |
phases.controls.content_review
| Field | Type | Meaning |
|---|
tools | []string | Tool names whose edits require review during this phase. |
paths | []string | Path globs whose edits require review during this phase. |
phases.parallel_task
| Field | Type | Meaning |
|---|
max_workers | int | Maximum workers in flight at once. Must be greater than zero when the block is present. |
max_read_workers | int | Sub-ceiling for read-only workers. |
max_write_workers | int | Sub-ceiling for workers that may write — the knob that limits overlay churn. |
phases.touch
| Field | Type | Meaning |
|---|
paths | []string | Path globs handed to workers as their focus paths for this phase. |
phases.advance
| Field | Type | Meaning |
|---|
when_gate_met | string | auto advances the moment the gate passes; coordinator leaves the leave-turn to the coordinator. |
phases.loop
| Field | Type | Meaning |
|---|
exit | string | next leaves down the spine; self re-enters this phase — the shape behind review and verify loops. |
phases.human_approval
| Field | Type | Meaning |
|---|
blueprint | string | The blueprint id being approved; the host records the approved content hash so a later edit invalidates the approval. |
readiness | string | A condition that must hold before the approval is even offered, so the human is not asked to approve half-finished work. |
phases.review_loop
| Field | Type | Meaning |
|---|
evidence_key | string | The evidence key the verdict is recorded under; satisfy the phase with evidence_passed:<key>. Required, and the manifest must reference it in a gate. |
iteration_cap | int | Maximum review rounds before the loop stops asking (default 3). |
verdict_schema | map[string]string | Field name to type for the structured verdict the reviewer submits. |
phases.transitions
| Field | Type | Meaning |
|---|
id | string | Stable wire key for the transition — pick it like an API name. |
to | string | Destination phase id. Self-edges are rejected. |
actors | []string | Who may fire it — human, coordinator, or both. Required. |
label | string | Button text shown to the human. Required. |
when | string | Optional readiness condition; while it is false the transition is not armed. Omit it to keep the exit always available on this phase. |
parameters
| Field | Type | Meaning |
|---|
type | string | The parameter type. depth is the typed one the host understands (none / light / thorough) and the only type depth_param accepts. |
default | string | The value used when neither a preset nor the start request supplies one. |
blueprint
| Field | Type | Meaning |
|---|
id | string | Blueprint id referenced by human_approval.blueprint and by depth-parameter stamping. Required when the block is present. |
file | string | Blueprint filename under the project’s .lycaon/blueprints/ convention directory. |
frontmatter | []string | Frontmatter keys the document must carry, so downstream phases can rely on them. |
presets
| Field | Type | Meaning |
|---|
id | string | Preset id; recorded in run variables as workflow.preset_id. |
name | string | Display name in the catalog. Defaults to the id. |
description | string | One-line summary of what this preset changes. |
trigger | string | An optional slash command that starts the workflow with this preset already applied. |
params | map[string]string | Parameter values this preset overlays on the manifest defaults. Required — a preset that changes nothing is rejected. |
injects
| Field | Type | Meaning |
|---|
on | string | The anchor id that fires the inject, from the same catalog rules bind to. |
selector | block | Narrows when the inject fires. Clauses AND together; an omitted clause matches everything. |
effect | string | inform — a workflow inject renders prompt content. Blocking is the rules plane, not this one. |
when | string | Optional CEL condition over the same fact catalogue rules use, evaluated when the anchor fires. |
render | string | The guidance template to render, by template ref — a guidance/ file your pack ships. |
tier | string | Must be workflow (the default) — a manifest can only author workflow-tier bindings. Standalone bindings ship as host/bindings/ units. |
dedup | yaml | Raw dedup policy for this inject, following the anchor catalog’s dedup grammar — omit it to inherit the anchor’s own policy. |
injects.selector
| Field | Type | Meaning |
|---|
surface | string | Fire only for this anchor surface — coordinator, worker, or phase. |
phase | string | Fire only in the named phase. |
workflow | string | Fire only for the named workflow id. |
tool | string | Fire only for a single tool name. |
tools | []string | Fire for any tool in the list. |
profiles | []string | Fire only for agents running under these tool profiles. |
surfaces | []string | Fire only on these coordinator surfaces. |
session_posture | []string | Fire only in these session postures. |
principal | any | Scope the inject to a caller identity — the multi-tenant clause shared with rule selectors. |
ledger | map[string]any | Match against evidence-ledger facts the host records for the run. |
Shapes
The blocks worth seeing whole — blueprint, human_approval, review_loop, invoke_workflow, parallel_task, and run variables — are in the product repository’s workflows.md. The shape the tutorial uses is the human checkpoint:
- id: approve
coordinator_surface: plan_approve
surface_template: agents/coordinator-surface-plan.md
mode_refs: [plan-stub]
complete_when: gates_satisfied
gates: [human_approval]
advance:
when_gate_met: auto
human_approval:
blueprint: plan
readiness: plan_stub_valid
next: build
Chat never approves — Approve is a host action. advance.when_gate_met: auto is required on a surface that has no workflow_advance tool.
Validate before you run
pw workflow validate # project overlays + bundled base
pw workflow validate --json # machine-readable diagnostics
Green means the manifest loads, every phase is leaveable with the tools and gates the host evaluates, and every catalog reference resolves. It cannot prove model behavior. Diagnostics carry stable codes — the generated table is in the Reference.
What overlays can’t do
- Bundled-only gates — domain leaves are rejected with
domain_leaf_on_overlay. - Implicit surfaces — overlays must set
coordinator_surface, surface_template, and mode_refs per phase. - Routing on prose — triggers are explicit slash commands; Approve is a host action button, never a chat phrase.
Phase-entry guidance is an injects: entry — see Prompts.