Skip to content

@workflow-manager/runner

CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes

workflow-author

core
311 linesSource

Author repeatable wfm workflows from a task description, validate them, run them, and narrate progress as the workflow executes. Load this skill when a user describes a multi-step task that should run the same way every time — turn it into a wfm workflow file, validate it, and then drive `wfm run` while translating status polls and approval gates into plain-language updates for the user.

workflow-author

Turn a natural-language task description into a wfm workflow file: a static, reviewable, re-runnable artifact instead of a one-off chat session. This skill is the authoring + operating counterpart to workflow-manager-cli — read that skill for the general command reference; this one is about deciding what to build and narrating a run to a human.

1. When to use

Use this skill when the user has a task that:

  • has more than one meaningful step, with a clear order or dependency between steps
  • should produce the same shape of result every time it runs — not a bespoke plan improvised fresh each session
  • benefits from an explicit quality gate (an objective check, a human sign-off, or both) before moving on
  • is worth sharing: a teammate, a CI job, or a future session should be able to run it unchanged

Do not reach for a workflow file for a single ad-hoc question, a one-shot investigation, or a task whose steps genuinely can't be known in advance. Ad-hoc orchestration (just doing the work directly) is cheaper than authoring a workflow when there is nothing to repeat. The signal to watch for: the user says some version of "every time we do X" or "we need a repeatable way to Y."

2. The authoring loop

Follow this sequence; do not skip validation.

  1. Elicit the task and success criteria. Ask (or infer from context) what "done" looks like for the overall task, and for each step you expect to need. Concrete, checkable criteria beat vague ones — "the change satisfies the objective and includes tests" is checkable; "does a good job" is not.
  2. Decompose into chronological steps. Every step gets a stable, kebab-case key (implement-fix, not Step 1). Wire dependsOn to express strict order — the engine resolves dependencies deterministically and rejects cycles, so an explicit dependsOn: [previous-step] is safer than relying on array order.
  3. Choose a quality gate per step. For each step's validation (or an approval step's approvalSpec.validation), pick one:
    • mode: agent with concrete criteria — an objective, checkable condition a second agent call can verify against the step's output (tests pass, files changed match scope, no TODOs left). Prefer this whenever the check can be phrased as a fact about the artifact.
    • a dedicated kind: approval step — a human judgment call (does this look right, is this the right tradeoff, are we comfortable shipping this). Use for genuinely subjective or high-stakes decisions.
    • mode: external — an outside system resolves the step (a webhook, a deploy pipeline finishing).
    • mode: none — mechanical steps with no interesting failure mode (formatting, a fixed notification).
  4. Write the workflow. Prefer the Markdown frontmatter format — humans (and you, later) can read the body notes alongside the machine-readable frontmatter; use JSON only for machine-generated definitions. Start from a scaffold:
    shell
    wfm scaffold --template agent-validated my-flow.md
    This drops a working three-step example (agent-validated task → approval → finalize) that you edit in place rather than writing from a blank file.
  5. Validate, and keep validating. wfm validate my-flow.md — fix every reported line, one at a time, until it prints Validation OK. Never hand a workflow to the user (or run it) with unresolved validation errors.
  6. Dry-run adapter-heavy workflows with mocks. If steps use real adapters (pi-agent, claude-code, opencode, acp), set taskSpec.payload.mockResult: success (or retry / rollback / fail to test routing) and taskSpec.adapterKey: mock temporarily, or wfm doctor my-flow.md to check host requirements without executing. opencode runs real by default, so to dry-run an opencode step specifically, either set taskSpec.payload.useRealAdapter: false (keep adapterKey: opencode) or swap adapterKey: mock. This confirms dependency wiring and approval gating before spending a real adapter call.
  7. Done. The file is the reusable artifact — no further "session state" to preserve. Re-running it later reproduces the same step sequence.

3. Schema cheat-sheet

Everything here matches src/types.ts on this branch — do not invent fields.

Top level (required: key, title, steps):

yaml
key: my-workflow            # stable external identifier
title: My Workflow          # default run objective
description: optional summary
objectives: [optional, run-level, objectives]
defaultRetryPolicy: { maxAttempts: 2 }
skills:                     # named skills resolvable by taskSpec.init.skills
  my-skill:
    source: ./skills/my-skill/SKILL.md   # must match skills/**/SKILL.md under the workflow dir
steps: [ ... ]

Step (required: key, kind):

yaml
- key: my-step                     # stable, kebab-case
  kind: task                       # task | approval | system
  title: optional display title
  objective: optional step-level objective
  dependsOn: [other-step-key]
  retryPolicy: { maxAttempts: 2 }
  validation:                      # gates confirmation for THIS step's own record
    mode: none | human | external | agent
    required: true
    autoConfirm: false
    agent:                         # only when mode: agent
      adapterKey: pi-agent | mock | opencode | codex | claude-code | kimi | gemini | qwen | acp   # default: this step's adapter
      criteria: "plain-language acceptance criteria the validator checks against"
      init: { model, skills, mcps, systemPrompts, context }
      payload: { mockResult: success }   # lets mock drive the validator in tests
  taskSpec:                        # required when kind: task
    adapterKey: pi-agent | mock | opencode | codex | claude-code | kimi | gemini | qwen | acp    # omit -> pi-agent
    init:
      model: openrouter/anthropic/claude-sonnet-4
      skills: [skill-name]
      mcps: [mcp://endpoint]
      systemPrompts: [Focus on X]
      context: { any: json }
    payload:
      mockResult: success | retry | rollback | restart | yield | fail   # mock adapter only
      timeoutMs: 600000            # per-step subprocess timeout (pi-agent default); SIGTERM on expiry
  approvalSpec:                    # required when kind: approval
    autoApprove: false
    validation: { mode: human, required: true, autoConfirm: false }

Critical gotcha (verified on this branch, src/parser.ts): the parser fills an unset step-level validation with { mode: "none", required: false, autoConfirm: true } by default — even on approval steps. canConfirm in src/engine.ts checks step.validation?.autoConfirm before step.approvalSpec?.validation?.autoConfirm. If you only set approvalSpec.validation and leave the step's own top-level validation unset, the default autoConfirm: true wins and the gate silently auto-approves instead of waiting for a human. Always set both validation and approvalSpec.validation on an approval step with matching mode/required/autoConfirm — see the worked example below.

Validation rules the CLI enforces: unique step keys; every dependsOn references an existing step; no dependency cycles; kind is task | approval | system; taskSpec.adapterKey (if set) is one of the supported adapters; validation.mode is none | human | external | agent; mode: agent is not allowed on approval steps (approvalSpec.validation).

Agent validation routing: a validator agent's verdict becomes a QA action — PROCEED (continue), RETRY_CURRENT (rerun this step with feedback), ROLLBACK_PREVIOUS (rerun an earlier step), or RESTART_ALL (restart the run) — bounded by the step's retryPolicy.maxAttempts.

Step timeouts: there is no top-level timeoutSec (or similar) step field — StepDefinition has no such field, and nothing in src/engine.ts reads one. The only supported timeout knob is adapter-specific: taskSpec.payload.timeoutMs, read by the subprocess-based executors (pi-agent, and the deprecated bespoke claude-code/opencode executors). It's milliseconds, defaults to 600000 (10 min) for pi-agent, and controls how long WFM waits before sending SIGTERM and failing the step with timedOut: true in mutated_payload. A long-running real step (an API-backed script, a slow build) should set this explicitly rather than relying on the default.

4. Running and narrating (the agent-as-UI protocol)

Once a workflow validates, you are the UI for the run: start it detached, poll its state, and turn each transition into a short update instead of dumping raw JSON at the user.

Start it detached, with a session file:

shell
wfm run my-flow.md --session-file .wfm/session.json &

The session file is written the moment the attach API is listening — { baseUrl, attachToken, runId, pid, startedAt } — and rewritten with endedAt + status when the run finishes. It is never deleted, so it doubles as your "is this still running" signal. Every attach command below accepts --session-file .wfm/session.json instead of separate --url/--token.

Poll on a cadence and narrate transitions, not raw payloads:

shell
wfm status --session-file .wfm/session.json

Read status and currentStepKey off the JSON, and translate: "running" + currentStepKey: "implement-fix" becomes something like "step 1/3 (implement-fix) is running, attempt 1...". Don't re-poll faster than the work can plausibly progress — a few seconds between polls is usually plenty; back off further once a step has been running a while.

If you're dry-running with adapterKey: mock, expect steps to resolve near-instantly: your very first status poll may already show several steps succeeded (or the run parked at an approval gate) with no intermediate "running" beat to narrate. That's expected — narrate what you actually observe rather than assuming a step-by-step cadence.

Use events for incremental detail between polls instead of re-reading the whole snapshot:

shell
wfm events --session-file .wfm/session.json --since 4

Track the last nextSequence you saw and pass it back as --since next time. Add --include-logs only when you actually want agent.stdout/agent.stderr chunks inline.

Use logs when the user asks what a step is doing right now:

shell
wfm logs --session-file .wfm/session.json --step implement-fix --limit 50

Detect and handle waitingForApproval. When status's top-level status is "waiting_for_approval", the JSON includes a waitingForApproval object with stepKey, reason, and a preview (summary plus items describing what's being reviewed, including dependency outputs). Summarize that preview for the user in plain language. Then either:

  • relay the decision the user gives you, or
  • if the user has explicitly delegated authority for this gate ("auto-approve the review steps," "you decide"), decide yourself and act:
shell
wfm approve --session-file .wfm/session.json --step review-gate --note "why you approved"
wfm cancel  --session-file .wfm/session.json --step review-gate --note "why you're stopping the run"

Never approve on the user's behalf without either their live input or a standing delegation they gave you for that specific gate — it is a QA checkpoint, not decoration.

On terminal status, report the outcome by reading endedAt and status back from the session file (the run process has exited by then, so the attach API is gone — the session file is the only source left):

shell
cat .wfm/session.json   # { ..., "endedAt": "...", "status": "succeeded" }

Exit codes (for the wfm run process itself, if you're waiting on it directly rather than polling): 0 — run succeeded; 2 — run finished but not successfully (failed, cancelled, or ended waiting); 1 — validation or runtime error before/during execution, not a normal terminal status.

5. Repeatability rules

  • Never rename or remove a published workflow's step keys — other automation and history may reference them. Add new steps or a new workflow key/version instead of mutating shape in place.
  • key (workflow) and step keys are stable external identifiers; change them only deliberately, and treat it as a breaking change for anything that depends on them.
  • Keep taskSpec.payload deterministic — avoid embedding timestamps, random IDs, or environment-specific paths that would make two runs diverge for reasons unrelated to the actual task.
  • Prefer validation.mode: agent criteria that a validator can check as a fact about the output (tests pass, a file exists, a diff touches only expected paths) over criteria that require taste. Save taste calls for approval steps.

6. Install/share

shell
wfm skill install workflow-author                    # this skill -> ./.claude/skills/
wfm skill install workflow-author --agent opencode    # -> ./.opencode/skill/
wfm skill install workflow-author --global            # -> ~/.claude/skills/

To share the workflow file itself (not this skill) with teammates, use the remote registry — wfm publish my-flow.md / wfm pull owner/slug; see the workflow-manager-cli skill and doc/guide/ for the full registry contract.

Worked example

A repeatable "fix a flaky test" workflow: an agent-validated implementation step, a human sign-off gate, then a finalize step. Validated on this branch with wfm validate (Validation OK) and executed end-to-end with the mock adapter.

yaml
---
key: fix-flaky-login-test
title: Fix Flaky Login Test
description: Diagnose and fix an intermittently failing login test, with a human sign-off before landing the fix
objectives:
  - the login test passes reliably and the fix is reviewed before merge
defaultRetryPolicy:
  maxAttempts: 2
steps:
  - key: implement-fix
    kind: task
    objective: Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it
    dependsOn: []
    retryPolicy:
      maxAttempts: 2
    validation:
      mode: agent
      required: true
      autoConfirm: false
      agent:
        criteria: >-
          tests/login.test.ts passes 20 consecutive local runs, the fix
          addresses a root cause (not a retry/sleep workaround), and no
          unrelated files changed.
        init:
          model: openrouter/anthropic/claude-sonnet-4
          systemPrompts:
            - Check the diff against the criteria; call out any retry/sleep workaround explicitly
    taskSpec:
      adapterKey: mock
      init:
        context:
          repo: example/webapp
        skills: [debugging, testing]
        systemPrompts: [Find the root cause before writing a fix; add a regression test]
      payload:
        mockResult: success
  - key: review-gate
    kind: approval
    objective: Human sign-off on the fix before it merges
    dependsOn: [implement-fix]
    # validation must be set here too, not just under approvalSpec — an unset
    # step.validation defaults to autoConfirm: true, which would silently skip
    # this gate. See the schema cheat-sheet above.
    validation:
      mode: human
      required: true
      autoConfirm: false
    approvalSpec:
      autoApprove: false
      validation:
        mode: human
        required: true
        autoConfirm: false
  - key: finalize
    kind: task
    objective: Open a PR with the fix, the regression test, and a summary of the root cause
    dependsOn: [review-gate]
    validation:
      mode: none
      required: false
      autoConfirm: true
    taskSpec:
      adapterKey: mock
      init:
        systemPrompts: [Open a PR summarizing the root cause, the fix, and the new regression test]
      payload:
        mockResult: success
---

# Fix Flaky Login Test

Repeatable workflow for chasing down a flaky test: implement a fix (agent-validated
against explicit, checkable criteria), get a human sign-off, then finalize.

Simulated narration transcript

plaintext
$ wfm run fix-flaky-login-test.md --session-file .wfm/session.json &
Attach API: http://127.0.0.1:43121 (token b354...)

[agent] Started the workflow in the background — I'll check in as it progresses.

$ wfm status --session-file .wfm/session.json
{"status":"running","currentStepKey":"implement-fix", ...}

[agent] step 1/3 (implement-fix) is running, attempt 1 — reproducing the flake and writing a fix.

$ wfm status --session-file .wfm/session.json
{"status":"waiting_for_approval","currentStepKey":"review-gate",
 "waitingForApproval":{"stepKey":"review-gate",
   "reason":"confirmation required for review-gate",
   "preview":{"summary":"Approve this gate to continue after implement-fix.",
     "items":[{"title":"Dependency implement-fix","status":"succeeded",
       "summary":"Reproduce the flake in tests/login.test.ts, diagnose the root cause, and fix it"}]}}}

[agent] implement-fix passed agent validation (tests green, real root-cause fix,
no unrelated files touched) and is now waiting on you: review-gate wants a
human sign-off before finalize runs. Want me to look at the diff summary, or
are you approving as-is?

[user] approve it

$ wfm approve --session-file .wfm/session.json --step review-gate --note "Fix looks correct, root cause addressed"
approved review-gate

[agent] Approved review-gate. finalize is running now (opening the PR)...

$ cat .wfm/session.json
{ ..., "endedAt": "2026-07-11T13:35:40.756Z", "status": "succeeded" }

[agent] Done — the workflow succeeded. implement-fix and finalize both ran
clean, and your sign-off on review-gate is recorded in the run's approval
audit trail.