| name | workflow |
| description | Orchestrate a MULTI-PHASE, dependent, or resumable run over many provider subagents from a JS script, off the main context (`cc-fleet workflow`). Use for fan-out→barrier→synthesis, per-item pipelines, loop-until-dry, or a run that must survive a kill and `--resume` from its journal. NOT a flat fan-out of independent tasks (that is /cc-fleet:subagent — cheaper, no script); NOT interactive collaboration (that is /cc-fleet:team); NOT trivial single-shot work for the main session. |
workflow — multi-phase JS orchestration over provider subagents
Wrong lane? A flat one-shot fan-out of independent tasks → /cc-fleet:subagent; interactive collaboration you message back and forth → /cc-fleet:team; arbitration in cc-fleet-shared/routing.md.
When this skill cites cc-fleet-shared/<file>.md, OPEN it with the Read tool at ../cc-fleet-shared/<file>.md relative to this SKILL.md — the cited content is load-bearing, not optional background.
Execution environment — check before running anything. Confirm your shell tool executes on the host where cc-fleet is installed. In sandboxed or remote agent sessions, a tool named Bash may run on an isolated machine with a different filesystem, PATH, processes, and tmux server — command not found, a healthy-looking doctor whose leaves can't reach your files, or a wrong working directory should prompt you to verify whether you are in a sandbox shell, not conclude that cc-fleet is broken. If so, route commands through a host-executing bridge tool (for example, desktop-commander) and pass host paths for any files you reference; do not retry the same Bash call expecting different results. If no host-executing tool is available, stop and explain that cc-fleet must run on its installation host.
A workflow is a JavaScript script that fans out provider cc-fleet subagent leaves and runs in a cc-fleet process, OFF the main session's context. You write the script; cc-fleet workflow run executes it. The orchestration plan lives in script variables (CPU, ~0 of your tokens) — you are invoked only when authoring the script, not on every scheduling decision. The API mirrors the native Claude Code Workflow tool — write the script exactly as you would a native workflow; the only addition is the provider option on agent().
When to use it
- Multi-phase or dynamic orchestration over many provider subagents: fan-out + barrier, per-item pipeline, loop-until-dry, branch-on-result, with a board run-tree.
- A single flat batch of independent one-shots is not a workflow — that's /cc-fleet:subagent. Don't write a script for it.
The provider ask ladder (ask at most once per task)
- The user named a provider or model → use it.
- Else run
cc-fleet default --json: if it returns a provider (source "configured" or "auto"), use it and STATE it in your kickoff line (e.g. "using glm (default)").
- Else (several providers, none default) ask the user ONCE which to use — list the enabled providers from
cc-fleet list --json (name + default_model + the one-line note in cc-fleet-shared/providers.md). After they pick, run cc-fleet default <chosen> so you never ask again. (cc-fleet default <p> is user-layer; only run it to FILL a blank default, never with --force.)
- A mid-task provider failure (insufficient balance / rate limit / auth) → STOP, tell the user what happened, propose the next provider, and WAIT for their confirmation. Never switch providers silently.
Model tier within a provider: fan-out / leaf work → omit --model (or --model fast); judge / synthesis / sustained work → --model strong. The provider's roster decides the actual model — see cc-fleet-shared/providers.md.
In a script, agent()'s opts.provider is optional: omitted, the leaf uses the run's default provider, resolved ONCE at launch and recorded with the run — so --resume stays stable even if the default changes later. A script meant to be shared or reproducible should still pin provider explicitly.
The script API (mirrors the native Workflow tool)
const meta = {name, description, whenToUse?, model?, phases?: [{title, detail?}]} — a top-level pure literal (no calls/vars/spreads; the native export const meta form is also accepted). name + description are required; model is the default for agents that omit it. Read statically before the run → the board shows the named, phase-skeletoned run immediately.
agent(prompt, opts) → Promise<string|object> — runs ONE provider subagent leaf. opts.provider is optional (omitted → the run's default provider, above); provider: "claude" runs the official claude CLI on the user's OWN Claude Code login (subscription OAuth) instead of a configured provider — a literal model id (fable/opus/sonnet/a full id, omitted → claude's login default, typically the costliest tier so name one), no roster keywords, no key material. The rest are optional: model, schema, label, phase, timeout (seconds), max_budget_usd, max_turns, isolation: "worktree", profile ("slim" default / "slim-ro" / "full"), tools, skills, mcp. An unknown option key throws (typos fail loudly). On a leaf failure the promise rejects — an un-caught top-level await agent() aborts the run; inside parallel/pipeline a failed element degrades to null. Leaf failures classify like subagent failures — dispatch table in "Leaf failures" below.
schema (a plain object) goes to the claude child via --json-schema: claude injects a forced StructuredOutput tool and enforces that it is CALLED (the native mechanism — no JSON instruction is added to the prompt); the promise resolves with the parsed structured payload. The three rules:
- a validation failure — or a result envelope without a structured payload — FAILS the leaf; there is NO automatic retry;
- the forced
StructuredOutput call costs turns — give a schema'd leaf max_turns ≥ 3 (a budget of 1 starves it);
What a workflow script can NOT use (determinism — the journal depends on it)
Date / Math.random() throw; eval / Function / dynamic code are removed; there is no setTimeout / require / fs / ESM import — pass timestamps or randomness in via args.
- Plain script statements only (the body runs inside an async wrapper, so top-level
await and return work); async generators (async function*) are not supported.
Running it
RUN=$(cc-fleet workflow run audit.js)
cc-fleet workflow status "$RUN" --json
cc-fleet workflow list --json
cc-fleet workflow stop "$RUN"
cc-fleet workflow stop "$RUN" --leaf <job|label>
cc-fleet workflow restart "$RUN" --leaf <job|label>
cc-fleet workflow wait "$RUN" --timeout 3m --json
The run is detached so it outlives this call and your session stays responsive.
Waiting on a run: arm wait in a backgrounded Bash (push, not poll)
Right after launching, arm the notifier — a backgrounded Bash whose EXIT is your wake-up:
RUN=$(cc-fleet workflow run audit.js)
cc-fleet workflow wait "$RUN" --timeout 3m --json
End your turn and keep working — never spawn an agent (or loop yourself) to poll a run.
On the wake, dispatch on the envelope's wait_outcome (+ exit code):
terminal (exit 0 done/stopped · 1 failed) — fetch the detail with workflow status "$RUN" --json (it carries run_error and the per-leaf list; the wait envelope deliberately doesn't) and report.
engine_gone (1) — the engine died without finalizing; propose cc-fleet workflow run <script> --resume "$RUN" (the journal replays the finished leaves).
parked (3) — every remaining leaf is held. FIRST re-check workflow status: leaves running/queued again means it was a transient (the engine was between leaves) — re-arm silently. Still parked → name the envelope's held leaves to the user and propose restart --leaf; never wait it out.
timeout (124) — a heartbeat, not a verdict. Compare counts/spent_* with the previous snapshot: progress → one short progress line and re-arm with a longer window; zero delta → inspect (workflow status; is one long leaf still inside its own timeout?) and escalate only on a real anomaly, else re-arm.
Window sizing: make the FIRST window short (2–3m — a provider auth/balance failure surfaces on the first leaf call), then 10–15m per re-arm. One wait per run; they are independent. After a session restart, re-arm every running run from workflow list --json.
For a human live view: cc-fleet workflow watch "$RUN" streams the run's events as text (in a terminal, or a backgrounded shell → the /tasks panel) and cc-fleet watch streams the whole fleet; the board's Dynamic Workflows view has the rich drill-in. Both print only canonical status — never a provider reply.
Leaf failures — dispatch on error_code (do not parse prose)
A failed leaf's error_code is in workflow status --json (jobs[]) and in the rejection that reaches the script. Same vocabulary as a one-shot subagent (the full table with context lives in /cc-fleet:subagent); the dispatch:
error_code | What you do |
|---|
INSUFFICIENT_BALANCE / KEY_INVALID / RATE_LIMITED | STOP — provider ask ladder, step 4 (never switch silently). KEY_INVALID → the user rotates the key; RATE_LIMITED → brief wait, one retry. |
NO_DEFAULT_PROVIDER / DEFAULT_PROVIDER_DISABLED / DEFAULT_PROVIDER_UNKNOWN / DEFAULT_PROVIDER_RESERVED | No usable default for a provider-less agent() (RESERVED = default_provider hand-set to claude, explicit-only — the user unsets/re-pins) — apply the provider ask ladder, then re-run. |
MODEL_NOT_FOUND | cc-fleet refresh <provider>, or drop the leaf's model to use the provider default. |
SUBAGENT_TIMEOUT | Raise the leaf's timeout or split the task; a leaf with no timeout defaults to 300s. |
SUBAGENT_OUTPUT_TOO_LARGE | The leaf's output exceeded the byte cap — have it write to a file and answer concisely; a blind retry overflows again. |
SUBAGENT_STOPPED | An operator stopped it (stop --leaf / run stop) — terminal, NOT a failure; never auto-retry. |
SUBAGENT_MAX_TURNS | A leaf hit the --max-turns cap. |
SUBAGENT_FAILED / PROVIDER_API_ERROR | Inspect (workflow status); restart --leaf once, or propose a provider switch (ask first). A provider: "claude" leaf on a logged-out machine fails here (the error preview names the login problem, no dedicated code) — tell the user to log in to Claude Code interactively. |
FINGERPRINT_MISSING / FINGERPRINT_STALE | Self-heal flow in cc-fleet-shared/troubleshooting.md (STALE = no claude binary — the flow can't help; fix Claude Code / PATH). |
| / |
Resume (content-hash journal)
Each run records a content-hash journal of its completed leaves. Re-run the same script under an existing run id to replay:
cc-fleet workflow run audit.js --resume "$RUN"
A leaf is keyed by its determinant (provider + model + prompt + schema + slim shape), so an unchanged re-run is ~100% cache hits, a leaf whose prompt you edited (and anything downstream of its output) re-runs, and a run that was killed resumes by replaying what finished before the kill. The determinism lockdown makes this exact: with no clock/PRNG, the same script+args produce the same keys. A failed leaf is never journaled, so resume re-runs it.
Non-goals (state plainly, don't oversell)
- No pause. A running
claude -p can't be cleanly suspended; use workflow stop (reaps the run) + run --resume (cheap restart via the journal) instead.
- Client-side
schema validation is a JSON-Schema subset — the list above, not the full spec (an external $ref URI is unsupported and fails; an unknown format is an annotation, not enforced). claude enforces that StructuredOutput is called; this backstop checks what it was filled with, and a failure is terminal (no retry).
- Key-safety is unchanged: the provider key flows only via
apiKeyHelper; prompts go to the leaf via stdin, never argv; the journal/events/board carry no key.
Worked example — research sweep (fan-out → pipeline → loop)
const meta = {
name: "api audit",
description: "map endpoints, draft checks, then probe for gaps",
phases: [{title: "map"}, {title: "build"}, {title: "probe"}],
};
phase("map");
const maps = (await parallel(
args.map((m) => () => agent("List exported endpoints in module " + m,
{provider: "deepseek", label: "map:" + m}))
)).filter(Boolean);
phase("build");
const checklists = await pipeline(
maps,
(prev, item, i) => agent("Draft an audit checklist for these endpoints:\n" + prev,
{provider: "glm", label: "build:" + i}),
);
phase("probe");
const gaps = [];
while (gaps.length < ) {
g = (
+ checklists.(), {: });
(g.() === ) ;
gaps.(g);
}
verdict = (
+ gaps.(), {: , : , : });
();
{ maps, checklists, gaps, verdict };
One run, three phases, a barriered fan-out, a no-barrier pipeline, a bounded loop-until-dry, and a single claude synthesis node — all sequenced by the script in a cc-fleet process, off your context. The script's top-level return value is NOT persisted or retrievable — to read the run's output, fetch a labeled leaf's answer with cc-fleet workflow result "$RUN" --label verdict --json.
Anti-patterns
- A script for a single flat independent batch → /cc-fleet:subagent.
- A long-lived collaborator you message back and forth → /cc-fleet:team.
Date.now() / setTimeout — unavailable (determinism); pass timestamps via args.
- Trusting
schema as deep validation, or treating a plain agent() result as JSON without schema.
- Unbounded ambition: the runtime hard-caps 1000
agent() calls/run, pools concurrency at min(16, cores-2), and caps a single parallel/pipeline list at 100,000 elements.
- Switching providers silently after a balance / rate-limit / auth failure → stop, tell the user, wait for their pick (provider ask ladder, step 4).