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

KeyRole
complete_whenThe gate expression that lets the phase finish
gatesLeaves that must all be met when complete_when: gates_satisfied
next / terminalThe spine — where the run goes, or where it ends
coordinator_surface, surface_template, mode_refsWhich tools and prompt the coordinator gets — explicit and required in project overlays
transitionsOptional 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.

SurfaceWhat the coordinator is doingTools
implement_routingDeciding 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_dispatchWorkers 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_synthesisThe 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_planPlanning 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_promotePromote-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_investigateThe 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_parkSibling 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_composeComposing 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_stubDrafting 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_researchResearching 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_approveFinishing 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_reviewThe 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_userA 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_buildTurning 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_allowlistNo 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:

PostureWhat the coordinator may doTool profile
specCoordinator specifies (plans/docs); no delegation dispatchcoordinator
buildImplementation + delegation closeoutcoordinator
orchestrateMulti-worker topology sessionscoordinator
vetReadonly coordinator; security proof required at closeoutworker_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

GateMet when
gates_satisfiedThe phase completes once every leaf in its gates: list is met. The workhorse complete_when for proof-style phases.
orchestration_completeThe run’s orchestration is closed out — required complete_when on every terminal phase.
child_run_completeA child workflow started with invoke_workflow finished successfully.
child_run_failedThe invoked child workflow run ended in failure — pair with a recovery phase.
hitl_approvedThe human-in-the-loop consult ended in approval.
human_approvalThe 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.

PrefixMet 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

FieldTypeMeaning
idstringStable workflow id — the catalog key, the own/disabled unit id (workflows/<id>), and what a child invoke_workflow names.
versionstringManifest version, required. Registry keys are id@version, so two versions of a workflow can be loaded side by side.
attachblockWhether this workflow attaches to a session on its own rather than waiting for its trigger.
extendsstringInherit another manifest in the same directory and override selected keys; lists merge, scalars replace. Resolved before validation.
namestringDisplay name in the workflow catalog and the run header.
descriptionstringOne-line summary shown beside the name in the catalog.
triggerstringThe slash command that starts the run (/triage). Routing is by exact command — the host never infers a workflow from prose.
initial_posturestringThe session posture the run starts in; the run restores the session’s previous posture when it ends.
iconstringCatalog icon name for the app’s workflow list.
featuredboolSurfaces the workflow in the app’s featured list rather than only the full catalog.
requires_repoboolThe workflow refuses to start with no folder open — set it when every phase assumes a project tree.
coordinator_profilestringTool profile the coordinator runs under for the whole run, when the workflow needs something other than the posture default.
surface_profilestringA named surface profile supplying per-phase surface defaults. Project overlays must still set the surface fields explicitly.
agents[]blockAgent bindings this run may dispatch, including the tool ceiling each agent receives.
gates[]stringRun-level gate leaves that must be met before the run can close out, in addition to each phase’s own gates.
rules[]stringRule unit paths this workflow activates for the length of the run — validated at load, so a missing rule file fails the manifest.
topologystringThe fan-out topology this run binds to; phases then bind to its stages with bind_topology_stage.
controlsblockRun-wide behavior switches — who advances phases, what happens on pause, stop, and decision rejection.
phases[]blockThe ordered phase list — the run’s spine. Each entry declares what must be true before the run leaves it.
parametersmap[string]blockTyped run parameters with defaults. Values land in run variables under params.<name> and can be read by gates.
blueprintblockDeclares the run’s governing document — the plan the workflow writes and the human approves.
presets[]blockNamed parameter recipes for the same manifest — one workflow, several ways to start it.
injects[]blockWorkflow-tier inform bindings — prompt content rendered at a lifecycle anchor while this run is active.

attach

FieldTypeMeaning
policystringOnly 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

FieldTypeMeaning
idstringAgent id available to the workflow; any id not listed here is rejected at dispatch.
toolsstringTool profile or explicit tool ceiling applied when this workflow dispatches the agent.
spawnboolWhether this agent may dispatch its own child agents while working for the workflow.

controls

FieldTypeMeaning
phase_advancestringhost hands phase advance to the host instead of the coordinator; omit it to let the coordinator advance with workflow_advance.
default_execution_modestringDefault coordinator execution mode for the run: investigate (work the tree directly), orchestrate (dispatch), or state_derived (follow the run state).
on_decision_rejectblockWhat the run does when the human rejects a decision it asked for.
on_pauseblockWhat happens to work in flight when the run pauses.
on_stopblockWhat a stop tears down.
content_reviewblockRun-wide human review of edits — which tools and paths must be shown to the human before they apply.
reportblockEnd-of-run report behavior.

controls.on_decision_reject

FieldTypeMeaning
pauseboolPause the run on rejection so the human can redirect it.
cancelboolCancel the run outright on rejection.

controls.on_pause

FieldTypeMeaning
hold_pendingboolHold queued work instead of discarding it, so a resume picks up where it stopped.
cancel_runningboolCancel workers that are already running rather than letting them finish.

controls.on_stop

FieldTypeMeaning
cancel_workersboolCancel every worker the run dispatched.
abort_delegationboolAbort the delegation batch rather than letting queued legs drain.
session_abortboolAbort the session’s current turn as well as the run.

controls.content_review

FieldTypeMeaning
tools[]stringTool names whose edits require review (typically the write tools).
paths[]stringPath globs whose edits require review.

controls.report

FieldTypeMeaning
enabledboolProduce the run’s closing report when the run completes.

phases

FieldTypeMeaning
idstringPhase id, unique within the manifest; next:, transitions.to, and every gate that names a phase use it.
activity_labelstringShort user-facing label shown while this phase is active.
complete_whenstringThe condition that lets the phase finish — gates_satisfied for proof-style phases, orchestration_complete on terminal phases.
entry_whenstringOptional condition the run must satisfy to enter the phase at all; an unmet entry condition skips it. Must be a known gate leaf.
invoke_workflowblockRun another workflow as a child from this phase. The phase must use complete_when gates_satisfied with child_run_complete in its gates.
invoke_triggerstringWhen the child starts — phase_enter (default) or coordinator_tool, letting the coordinator decide the moment.
nextstringThe phase the run advances to on the spine. Omit it only on a terminal phase.
skippableboolThe phase may be skipped by the host when its work is not needed — for example when a depth parameter resolves to none.
on_enterblockEffects applied once as the run enters the phase.
on_reenterblockEffects applied when the run comes back to a phase it already visited — a returning worker, a loop, a rejected review.
controlsblockPhase-scoped overrides of the run-wide controls: block.
scan_on_enter[]stringScanner categories to run as the phase opens; results are available to the phase’s gates and guidance.
scan_on_exit[]stringScanner categories to run as the phase closes — the usual place for a security sweep before hand-off.
gates[]stringThe leaves that must all be met when complete_when is gates_satisfied. Compose them from the gate kit above.
require_hitlboolThe phase cannot complete without a human-in-the-loop consult. Cannot be combined with request_user_feedback.
bind_topology_stagestringBind the phase to a named stage of the manifest’s topology, so the stage’s fan-out belongs to this phase.
bind_parallel_group[]stringParallel group names whose legs this phase owns — how one phase waits on several concurrent legs.
parallel_taskblockConcurrency ceiling for workers dispatched from this phase.
touchblockThe paths this phase’s delegated work is expected to touch.
terminalboolMarks the end of the spine. Terminal phases must complete with orchestration_complete.
coordinator_surfacestringThe capability surface the coordinator holds during this phase — one of the shipped surface ids. Required, with surface_template, in project overlays.
surface_templatestringThe coordinator prompt file for the surface. Required whenever coordinator_surface is set.
mode_refs[]stringAdditional coordinator mode templates layered into the phase prompt.
advanceblockWho advances once the phase’s gate is met.
loopblockWhere the phase goes when its loop iterates.
intake[]stringIntake keys collected from the human in this phase; each answer lands in run variables under intake.<key> and can gate the phase.
human_approvalblockDeclares this phase’s approval request — the human checkpoint the app renders as a button.
review_loopblockThe reviewer-verdict convention — dispatch a reviewer, record a structured verdict as evidence, iterate up to a cap.
depth_paramstringNames a depth-typed parameter that scales this phase — none skips it, light and thorough widen the fan-out.
transitions[]blockNamed exits off the next: spine, rendered as buttons and offered to the coordinator as a tool.

phases.invoke_workflow

FieldTypeMeaning
workflow_idstringThe child workflow’s id.
versionstringThe child manifest version to run — pinned, so a new version of the child cannot change this run silently.

phases.on_enter

FieldTypeMeaning
set_posturestringMove the session posture on entry — this is what keeps a planning phase from starting to edit files.
set_execution_modestringForce the coordinator’s execution mode for this phase instead of deriving it from run state.
inject_kickstringRejected on load — author phase-entry guidance as a top-level injects: entry on phase.entered instead.
prompt_coordinatorboolPrompt the coordinator immediately on entry rather than waiting for the next user turn.
request_user_feedbackblockAsk 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

FieldTypeMeaning
promptstringThe question text shown to the human. Required.
response_typestringtext (default), single_choice, or multi_choice. Choice types require at least two options.
options[]stringThe offered choices — only valid with a choice response type.
allow_otherboolLet the human answer outside the offered options.

phases.on_reenter

FieldTypeMeaning
inject_kickstringGuidance template rendered to the coordinator on re-entry.
reenter_legstringThe delegation leg to resume on re-entry.

phases.controls

FieldTypeMeaning
content_reviewblockHuman review of edits for this phase only.

phases.controls.content_review

FieldTypeMeaning
tools[]stringTool names whose edits require review during this phase.
paths[]stringPath globs whose edits require review during this phase.

phases.parallel_task

FieldTypeMeaning
max_workersintMaximum workers in flight at once. Must be greater than zero when the block is present.
max_read_workersintSub-ceiling for read-only workers.
max_write_workersintSub-ceiling for workers that may write — the knob that limits overlay churn.

phases.touch

FieldTypeMeaning
paths[]stringPath globs handed to workers as their focus paths for this phase.

phases.advance

FieldTypeMeaning
when_gate_metstringauto advances the moment the gate passes; coordinator leaves the leave-turn to the coordinator.

phases.loop

FieldTypeMeaning
exitstringnext leaves down the spine; self re-enters this phase — the shape behind review and verify loops.

phases.human_approval

FieldTypeMeaning
blueprintstringThe blueprint id being approved; the host records the approved content hash so a later edit invalidates the approval.
readinessstringA condition that must hold before the approval is even offered, so the human is not asked to approve half-finished work.

phases.review_loop

FieldTypeMeaning
evidence_keystringThe 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_capintMaximum review rounds before the loop stops asking (default 3).
verdict_schemamap[string]stringField name to type for the structured verdict the reviewer submits.

phases.transitions

FieldTypeMeaning
idstringStable wire key for the transition — pick it like an API name.
tostringDestination phase id. Self-edges are rejected.
actors[]stringWho may fire it — human, coordinator, or both. Required.
labelstringButton text shown to the human. Required.
whenstringOptional readiness condition; while it is false the transition is not armed. Omit it to keep the exit always available on this phase.

parameters

FieldTypeMeaning
typestringThe parameter type. depth is the typed one the host understands (none / light / thorough) and the only type depth_param accepts.
defaultstringThe value used when neither a preset nor the start request supplies one.

blueprint

FieldTypeMeaning
idstringBlueprint id referenced by human_approval.blueprint and by depth-parameter stamping. Required when the block is present.
filestringBlueprint filename under the project’s .lycaon/blueprints/ convention directory.
frontmatter[]stringFrontmatter keys the document must carry, so downstream phases can rely on them.

presets

FieldTypeMeaning
idstringPreset id; recorded in run variables as workflow.preset_id.
namestringDisplay name in the catalog. Defaults to the id.
descriptionstringOne-line summary of what this preset changes.
triggerstringAn optional slash command that starts the workflow with this preset already applied.
paramsmap[string]stringParameter values this preset overlays on the manifest defaults. Required — a preset that changes nothing is rejected.

injects

FieldTypeMeaning
onstringThe anchor id that fires the inject, from the same catalog rules bind to.
selectorblockNarrows when the inject fires. Clauses AND together; an omitted clause matches everything.
effectstringinform — a workflow inject renders prompt content. Blocking is the rules plane, not this one.
whenstringOptional CEL condition over the same fact catalogue rules use, evaluated when the anchor fires.
renderstringThe guidance template to render, by template ref — a guidance/ file your pack ships.
tierstringMust be workflow (the default) — a manifest can only author workflow-tier bindings. Standalone bindings ship as host/bindings/ units.
dedupyamlRaw dedup policy for this inject, following the anchor catalog’s dedup grammar — omit it to inherit the anchor’s own policy.

injects.selector

FieldTypeMeaning
surfacestringFire only for this anchor surface — coordinator, worker, or phase.
phasestringFire only in the named phase.
workflowstringFire only for the named workflow id.
toolstringFire only for a single tool name.
tools[]stringFire for any tool in the list.
profiles[]stringFire only for agents running under these tool profiles.
surfaces[]stringFire only on these coordinator surfaces.
session_posture[]stringFire only in these session postures.
principalanyScope the inject to a caller identity — the multi-tenant clause shared with rule selectors.
ledgermap[string]anyMatch 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.