---
name: codemode-how-to-workflow
description: Discover, launch, monitor and recover YAML workflows, use selected jobTools, or track dynamic script graphs and child transcripts in Activity.
pinned: true
---

# Workflows in code mode

Use `workflow` for persisted YAML runs and `dynamicWorkflow` for tracked script graphs with a durable step journal. A managed step may also receive workflow-selected `jobTools`. Discover each namespace for current signatures. Workflow is available whenever its package is selected, with no minor-mode activation.

## Discover, launch, monitor

1. `workflow.list` discovers definitions, tags and pages, not existing runs. Inspect the definition and its imports before launching.
2. Call `workflow.validate({ workflowPath, inputs, env, runner, command, choice, profile, job })` with the settings you intend to launch. This read-only check reuses the parser/doctor and actual command resolver: imports, references, required/default inputs, declared input options/types, command/choice/runConfig bindings, environment interpolation phases and host injections. It never launches steps or reads a secret file. Supply only the optional settings you need.
3. Inspect `valid`, every diagnostic's `code`, `location` and corrective `message`, and the separate `deferred` list. Fix errors and revalidate after every edit or launch-setting change. `valid: true` means no detected errors, not 100% runtime coverage. Run identities, assembled prompts, child invocation inputs, conditional context files, host selection/authentication and shell/artifact behavior may need execution-time checks. Never replace deferred values with invented data or claim they were verified. Binding values are omitted from reports and call cards.
4. `workflow.launch` starts a run only when authorized. Prefer a declared in-process Pi command, for example `command: "pi"` when that name is declared. `pi`, `po` and `claude` are literal workflow command names, not aliases. Never rewrite a declared command to match an example. `runner` selects the runner, not the command; `runner: "codex"` does not imply `command: "pi"`. Without `command`, each step uses its first command. An acknowledgement `{ message, run? }` is not completion. The run reference may be absent until an identity exists. Use `workflow.status({ runKey, workspace })`, terminal notifications, and final job/step states before reporting success.
   A definition whose `validate` result reports `isolation: 'checkout'` expects its own checkout. The engine never creates one: start it through an isolating launcher such as po-sparo `worktrees.startWorkflow`, not `workflow.launch` from a shared checkout.
5. Pause, resume and stop only at the person's request or an explicit launcher delegation. Pass `expectedRunId` from the inspected run so a replacement generation cannot be controlled accidentally. Keep the exact `runKey` and separate `workspace`.

Shell steps, child scripts and background services use executable `/bin/zsh` when available, otherwise `/bin/sh`. Prefer POSIX shell syntax for workflows that must run on minimal Linux hosts.

Dispatcher sessions can list, validate and launch definitions; inspect, control or recover runs in the owner's session. Closing an exec binding does not stop a successfully launched YAML workflow; existing owner-session shutdown rules still apply. Calls are bound to the current session and reject abort, disposal and inactive package ownership.

List, validate, status, listFailed and recoveryEvidence are reads. Launch, pause, resume, stop and recover are effects serialized per session. Workflow steps consume quota and may repeat external effects: quiet progress is not a reason to replay. Inspect live ownership and persisted state first.

## Recover safely

Recovery resumes an existing terminal failed or interrupted run from its active repair, in the checkout it ran in, preserving completed work. It is not a fresh launch or permission to control another session's live run. Runs recorded with an engine-owned worktree predate its removal and cannot be recovered; launch them fresh. `workflow.listFailed` accepts workspace/runKey and exact-match `env` filters; summaries omit environment data.

A run launched by a plugin (such as Agiflow) may hold a lock for its job. The engine tells the plugin how each run ended: success releases the lock, while a failed or interrupted run keeps it so recovery reuses it and no one else picks up the job. Recover with `workflow.recover` as usual. Never release a held lock by hand or launch a fresh duplicate. When the person decides not to recover a failed run, `workflow.abandon` (with their approval) releases its lock and the run can never be recovered. `lockReleased: false` means the plugin that holds the lock is not loaded; it is released when it is.

Before `workflow.recover`, call `workflow.recoveryEvidence` with the exact run key and workspace. Read every relevant log source, process state, the recorded run.json and only the repair referenced by activeRepairId. Evidence is terminal-only and read-only; it does not scrape an earlier session's live launcher.

Choose exactly one outcome: recover when the recorded target remains valid and failure was transient; launch fresh when it cannot resume or the branch has diverged materially; defer when a named prerequisite remains unresolved. An empty failed-job target cannot be recovered. Never manufacture a target, edit issue.md or repair.json, or discard the active repair or the run's checkout. Prepare run.json only when verified recovery state requires it. Treat an already-claimed result as another recovery winning, not permission to retry or launch fresh. Do not use dryRun for a real recovery.

Under a dispatch contract only: enforce its pass budget (recover and fresh launch each cost one launch), do not recover terminal repair-triage runs, do not act on jobs in blocked columns, and report manually launched runs without a job id rather than adopting them. These job-id and board rules do not apply to ordinary manually launched workflows.

After recovery, verify real process and registry progress with workflow.status before reporting an outcome. In server sessions replay belongs to the recovering session, whose closure requests a stop. Recovery cannot make external effects idempotent or guarantee survival of parent shutdown. User-requested stops do not trigger diagnosis or automatic restart.

## Repair a stuck run (needs the person)

Use this only when recovery cannot proceed: run.json is invalid so the run is missing from listings, a dead run still sits in running, the fix budget is spent, or a repair triage locks recovery until a person unblocks it. Otherwise recover.

1. `workflow.inspectRun({ runKey, workspace })` returns every stage directory the run occupies: raw run.json and control files, the parsed record or its validation error, whether its pid is alive, and a `revision`.
2. Ask the person what to change before calling `workflow.repairRun`. It always needs their approval and is refused where nobody can answer. It throws `ApprovalPendingError` while they decide. Keep working; the outcome arrives later as a message. Do not call it again while one is pending.
3. Pass the exact `revision`, a short `reason`, and only the change you need. `record` is a full replacement run.json (copy it from the inspection and edit it), checked against the schema; workspace and run key cannot change. A record stage of `error` moves a dead running run to error. A repair never moves a run into running: repair it to error, then `workflow.recover`. `fix` rewrites fix.md (front matter `restart-from: <job>` picks where recovery restarts) or removes it with null. `remove` drops stale control files such as `recovery-claim.json`.
4. To lift a spent fix budget, lower or drop `fixLoopCycle` in the record. To unblock a triage lock, set the stated restart target in fix.md as the person directs. Never edit issue.md or repair.json.
5. A running run with a live pid is refused. Stop it first; pass `force` only when the person confirms it is hung. A run owned by another live session must be repaired there. A changed revision means inspect again, not retry.

Every repair keeps the replaced files under `repair-backups/` in the run directory. After a repair, recover and verify with `workflow.status`.

## Managed child jobs and independent checks

A managed lead sees its main job's trusted acceptance table before its first turn: each criterion,
its description, authorised `verified-by` child definition, current state and latest feedback. The
lead knows the requirements but cannot attest them. `children` is the allowlist of reusable job
definitions, not a fixed chain. Discover `workflow` again inside managed sessions for role-scoped methods.

- Lead: call `workflow.spawnChild({ job, prompt, inputs })` when that child has useful work. The
  assignment is task text only; configuration and verifier authority come from the captured YAML.
  Inputs must match the child's declaration. Keep secrets out of assignments, input values and feedback.
- Inspect `workflow.children()` or `workflow.childStatus({ invocationId })` for linked session ids,
  states, checks and usage coverage. Unknown token/cost values are not zero. Route native intercom
  sends by exact session id, never a bare name. Full-session ask/reply is not provided.
- Child verifier: use `workflow.reviewArtifacts()` to obtain the immutable review snapshot, then
  `workflow.reportCriteria({ results: { criterionKey: { status: 'checked' | 'not-met', feedback } } })`.
  Only criteria that authorise this invocation's definition can be reported. Reports commit after
  successful invocation settlement; edits to reviewed artifacts invalidate their checks. Do not
  change an artifact yourself and certify different bytes from the snapshot.
- `not-met` is successful feedback, not a failed step or repair request. The lead chooses how to
  address it and which children to call again. The workflow never auto-spawns children by default.
- At settlement, unresolved children hold the lead open. Unchecked/not-met criteria prevent
  completion and return the criteria table and bounded feedback to the SAME lead session.
  `completion.max-continuations: 3` permits three additional prompts after the initial settled
  attempt, not three total attempts. Child-wait notifications consume no continuations. Exhaustion
  fails explicitly with outstanding criteria. Decision files and exit code 0 cannot bypass this guard.
- A person's stop cancels owned children and stays stopped. Host restart may create a replacement
  lead conversation seeded from the durable ledger; it does not promise the same conversation id.

## Selected job tools

When the session lists the pinned `workflow-job-tools` skill, read it before using its methods. Then
call `discover({ namespace: 'jobTools' })` for the exact declarations and use `exec` to call them.
The skill teaches judgement and ordering; discovery supplies signatures. Do not invent methods or
assume tools selected for another job are present here. Ordinary sessions and external Claude/Codex
CLI runners do not receive this namespace, and there is no direct-tool fallback for it.

The workflow's optional `tools` folder is the default; a job's `tools` overrides it for its main steps,
`preJob` and `postJob`. Repair triage uses `repair.tools` or the default. Global phases use the default.
Selection comes from this session's captured config, never step `cwd` or environment values. Paths
retain the declaring YAML's origin through imports and job-template inheritance.

Methods are flat named async functions with JSON inputs/results. Follow the skill's input validation
and effect rules. Approval-gated methods use the normal code-mode approval flow; never bypass it or
retry an effect merely because an exec was cancelled. Bindings refuse foreign sessions, abort and
disposal. Exec cleanup does not stop the owning YAML workflow or change recovery ownership.

Authors provide trusted JavaScript `index.mjs` plus non-empty `SKILL.md`, not TypeScript or sandboxed
agent code. A per-session default-export factory receives `{ sessionId, cwd, workflow, fs, shell,
signal, acceptedArtifacts? }` and returns the existing namespace contract named `jobTools`: summary, declarations and
`bind(run)`. Validate method inputs, honour `run.signal` for work and register exec cleanup through
`run.onDispose`. Keep per-session state inside the factory. Optional approval policies must name a
returned method. Native modules are cached by file URL; restart the host after same-path module
edits. There is no hot reload or external CLI bridge. Missing or malformed selected tools fail the
managed session before its first prompt. See the package README's **Job-specific tools** section for
a runnable module and skill example.

An authorised downstream consumer's trusted factory can receive `acceptedArtifacts({ job })`.
It rechecks the runtime ledger and current revision and returns the accepted immutable bytes.
A task-writing adapter must fail closed when this capability is absent or stale, and construct its
writes from those bytes. Inside `bind(run)`, use `run.runJson({ command, args, input? })` for
shell-free argv plus JSON stdin/stdout. It inherits the session cwd/environment, is bounded to 15
seconds and the process bridge byte limits, and cancels with this exec/session. It returns
`{ exitCode, output, stderr }`; successful exit alone never certifies acceptance. Do not interpolate
input JSON or assignments into commands. Environment flags, PASS files and caller-provided acceptance booleans are
not authority. This adapter guards its own write path only: neither jobTools nor the acceptance
capability sandboxes an unrestricted agent's raw shell or other external tools.

## Hosts without code mode

Use list_workflows and launch_workflow for discovery/launch; workflow_run for status/pause/resume/stop; workflow_tools for list-failed, recovery-evidence and recover. These tools share the same operations and safety checks. Managed leads receive workflow_spawn_child,
workflow_child_status and workflow_children; child verifiers receive workflow_review_artifacts and
workflow_report_criteria. Those pairs are main-agent-only and withdrawn when code mode is bound.
scaffold_workflow stays a separate Help-only direct tool.

# Dynamic script graphs

Use `dynamicWorkflow` when a script has multiple steps or bounded item fan-out and the person should see progress in Activity. Mastra 1.78.0 executes the workflow. Po observes lifecycle and `team.run` child transcripts; it is not another scheduler. Discover `dynamicWorkflow` for the current signatures.

Use the supplied `z` (the Zod 3 API), `createStep`, and `createWorkflow`, not raw npm imports. Build, commit, create a run, then await `start({inputData})`. Run long work inside `exec_background` so the parent remains responsive; cancellation and disposal stop active runs.

```ts
const { z, createStep, createWorkflow } = dynamicWorkflow;
const phase = (id) =>
  createStep({
    id,
    inputSchema: z.string(),
    outputSchema: z.string(),
    execute: async ({ inputData }) => {
      const child = await team.run({ task: inputData, systemPrompt: 'Complete this item and report your findings.' });
      // The script owns its failure policy: inspect child.status and decide
      // whether to throw, retry, or return failure data for another phase.
      return inputData;
    },
  });
const item = createWorkflow({ id: 'item', inputSchema: z.string(), outputSchema: z.string() })
  .then(phase('gather'))
  .then(phase('check'))
  .commit();
const workflow = createWorkflow({ id: 'items', inputSchema: z.array(z.string()), outputSchema: z.array(z.string()) })
  .foreach(item, { concurrency: 3 })
  .commit();
return await (await workflow.createRun()).start({ inputData: ['first', 'second'] });
```

Supported composition: `.then(step)`, `.parallel([step,...])`, `.branch([[predicate,step],...])`, and `.foreach(committedTrackedItemWorkflow,{concurrency:positiveInteger})`. Branch predicates receive execution parameters and must return a boolean. Every matched branch can execute. Commit each nested workflow before adding it. Create every nested handle within the same exec binding. Use distinct step ids in a workflow. Handles are not durable and cannot be shared between exec calls.

Not supported: bare-step foreach, dynamic concurrency callbacks, raw Mastra instances, custom engines or storage, streams, Mastra suspend/resume, time travel, retries configured through workflow options, startAsync, or private methods. Only `id`, optional `description`, `inputSchema`, `outputSchema`, and step `execute` options are accepted. `createRun()` takes no options; `start` accepts only `inputData`; `cancel()` stops that run.

Return `null`, not `undefined`, when a callback intentionally has no output. The code-mode membrane converts guest callback exceptions to undefined; Po reports that as failed and directs you to exec diagnostics, rather than inventing the original error. A normal returned failure object is data, not an automatic exception. Mastra's own failure/queue behavior still applies. Mastra 1.78 validates step input but does not enforce step output schemas at runtime. Po preserves that behavior rather than adding a second validation engine. A child-agent failure remains visible even if the script handles it and Mastra finishes successfully. Po keeps execution status separate from this application outcome.

Activity snapshots are metadata only: names, topology, status, timing, bounded errors and child run ids. They omit prompts, schemas, callback source, input and output. Keep sensitive data out of ids and error messages. At most 20 finished root runs remain in memory, active roots are never evicted, and groups publish up to 1000 rows with true totals and aggregate counts.

## Resume after a restart

Po saves the exact outer exec source and JSON-serializable successful step outputs under the session's workflow artifacts. On restart or release, the owner agent receives a prompt to arm `await dynamicWorkflow.resume(recordId)` and import the returned `specifier`. Resume is scoped to that exec binding. Use the exact saved script, root workflow ids, root order and input. A source or root mismatch throws before that root's callbacks run; there is no uncached fallback.

- Code outside steps re-runs. Put effects you want memoized inside steps.
- Step outputs must be JSON-serializable. Other outputs are not saved and those steps re-run.
- Branch predicates must be pure because they run again during replay.
- A step interrupted before its output was saved repeats its side effects.
- Imported scripts are not copied. Keep their definitions stable for replay.

A person's stop does not resume automatically. Interrupted runs appear stopped with the restart note, not a separate UI status.
