@aicopycoders/exodus 2026.7.200-beta.8 → 2026.8.1100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +51 -0
  2. package/assets/docs/CLAUDE.md +3 -0
  3. package/assets/docs/PIPELINES.md +50 -0
  4. package/assets/skills/exodus/SKILL.md +6 -2
  5. package/assets/skills/exodus-meme/SKILL.md +44 -28
  6. package/assets/skills/exodus-winners/SKILL.md +239 -0
  7. package/assets/skills/exodus-workflow/SKILL.md +464 -63
  8. package/dist/bin/exodus.js +3 -0
  9. package/dist/commands/bank.d.ts +88 -0
  10. package/dist/commands/bank.js +346 -0
  11. package/dist/commands/browse.js +2 -1
  12. package/dist/commands/creative.js +2 -2
  13. package/dist/commands/genesis.js +16 -4
  14. package/dist/commands/keys.js +1 -0
  15. package/dist/commands/meme.js +9 -6
  16. package/dist/commands/primer.js +2 -1
  17. package/dist/commands/session.d.ts +55 -0
  18. package/dist/commands/session.js +202 -0
  19. package/dist/commands/status.js +4 -4
  20. package/dist/commands/swipe.js +4 -4
  21. package/dist/commands/template.js +2 -2
  22. package/dist/commands/update.d.ts +2 -0
  23. package/dist/commands/update.js +20 -0
  24. package/dist/commands/winners.js +13 -3
  25. package/dist/commands/workflow.d.ts +293 -8
  26. package/dist/commands/workflow.js +2396 -90
  27. package/dist/lib/client.d.ts +8 -1
  28. package/dist/lib/client.js +30 -6
  29. package/dist/lib/format.d.ts +7 -0
  30. package/dist/lib/format.js +52 -5
  31. package/dist/lib/intel-format.js +2 -1
  32. package/dist/lib/keys-sync.js +1 -0
  33. package/dist/lib/poll.js +7 -4
  34. package/dist/lib/route-support.d.ts +4 -0
  35. package/dist/lib/route-support.js +22 -0
  36. package/dist/lib/runStatus.d.ts +22 -0
  37. package/dist/lib/runStatus.js +139 -0
  38. package/dist/lib/workflowText.d.ts +3 -0
  39. package/dist/lib/workflowText.js +113 -0
  40. package/package.json +5 -2
@@ -1,39 +1,101 @@
1
1
  ---
2
2
  name: exodus-workflow
3
- description: Run, chain, build, and edit saved Exodus Workflows — the multi-node automations that wire Genesis bots, primers, briefs, and image nodes together on the dashboard canvas — from the CLI. Use it to list a brand's workflows, describe what a workflow needs before running it (inputs, per-workspace primer prerequisites, outputs), run one and collect its outputs, chain one workflow's output into another's input, browse the bot catalog, and author or update a workflow contract via export → edit → import. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("exodus, run my launch workflow", "exodus, what does this workflow need", "exodus, build a workflow that turns a swipe into hooks then ads", "list my exodus workflows", "exodus, edit that workflow"), named this skill or /exodus-workflow, ran an `npx @aicopycoders/exodus workflow` command, or the `exodus` hub skill routed here. Never claim generic "workflow"/"automation" requests ("automate this", "build me a workflow") without Exodus context — in shared folders those may belong to the user's other tools; if the user did not say exodus, this skill is not for them. The bare word "Genesis" without "exodus" refers to the member's own Genesis API key and personal bot recipes, NOT to Exodus.
3
+ description: Run, chain, build, edit, and automate saved Exodus Workflows — the multi-node automations that wire Genesis bots, primers, briefs, transforms, and image nodes together on the dashboard canvas — from the CLI. Use it to list a brand's workflows, describe what a workflow needs before running it (inputs, per-workspace primer prerequisites, outputs), run one and collect its outputs, chain one workflow's output into another's input, browse the bot catalog, author or update a workflow contract via templates → edit → validate → import, resolve runs that park (approve a run paused at a checkpoint, check the workflow inbox, repair a stalled run, answer a nested workflow's slots), continue a bot session in chat, enable/disable/fire triggers, read the brand's copy banks or promote a winner, and list versions or roll one back. Only invoke when the user has explicitly invoked Exodus: they said "exodus" in the request ("exodus, run my launch workflow", "exodus, what does this workflow need", "exodus, build a workflow that turns a swipe into hooks then ads", "list my exodus workflows", "exodus, edit that workflow", "exodus, approve that checkpoint", "exodus, check my workflow inbox", "exodus, resolve that parked run", "exodus, continue that session", "exodus, enable/fire the trigger", "exodus, read my hooks bank", "exodus, promote this winner", "exodus, list the versions / roll it back"), named this skill or /exodus-workflow, ran an `npx @aicopycoders/exodus workflow` / `session` / `bank` command, or the `exodus` hub skill routed here. Never claim generic "workflow"/"automation"/"inbox"/"promote a winner"/"continue the session" requests ("automate this", "build me a workflow", "check my inbox") without Exodus context — in shared folders those may belong to the user's other tools; if the user did not say exodus, this skill is not for them. The bare word "Genesis" without "exodus" refers to the member's own Genesis API key and personal bot recipes, NOT to Exodus.
4
4
  ---
5
5
 
6
6
  ```operator-guide
7
- Subcommands:
7
+ Core — discover & run:
8
8
  exodus workflow list [--json] List the brand's saved workflows
9
9
  exodus workflow describe <workflowId|name> [--json] Inputs, prerequisites, outputs
10
10
  exodus workflow bots [--category <cat>] [--slug <slug>] [--json] Bot catalog / one bot's spec
11
- exodus workflow run <workflowId|name> [--input key=value ...] [--wait] [--json] Run it
12
- exodus workflow status [--id <runId>] [--json] Poll a run / read its outputs
13
- exodus workflow export <workflowId|name> [--out <file>] Dump the contract JSON
14
- exodus workflow import <file> [--update <workflowId>] [--dry-run] [--json] Author/edit
15
-
16
- Key flags:
17
- --input key=value Repeatable run input; the value may contain "="
18
- --input key=@path Load the value from a file (path resolved from the cwd);
19
- use key=@@text to keep a leading "@" as a literal character
20
- --wait Poll until the run reaches a terminal status, then print outputs
21
- --id <runId> Run id for `status` detail
22
- --category <cat> (bots) Filter the catalog to one category id
23
- --slug <slug> (bots) Show a single bot's full port + param spec
24
- --out <file> (export) Write JSON to a file instead of stdout
25
- --update <workflowId> (import) Update this workflow in place (optimistic-concurrency
26
- guarded by the export's updatedAt — a 409 means re-export first)
27
- --dry-run (import) Validate + resolve refs without writing; returns issues
11
+ exodus workflow run <workflowId|name> [--input key=value ...] [--fill <name>] [--terminal <nodeId> ...] [--auto-approve] [--wait] [--out <dir>] [--json] Run it
12
+ exodus workflow status [--id <runId>] [--out <dir>] [--json] Poll a run / read its outputs
13
+ exodus workflow export <workflowId|name> [--version <n>] [--out <file>] [--json] Dump the contract (YAML)
14
+
15
+ Authoring:
16
+ exodus workflow templates [list] [--json] List starter templates
17
+ exodus workflow templates export <key> [--out <file>] [--json] Write a template's YAML to edit
18
+ exodus workflow schema [--kind <kind>] [--face <face>] [--json] Live graph vocabulary
19
+ exodus workflow validate <file> [--update <workflowId>] [--json] Check a file (import --dry-run's door)
20
+ exodus workflow import <file> [--update <workflowId>] [--dry-run] [--json] Create/update a workflow
21
+
22
+ Parked runs:
23
+ exodus workflow inbox [--json] Every run parked waiting on you
24
+ exodus workflow checkpoint <runId> [show | edit <n> | approve [--reject <n,..>] [--wait] | retry [--wait] | cancel [--reason "..."]] [--json] Resolve a checkpoint park
25
+ exodus workflow gate <runId> RETIRED in 2.0 — prints a pointer at "checkpoint" and exits 1
26
+ exodus workflow repair <runId> retry|skip|kill [--wait] [--json] Resolve a repair park
27
+ exodus workflow answer <runId> [--slot key=value ...] [--json] Answer a nested workflow's slots (no --slot → list them)
28
+
29
+ Sessions + triggers:
30
+ exodus session list [--json] Chat sessions on the active brand
31
+ exodus session show <sessionId> [--json] A session's turns
32
+ exodus session chat <sessionId> "message" [--json] Append a message, print the reply
33
+ exodus workflow triggers <workflowId|name> [enable <n> | disable <n> | fire [<n>] [--text "..."] [--wait]] [--json] List/flip/fire triggers
34
+
35
+ Banks + versions:
36
+ exodus bank list [--json] The brand's copy banks
37
+ exodus bank show <key> [--json] One bank's entries (newest-first)
38
+ exodus bank promote <key> [text] [--file <path>] [win/provenance flags] [--json] Land a winner (fires the flywheel)
39
+ exodus workflow versions <workflowId|name> [--json] A workflow's saved history
40
+
41
+ Flag notes that add agent-level guidance (everything else is in --help):
42
+ --input key=@path Load an input's value from a file (path resolved from cwd);
43
+ key=@@text keeps a leading "@" as a literal character.
44
+ --input key=<path> (run) A FILE input — one `describe` reports with source
45
+ "asset" — takes a local file path instead of text: the CLI
46
+ uploads the file and sends the stored asset in its place
47
+ (--input hero=./photos/hero.png). An http(s) URL works too
48
+ (--input hero=https://example.com/hero.png) — the server
49
+ fetches and registers it at launch. An asset id from an
50
+ earlier upload is relayed as-is. Accepted: image PNG/JPEG/
51
+ WebP/GIF ≤15MB, video MP4/MOV/WebM ≤200MB, audio MP3/M4A/
52
+ WAV/OGG ≤50MB, document PDF/TXT/MD/DOC/DOCX ≤25MB.
53
+ --terminal <nodeId> (run) Repeatable. Scope the run to the upstream closure of
54
+ these end node(s); omit to run the whole graph.
55
+ --auto-approve (run) Send this ONE run off unattended: every Checkpoint it
56
+ reaches is approved for you, exactly as it stands, and the
57
+ run records that nobody looked. Without it a Checkpoint
58
+ parks and waits for a person. Never a workflow setting —
59
+ you choose it per launch, so ask before using it.
60
+ --out <dir> (run --wait / status --id) Save the finished run's delivered
61
+ outputs into this directory and print the paths: text as
62
+ .md, storyboards/frame sets as .json, images/video/audio
63
+ downloaded. Unfulfilled slots are reported, not written.
64
+ --version <n> (export) A real 1-based id from `workflow versions`; a
65
+ version export carries NO triggers/description.
66
+ --reject <n,..> (checkpoint approve) Only when the checkpoint is holding
67
+ one arrival per item (a Splitter fan-out). Drops those
68
+ item numbers —
69
+ the "Item N of M" headings in `checkpoint show` — and
70
+ approves the rest. Rejecting every item is refused.
71
+
72
+ Every workflow/session/bank verb takes --json, and that output IS the machine API —
73
+ there is no separate agent command set.
28
74
 
29
75
  Auth: Bearer (EXODUS_API_KEY). The canvas lives on the dashboard at /workflows.
30
76
  ```
31
77
 
32
- # Workflow — Run, Chain, and Build Saved Automations
78
+ # Workflow — Run, Resolve, Build, and Automate Saved Workflows
33
79
 
34
- An Exodus **Workflow** is a saved graph the member builds on the dashboard canvas (`/workflows`): brief and primer sources feed Genesis bots, bots feed other bots or an image node, and an Output node collects the deliverables. This skill is the CLI operator for those graphs — discover and run them, chain one into the next, and author or edit the contract without opening a browser.
80
+ An Exodus **Workflow** is a saved graph the member builds on the dashboard canvas (`/workflows`): brief and primer sources feed Genesis bots, bots feed transforms, formatters, other bots, or an image node, deposits land copy in banks, a Checkpoint box on a wire stops the run for a human's approval, and an Output node collects the deliverables. This skill is the CLI operator for those graphs — a **peer door** to the same artifact the web canvas edits. Anything you can do to a run in the browser (approve a checkpoint, repair a stall, continue a session, promote a winner), you can do here.
35
81
 
36
- Three jobs, three sections below. Read the request and jump to the one that fits; don't walk all three when the user just wants a run.
82
+ **The `--json` contract (read this once).** Every `workflow`, `session`, and `bank` verb takes `--json`, and that structured output **is** the machine API — there is no separate agent command set and no MCP server. Read for humans without it; parse `--json` when you need to chain or branch on the result.
83
+
84
+ **Auth is one bearer key.** `EXODUS_API_KEY` from `.env` authorizes every verb; there are no scoped tokens. If a call 401s, run `npx @aicopycoders/exodus doctor`.
85
+
86
+ ## The agent operating loop
87
+
88
+ The end-to-end narrative, every step naming its verb. You rarely walk all of it — jump to the section you need — but this is the spine when you're driving a workflow from nothing to a promoted winner:
89
+
90
+ 1. **Author** — start from a template (`workflow templates`, then `templates export <key>`), edit the YAML, and pull live vocabulary with `workflow schema` as you go.
91
+ 2. **Validate** — `workflow validate <file>` until it comes back clean (it's `import --dry-run` under its own door).
92
+ 3. **Import** — `workflow import <file>` to create, or `import <file> --update <id>` to edit in place.
93
+ 4. **Describe** — `workflow describe` to confirm inputs, prerequisites (✓/✗ primers), and outputs before spending a run.
94
+ 5. **Run** — `workflow run … --wait` (streams progress, prints outputs) or omit `--wait` and poll `workflow status --id <runId>`. Inputs come from `--input key=value` flags, from `--fill <name>` (a saved fill — a named, reusable set of launch answers saved from the app's Run dialog; flags win per key when you pass both), or a mix. Launches that don't satisfy the workflow's entry contract are rejected at the door with the gap named — `describe` shows each input's type, required/optional, and accepted values. Add `--auto-approve` only when the run is meant to go start-to-finish with nobody watching: it approves every Checkpoint the run hits, unchanged, instead of stopping for a verdict. It's a per-launch choice, not a setting on the workflow, and the run keeps a record of each stop it waved through — so confirm with the user before you pass it.
95
+ 6. **Notice the park** — a run can stop and wait on you. There are no webhooks in v1; you notice a park one of three ways: `run --wait` prints a pause notice naming the verb to use (the command keeps waiting — resolve from another shell, or Ctrl-C and pick it up via `inbox`), `workflow inbox` lists it, or `workflow status` shows `awaiting-review`.
96
+ 7. **Resolve** — `workflow checkpoint <runId> …` (the run reached a Checkpoint box), `workflow repair <runId> …` (a stalled collector), or `workflow answer <runId> …` (a nested workflow's slots).
97
+ 8. **Harvest** — read deliverables with `workflow status --id <runId> --json`; read banked deposits with `bank show <key>`; keep thinking with a bot via `session chat`.
98
+ 9. **Promote** — `bank promote <key>` lands the winner **and** fires the Winner Flywheel; then check `workflow inbox` for any background run it kicked off.
37
99
 
38
100
  ## 1. Discover & run
39
101
 
@@ -55,7 +117,7 @@ npx @aicopycoders/exodus workflow describe "Launch Flow"
55
117
 
56
118
  Accepts a workflow id **or** a name. It reports three things:
57
119
 
58
- - **Inputs** — each free input the run needs: field name, source (`text`, `swipe-ad`, `swipe-bundle`, `organic-url`, `ad-url`), whether it's required, and a description. These are the `--input key=value` pairs you'll pass.
120
+ - **Inputs** — each free input the run needs: field name, source (`text`, `swipe-ad`, `swipe-bundle`, `organic-url`, `ad-url`, `asset`), whether it's required, and a description. These are the `--input key=value` pairs you'll pass. A source of **`asset`** is a **file** input — the workflow wants an actual image, video, audio clip, or document, and describe names which family it takes (e.g. `hero — asset (image), required`).
59
121
  - **Prerequisites** — the per-workspace primer markers the graph's primer nodes depend on, each shown **✓ stored** or **✗ missing** for *this brand*. **If any prerequisite is ✗, warn the user before running** — the run will not produce good output (or will fail) without it. Route them to the `exodus-primer` / `exodus-foundation` skill to fill the gap first, rather than burning a run.
60
122
  - **Outputs** — the deliverables the Output node collects (text or image, with labels), so you know what to expect back.
61
123
 
@@ -63,6 +125,18 @@ Accepts a workflow id **or** a name. It reports three things:
63
125
 
64
126
  Fill every required input. For a free-text brief you can compose a **bespoke brief from in-session research** — the winning angle you just worked out, the swipe you analyzed — and pass it inline (`--input brief="..."`) or, if it's long, write it to a project file and pass it with `@file` (see Chain, below). For URL/swipe sources, pass the value the source expects.
65
127
 
128
+ For an **`asset` (file) input, pass the path to a local file** and the CLI does the uploading for you — mint, push the bytes, register, then start the run with the stored asset:
129
+
130
+ ```bash
131
+ npx @aicopycoders/exodus workflow run "Product Shots" --input hero=./photos/hero.png --wait
132
+ ```
133
+
134
+ A bare path is the argument (a leading `@` is accepted and ignored — a file field never text-expands). Accepted files: **image** PNG/JPEG/WebP/GIF ≤15MB · **video** MP4/MOV/WebM ≤200MB · **audio** MP3/M4A/WAV/OGG ≤50MB · **document** PDF/TXT/MD/DOC/DOCX ≤25MB. **Every file you passed is checked first — all of them — before a single byte moves**, so one bad path can't leave the others half-uploaded; you get every problem in one message and fix them in one edit. If an upload itself dies partway, the message names the asset ids that DID land so you can pass those instead of re-uploading them.
135
+
136
+ There is no file picker and no prompt: a required file input with no `--input` fails immediately, naming the flag to pass — **unless you scoped the run with `--terminal`**, since the file's node may not even be in that slice; there the server decides. If you already uploaded the file on an earlier run, pass the asset id instead of the path and it's relayed as-is.
137
+
138
+ One edge case worth knowing: the CLI reads the workflow's input list before it uploads anything. If that read fails (the server is down or flaking) and any value you passed looks like a file path, the run stops right there rather than sending your path as literal text. Retry, or pass an already-uploaded asset id.
139
+
66
140
  ### Step 4 — Run and collect
67
141
 
68
142
  ```bash
@@ -75,95 +149,422 @@ npx @aicopycoders/exodus workflow run "Launch Flow" --input brief="new cortisol
75
149
  npx @aicopycoders/exodus workflow status --id <runId>
76
150
  ```
77
151
 
78
- Report the outputs plus a short take. Don't call a run "done" off the kickoff line alone — a run reaches `completed`, `partial`, or `failed`; a `partial` means some nodes were skipped or failed, so read the status before declaring victory.
152
+ To run only part of a graph, scope it with `--terminal <nodeId>` (repeatable): only nodes feeding the picked end node(s) execute; the rest are recorded out-of-scope.
79
153
 
80
- ## 2. Chain — feed one workflow's output into the next
154
+ **Read the `Deliveries:` block first.** Each Output node on the canvas is a named, typed promise — "Final ad copy · text", "Hero image · asset" — and a finished run answers every one of them: `delivered` with the artifacts underneath, or `unfulfilled` with the reason the upstream node gave. That block is the honest scorecard; the `Outputs:` block below it is the same artifacts as one flat pile, kept for chaining. If a run says `completed` but a slot says `unfulfilled`, the workflow did NOT deliver what it promised — say so.
81
155
 
82
- The chaining surface is the run's flattened outputs. After a `--wait` run (or once `status` shows terminal), pull the machine-readable outputs:
156
+ **Save the deliverables to disk with `--out <dir>`** (works on `run --wait` and on `status --id`):
83
157
 
84
158
  ```bash
85
- npx @aicopycoders/exodus workflow status --id <runId> --json
159
+ npx @aicopycoders/exodus workflow run "Launch Flow" --input brief="new offer" --wait --out ./deliverables
160
+ npx @aicopycoders/exodus workflow status --id <runId> --out ./deliverables
86
161
  ```
87
162
 
88
- The response carries a top-level `outputs` array — each entry is `{ nodeId, botSlug?, type, label, text?, imageUrl?, imageId? }`. Extract the text you want (e.g. `.outputs[].text`) and pass it into the next workflow's input:
163
+ Every delivered slot becomes a file named `<workflow>-<runId tail>-<slot key>.<ext>` — text as `.md`, storyboards and frame sets as `.json`, images/video/audio downloaded from their URLs — and each written path is printed. Unfulfilled slots are reported, never written. The directory is created if it doesn't exist.
164
+
165
+ Report the outputs plus a short take. Don't call a run "done" off the kickoff line alone — a run reaches `completed`, `partial`, `failed`, or **parks** (`awaiting-review`). A `partial` means some nodes were skipped or failed; a park means it's waiting on you (see the next section). Read `status` before declaring victory.
166
+
167
+ ## 2. Runs that park
168
+
169
+ A run doesn't always finish on its own — it can **park** and wait for a human decision. There are three actionable park kinds:
170
+
171
+ - **checkpoint** — the run reached a **Checkpoint** box on the canvas and is holding there until you approve what's flowing through it.
172
+ - **repair** — a require-all collector stalled on a dead input and needs a decision.
173
+ - **slots** — a nested sub-workflow is waiting on inputs (slot answers).
174
+
175
+ **No webhooks in v1 — nothing parks silently, but nothing pings you either.** You find parks three ways: `run --wait` prints a pause notice naming the verb to use (a park is not terminal — the command keeps polling so it can resume after you resolve; resolve from another shell, or Ctrl-C and pick it up via `inbox`); `workflow inbox` lists every parked run badged by kind (`checkpoint`/`repair`/`slots`; the badge `gate` shows up only on a pre-2.0 run parked at the retired Gate node — those can't be resumed, cancel and run the workflow again) and how it started (`bg`, `trig:<event>`); or `workflow status` shows `awaiting-review`. **`inbox` is the one place nothing hides** — including background runs a trigger fired or a promote's flywheel kicked off.
89
176
 
90
177
  ```bash
91
- # Inline, for short outputs
92
- npx @aicopycoders/exodus workflow run "Ad Writer" --input hooks="<text from run A>" --wait
178
+ npx @aicopycoders/exodus workflow inbox
93
179
  ```
94
180
 
95
- **The file-as-persistence convention.** For anything reusable or long — a brief, a sales letter, a hook pool — write the text to a **markdown file in the project** and pass it with `@file`. This is the deliberate v1 alternative to a server-side store: the file *is* the persistence layer, versioned in the project alongside the brand's other artifacts.
181
+ ### The `gate` verbs retired in 2.0
182
+
183
+ `exodus workflow gate …` no longer does anything: the Gate node it drove was
184
+ removed. Running it prints a pointer at `workflow checkpoint` and exits 1.
185
+
186
+ A run that parked at a Gate box **before** 2.0 is frozen there — it can't be
187
+ resumed, because the step it's waiting on no longer exists. Cancel it and run the
188
+ workflow again:
96
189
 
97
190
  ```bash
98
- # Save run A's text output to a project file, then feed it to workflow B
99
- npx @aicopycoders/exodus workflow status --id <runId> --json # copy .outputs[].text into state/hooks.md
100
- npx @aicopycoders/exodus workflow run "Ad Writer" --input hooks=@state/hooks.md --wait
191
+ npx @aicopycoders/exodus workflow checkpoint <runId> cancel --reason "legacy gate park"
101
192
  ```
102
193
 
103
- `@path` is resolved from the current directory. If the literal value you need actually starts with `@`, escape it as `@@` (e.g. `--input handle=@@brand` passes the string `@brand`).
194
+ ### Checkpoint parks — show / edit / approve / retry / cancel
195
+
196
+ A **Checkpoint** is its own box on the canvas, sitting on a wire between two steps: the step before it finishes, the work reaches the Checkpoint, and the whole run stops there until a human signs off. Because it's a box you can see, the diagram tells you exactly where a run will stop. It waits **indefinitely**, so a `run --wait` left unattended simply keeps polling; nothing times out and nothing is lost. A Checkpoint is never approved automatically — not in a background run, not in a triggered one. The single exception is a run someone launched with `run --auto-approve`, which is an explicit "nobody is watching this one" choice made at launch time; those runs approve each Checkpoint as they reach it and mark the stop as auto-approved.
197
+
198
+ The box itself does no work: whatever arrives goes straight back out, unchanged, the moment you approve.
199
+
200
+ There are **no candidates to pick** — there's held work to read and either bless, fix, or redo. Outputs are numbered **1-based**:
201
+
202
+ ```bash
203
+ npx @aicopycoders/exodus workflow checkpoint <runId> # show what's waiting at the checkpoint
204
+ npx @aicopycoders/exodus workflow checkpoint <runId> edit 1 --text "tighter opener" # rewrite an output in place
205
+ npx @aicopycoders/exodus workflow checkpoint <runId> approve --wait # sign off, the run continues
206
+ npx @aicopycoders/exodus workflow checkpoint <runId> retry --wait # redo the step feeding it, wait for fresh output
207
+ npx @aicopycoders/exodus workflow checkpoint <runId> cancel --reason "wrong direction" # stop the run here
208
+ ```
209
+
210
+ - **`edit`** takes `--text`, `--file <path>`, or piped stdin. It does **not** resume the run — edit first, then `approve`.
211
+ - **`retry`** discards what's waiting and re-runs the step **feeding** the checkpoint (re-running the checkpoint itself would hand back the identical text, since it only passes things through). The run parks right back at the *same* checkpoint with the fresh output — that re-park **is** the success. So `retry --wait` finishes as soon as the new output is ready, prints it, and hands you back the same four choices; it does **not** wait for the run to complete (it never would). Review the fresh output and `approve` when you're happy.
212
+ - **`cancel`** stops the run for good, recording your reason — the same audit trail the web uses.
213
+
214
+ **Authoring one** (when you're building or editing a graph from the CLI): add a `checkpoint` node and put it **on the wire** — producer → checkpoint `in`, checkpoint `out` → whoever used to receive it. `in` accepts any wire type so the box drops onto any wire; `out` is typed by `config.passType`, which must name the **same type as the wire feeding it** (`text` is the default and by far the common case). Get that wrong and `validate` returns `bad-config` naming the wire's real type.
215
+
216
+ ```yaml
217
+ - id: approve-hooks
218
+ kind: checkpoint
219
+ config:
220
+ passType: text
221
+ ```
222
+
223
+ **Older workflows convert themselves.** Before this, a pause-for-approval was a hidden switch inside a step's settings. Any graph that still carries one is rewritten automatically — the switched step becomes that step followed by a Checkpoint box — the moment it's opened, saved, imported, or restored. Never author the old `checkpoint` config key; it's dropped on sight. Runs that finished (or parked) under the old shape are left exactly as they are, and a run parked the old way still resolves through these same verbs.
224
+
225
+ #### When the work arrived once per item (a Splitter fan-out)
226
+
227
+ If a Splitter (below) sits upstream, the checkpoint saw **one arrival per item**, and `show` groups what's waiting under **`Item 1 of 7`**-style headings. The run still parks **once**, after every lane has finished — you review the whole batch in one sitting.
228
+
229
+ The numbering does **not** restart per item: rows are numbered straight through, so `edit 4` still means row 4 wherever it sits. What's new is that you can drop whole items:
230
+
231
+ ```bash
232
+ npx @aicopycoders/exodus workflow checkpoint <runId> # grouped by item
233
+ npx @aicopycoders/exodus workflow checkpoint <runId> approve --reject 2 --wait # keep all but item 2
234
+ npx @aicopycoders/exodus workflow checkpoint <runId> approve --reject 2,5 --wait # drop items 2 and 5
235
+ ```
236
+
237
+ - `--reject` takes the **item numbers you see** (1-based, comma-separated) — not row numbers, not the 0-based indexes in `--json`.
238
+ - **Rejecting is filtering, not failing.** The dropped items disappear from the batch and the run carries on with the survivors; nothing is marked failed and no warning is raised. Say it that way to the user: "kept 5 of 7."
239
+ - **Approve with no `--reject` keeps everything** — the ordinary approve, unchanged.
240
+ - **Rejecting every item is refused.** If nothing is worth keeping, `cancel` the run — that's the honest action, and the CLI relays the server's message telling you so.
241
+ - `edit <n>` works inside a fan exactly as it does anywhere else; edit the rows you want to fix, then approve (with or without `--reject`).
242
+
243
+ ### Repair parks — retry / skip / kill
244
+
245
+ ```bash
246
+ npx @aicopycoders/exodus workflow repair <runId> retry # re-run the dead input
247
+ npx @aicopycoders/exodus workflow repair <runId> skip # drop the dead input, continue with the rest
248
+ npx @aicopycoders/exodus workflow repair <runId> kill # give up on the run
249
+ ```
250
+
251
+ Add `--wait` to `retry`/`kill` to poll through to the next terminal state or park.
252
+
253
+ ### Slot parks — answer
254
+
255
+ A nested sub-workflow waiting on inputs is a **slots** park. Run `answer` with **no** `--slot` to list the pending slot ids, then supply them:
256
+
257
+ ```bash
258
+ npx @aicopycoders/exodus workflow answer <runId> # list the slot ids it wants
259
+ npx @aicopycoders/exodus workflow answer <runId> --slot tone=casual --slot length=short
260
+ ```
261
+
262
+ ## 3. Sessions
263
+
264
+ Some bots run in **session-mode**: their run leaves behind a live chat session you can keep talking to. You don't create sessions here — they're born from runs — but you can list, read, and continue them:
265
+
266
+ ```bash
267
+ npx @aicopycoders/exodus session list # sessions on the active brand
268
+ npx @aicopycoders/exodus session show sess_123 # the session's turns
269
+ npx @aicopycoders/exodus session chat sess_123 "make the hook punchier" # one-shot: append + print reply
270
+ ```
271
+
272
+ `session chat` is a one-shot append-and-reply, not an interactive REPL — one message, one reply per call. **Replies can take minutes** (the chat route's budget is ~5 minutes), so don't treat a long wait as a hang. The `workflow status` output names the session tied to a run — that's your run→chat jump when you want to keep thinking past what a run produced.
273
+
274
+ ## 4. Triggers
275
+
276
+ A **trigger** makes a workflow run itself — on a schedule (cron) or on a platform event (e.g. a promoted winner). Triggers live on the workflow and are addressed by **1-based position**, not by id:
277
+
278
+ ```bash
279
+ npx @aicopycoders/exodus workflow triggers "Winner Flywheel" # numbered list
280
+ npx @aicopycoders/exodus workflow triggers "Winner Flywheel" enable 1 # flip one switch on
281
+ npx @aicopycoders/exodus workflow triggers "Winner Flywheel" disable 1 # flip it off
282
+ npx @aicopycoders/exodus workflow triggers "Winner Flywheel" fire 1 --text "fake winner copy" --wait
283
+ ```
284
+
285
+ - **`enable`/`disable` flip exactly one switch** — never rewrite a trigger by re-importing the whole contract to toggle it. The CLI reads the live trigger list and sends a **fingerprint** of the trigger's fields as a guard, so if the workflow changed under you the flip **fails loud** rather than hitting the wrong trigger. If it fails, re-list and retry.
286
+ - **Add or remove triggers by editing the YAML export** (they live in the head export) and re-importing — enable/disable never adds or removes, only toggles.
287
+ - **`fire`** starts a **real** background run on the workflow owner's keys. An event trigger's `fire` needs `--text` (the input the event would carry); a cron trigger's `fire` rejects `--text`. Its checkpoints park into the **inbox** like any other run.
288
+
289
+ **The ruled test flow:** author the workflow with the trigger **disabled** → `fire <n> --text "fake winner copy"` to dry-fire it and watch the run (checkpoints land in the inbox) → `enable <n>` once you're happy with what it produces.
290
+
291
+ ## 5. Banks + the flywheel
292
+
293
+ **Banks** are the brand's copy libraries (hooks, body, headlines, …) that primers and workflows fold in. A fresh brand starts with six, all empty.
294
+
295
+ ```bash
296
+ npx @aicopycoders/exodus bank list # the brand's banks + entry counts
297
+ npx @aicopycoders/exodus bank show hooks # entries, newest-first, with source + win metrics
298
+ npx @aicopycoders/exodus bank promote hooks "Stop scrolling — your knees will thank you"
299
+ ```
300
+
301
+ `bank promote` lands a winning line **and fires the Winner Flywheel** — it emits the `winner-promoted` event with **exact web parity and no opt-out**. Any workflow with an enabled matching trigger then starts a background run. So **after a promote, check `workflow inbox`** for the background run it may have kicked off. Promote reads text from an argument, `--file <path>`, or stdin (one of the three), and takes optional flags for awareness tagging (`--awareness`, body bank), win metrics (`--spend`, `--roas`, `--ctr`, `--note`), and provenance (`--run`, `--node`) — see `bank promote --help`.
302
+
303
+ ## 6. Versions
304
+
305
+ Every import saves a **version**. You can list a workflow's history and export any past version:
306
+
307
+ ```bash
308
+ npx @aicopycoders/exodus workflow versions "Launch Flow" # saved history, newest-first
309
+ npx @aicopycoders/exodus workflow export "Launch Flow" --version 3 --out v3.yaml
310
+ ```
311
+
312
+ Version numbers are real **1-based ids** from `versions` (not offsets). Two patterns:
313
+
314
+ ```bash
315
+ # Diff two saved versions
316
+ npx @aicopycoders/exodus workflow export X --version 3 --out v3.yaml
317
+ npx @aicopycoders/exodus workflow export X --version 5 --out v5.yaml
318
+ diff v3.yaml v5.yaml
319
+
320
+ # Roll back to an earlier version
321
+ npx @aicopycoders/exodus workflow export X --version 3 --out v3.yaml
322
+ npx @aicopycoders/exodus workflow import v3.yaml --update <id>
323
+ ```
104
324
 
105
- Use this pattern to compose pipelines the canvas doesn't wire directly: run the research/hook workflow, persist its best output as a file, then run the writing workflow off that file — reproducibly, because the file stays.
325
+ A version export **intentionally carries no triggers or description** — those aren't versioned, so a rollback leaves them unchanged; the head export (`export` with no `--version`) is the only place triggers appear. Exporting a backup before a destructive `--update` is still good habit, but rollback is a real, supported move now.
106
326
 
107
- ## 3. Build & edit — author a workflow contract
327
+ ## 7. Build & edit — author a workflow contract
108
328
 
109
- The documented way to learn the contract format is to **export an existing workflow and study it** — there is no separate schema doc. The loop is: learn the bots → learn the format → compose → dry-run until clean → import → review on the canvas.
329
+ Cold-start is a **template, not a blank file** — there is no `workflow new`. Learn the vocabulary live (`workflow schema` replaces any notion of a static schema doc), start from a template, and treat `validate` as your compiler loop. The loop: pick a starter → export it → edit → check the schema for vocabulary → `validate` until clean → `import` → review on the canvas.
110
330
 
111
- ### Step 1 — Learn the bot catalog
331
+ ### Step 1 — Learn the bot catalog and the graph vocabulary
112
332
 
113
333
  ```bash
114
334
  npx @aicopycoders/exodus workflow bots # full catalog, grouped by category
115
- npx @aicopycoders/exodus workflow bots --category writing # one category
116
335
  npx @aicopycoders/exodus workflow bots --slug new-hook-bot # one bot's full port + param spec
117
- npx @aicopycoders/exodus workflow bots --json # raw catalog for machine use
336
+ npx @aicopycoders/exodus workflow schema # LIVE node kinds, ports, config rules, faces, wiring rules
337
+ npx @aicopycoders/exodus workflow schema --kind transform # one node kind
338
+ npx @aicopycoders/exodus workflow schema --kind formatter # the Formatter's two modes, bounds, and notes
339
+ npx @aicopycoders/exodus workflow schema --kind splitter # the Splitter's three modes, the item cap, and the lane rules
340
+ npx @aicopycoders/exodus workflow schema --kind checkpoint # the Checkpoint box: its two ports and config.passType
341
+ npx @aicopycoders/exodus workflow schema --face splitter # one transform face's ports + spec
118
342
  ```
119
343
 
120
- Per bot you get its **input ports** (id, what it accepts — `text` / `primer` / `image`, whether it's required, and for primer ports which `primerKinds` gate it — `body` / `hook` / `headline` / `summary`), its **params** (select/text/toggle/number/multiselect, with options and defaults), and its output type. This is what tells you which node can wire into which — a port only accepts an edge whose source produces a type it lists in `accepts`.
344
+ Per bot you get its **input ports** (id, what it `accepts` — `text` / `primer` / `image`, whether it's required, and for primer ports which `primerKinds` gate it), its **params**, and its output type. `workflow schema` prints the **live** graph grammar from the backend you're deployed against — node kinds, port ids, config rules, transform faces, collector policy, wiring rules — so what you author matches what will validate. This is always current; pull it fresh rather than trusting memory.
345
+
346
+ **Faces now carry a `retired` flag.** `schema` (and `schema --face <face>`) reports it per face. Five faces are retired — `parser` (Parser/Extractor), `edit-shorten` (Shorten), the old `formatter` reshaper, and (newest) `splitter` (Splitter (Text)) and `decomposer`. **Retired means "no new ones," not "broken":** they still validate, still run, and every existing graph that names one keeps working unchanged, so never rewrite a member's graph to get rid of one. Just don't author a fresh one — use the `formatter` node kind instead of `parser`/`formatter`, and the `splitter` **node kind** instead of the `splitter`/`decomposer` faces (both below). The ad-hoc edit-pass buttons on finished run results, Shorten included, are a different surface and are unaffected.
121
347
 
122
- ### Step 2 — Learn the format by exporting
348
+ ### Step 2 — Start from a template
123
349
 
124
350
  ```bash
125
- npx @aicopycoders/exodus workflow export "Launch Flow" --out launch.json
351
+ npx @aicopycoders/exodus workflow templates # list the starters (incl. Winner Flywheel)
352
+ npx @aicopycoders/exodus workflow templates export complete-ad-set --out my.yaml
126
353
  ```
127
354
 
128
- Study `launch.json`: it's the `exodus-workflow` contract — `nodes` (each with `id`, `kind` = `brief`/`bot`/`primer`/`image`/`output`, a `config`, and an optional `position`) and `edges` (`source`/`sourceHandle` → `target`/`targetHandle`, the handles being port ids). An export also carries `workflowId` + `updatedAt` at the top — those are update anchors, not part of the graph.
355
+ `templates export` writes the server-rendered YAML verbatim — a real, valid graph to edit down, not a stub. You can also learn the format by exporting an existing workflow (`workflow export "Launch Flow" --out launch.yaml`); export writes **canonical YAML** by default (fixed key order, so equal workflows dump byte-identically and diffs stay clean). Pass `--json` on export for the legacy JSON contract; import reads either.
356
+
357
+ The contract is `nodes` (each with `id`, `kind`, a `config`, and optional `position`) and `edges` (`source`/`sourceHandle` → `target`/`targetHandle`, the handles being **port ids**). **Node positions are optional** — omit them and the server auto-layouts on import. An export also carries `workflowId` + `updatedAt` at the top — those are update anchors, not part of the graph.
129
358
 
130
- ### Step 3 — Compose the contract
359
+ ### Step 3 — Validate like a compiler loop
360
+
361
+ ```bash
362
+ npx @aicopycoders/exodus workflow validate my.yaml
363
+ ```
131
364
 
132
- Author the JSON by hand or by adapting an export. **Node positions are optional** — leave them out and the server auto-layouts the graph on import; you don't need to compute coordinates.
365
+ `validate` is the front door; `import --dry-run` is the same check under a different door. It **needs network + login** — there is no offline validation, ever. It returns compiler-style issues, each naming the **node id**, the **port id** where relevant, and a concrete **remedy**. Codes include `dangling-edge`, `unknown-port`, `type-mismatch`, `missing-required-input`, `duplicate-input`, `cycle`, `unknown-kind`, `bad-config`, and the three Splitter lane rules `nested-split`, `lane-ineligible`, and `fan-overlap`. Fix, re-run `validate`, repeat until clean.
133
366
 
134
- ### Step 4 — Dry-run like a compiler loop
367
+ ### Step 4 — Import (create or update)
135
368
 
136
369
  ```bash
137
- npx @aicopycoders/exodus workflow import launch.json --dry-run
370
+ npx @aicopycoders/exodus workflow import my.yaml # create a new workflow
371
+ npx @aicopycoders/exodus workflow import my.yaml --update wf_123 # update in place
372
+ ```
373
+
374
+ Then **tell the user to review it on the canvas at `/workflows`** — the CLI writes the graph; the human eye confirms it reads right.
375
+
376
+ **The 409 rule.** `--update` sends the export's `updatedAt` as an optimistic-concurrency guard. If the workflow moved on the dashboard since your export, the import 409s. Re-export the current workflow, reapply your edits onto the fresh contract, and import again — never force past a 409, or you'll clobber their change.
377
+
378
+ ### The `formatter` node — guarantee the shape of what comes out
379
+
380
+ Reach for a `formatter` node whenever the next step needs a **known shape**: a rendered layout, or named fields split onto their own wires. Two modes on one kind, chosen by `config.mode`. Its ports are **derived from its config** in both modes, so edit the config and the handles change — re-check your edges after any edit.
381
+
382
+ **`mode: template`** — deterministic, no model call. Write a layout with `{{variable}}` slots; **each distinct slot becomes a required `text` input port named for the variable**, and the single output port is `formatted`. Same inputs → identical output, every time. A slot that gets nothing renders empty and warns; it never fails the run.
383
+
384
+ ```yaml
385
+ - id: assemble
386
+ kind: formatter
387
+ config:
388
+ mode: template
389
+ template: |
390
+ HOOK: {{hook}}
391
+
392
+ {{body}}
393
+
394
+ CTA: {{cta}}
395
+ ```
396
+
397
+ Ports: inputs `hook`, `body`, `cta` (all required, one wire each); output `formatted`.
398
+
399
+ **`mode: extract`** — the AI reads loose text, the node **validates the answer against your declaration**. Declare 1–12 fields; **each field key becomes its own output port**, and the one input port is `source` (required, accepts many wires — their texts are joined). A `list` field emits **one artifact per element** on its port, so downstream nodes get hooks one at a time, not one blob.
400
+
401
+ ```yaml
402
+ # Winner ad text in → hooks, offer, and a mechanism line out, each on its own wire.
403
+ - id: mine-winner
404
+ kind: formatter
405
+ config:
406
+ mode: extract
407
+ temperature: 0.2 # 0..1, default 0.2
408
+ maxRetries: 2 # 0..3, default 2 → up to 3 model attempts
409
+ fields:
410
+ - { key: hooks, label: Hooks, type: list, required: true }
411
+ - { key: offer, label: Offer, type: text, required: true }
412
+ - { key: mechanism, label: Mechanism, type: text, required: false }
413
+ ```
414
+
415
+ Ports: input `source`; outputs `hooks`, `offer`, `mechanism`. Wire `sourceHandle: hooks` into a hook-consuming bot.
416
+
417
+ **The contract that makes it worth using: extract mode never emits a wrong shape.** If the reply isn't a JSON object with exactly the declared keys, correctly typed, and non-empty where you marked `required`, the node re-asks the model with the exact violations attached. `maxRetries` counts those re-asks *after* the first attempt (default `2` = up to 3 attempts). When the budget is spent the node **fails the run** with the last answer and its errors on the failure note — it never passes nonconforming output downstream. Tell the user that plainly when an extract node fails: the model couldn't produce the declared shape, so the node refused to guess.
418
+
419
+ **Field rules** (each row needs all three of `key`/`type`/`required`): `key` is letters, digits, spaces, underscores, hyphens, ≤40 chars, unique in the list; `type` is `text`, `number`, or `list`; `required` must be an explicit `true`/`false`. `label` is optional and defaults to the key.
420
+
421
+ ### The `splitter` node — one thing in, N parallel lanes out
422
+
423
+ A **Splitter** turns one thing into many. Feed it a list (or text a model can cut into pieces) and it emits N **items** — and here is the part that matters: **every box after it runs once per item.** Five items means five copies of the same downstream chain running side by side, like five assembly lines. Each piece of work knows its own number, so the run view and the CLI can say "Item 3 of 7".
424
+
425
+ It has the same ports in every mode: one required input `source` (it accepts several wires), one output `items`. Three modes, picked by `config.mode`:
426
+
427
+ - **`structural`** — no model call. Whatever artifacts arrive on `source` **are** the items. This is the natural partner of a Formatter `list` field, which already emits one artifact per element. (A single artifact whose text is a JSON list of strings gets exploded too.)
428
+ - **`rule`** — no model call. Joins the incoming text and cuts it on a `delimiter`: `newline`, `blank-line`, `numbered` (1. 2. 3. list markers), or `custom` plus a `customPattern` (a **literal** separator, not a regular expression).
429
+ - **`semantic`** — the only mode that calls a model. Your `instruction` says what counts as one item ("separate this into ad concepts"); the reply must be a clean list, and a bad one is re-asked before the node gives up. Set `model` to pick which writing model does the cutting; leave it out for the default one.
430
+
431
+ ```yaml
432
+ - id: split-modules
433
+ kind: splitter
434
+ config:
435
+ mode: semantic
436
+ instruction: "Break this winning ad into its reusable modules — hook, mechanism, proof, offer, CTA. One module per item."
437
+ maxItems: 25
438
+ ```
439
+
440
+ **Aiming a `structural` Splitter at one field.** A bot that answers in JSON usually hands you a whole payload — `{"hooks": ["…", "…"], "notes": "…"}` — and splitting *that* gives you one item: the blob. Set `sourceField: hooks` and the Splitter reaches inside and splits **just that list** instead, ignoring everything else. If several payloads arrive on the wire, their lists are pooled in the order they landed.
441
+
442
+ ```yaml
443
+ - id: split-hooks
444
+ kind: splitter
445
+ config:
446
+ mode: structural
447
+ sourceField: hooks
448
+ maxItems: 25
449
+ ```
450
+
451
+ Two things to know. First, an aimed Splitter is **strict**: every payload arriving has to be a JSON object that actually has that field, and the field has to hold a list of text. Anything else stops the step with a plain sentence naming the field — it never quietly falls back to splitting the raw payload, because a one-item fan that "worked" is the worst possible answer. Second, don't aim a Splitter that's fed **from a Formatter field**: that wire already carries the values one by one, so there's nothing left to reach inside, and the run is blocked (`splitter-field-conflict`) with a note telling you to clear the field name. Leave `sourceField` out entirely for the original behaviour — whatever arrives *is* the list. The field name is one word, up to 60 characters, not a path.
452
+
453
+ **`maxItems` is a hard cap and it is required in every mode** (default 25, raise it to at most 100 deliberately — you cannot remove it). Going over the cap **fails the node loudly** with the real count ("Splitter produced 61 items; cap is 25"). It never quietly trims the list: silent data loss is worse than a visible failure. Producing **zero** items fails too, for the same reason.
454
+
455
+ **Reviewing a fan item by item.** Put a **Checkpoint node inside the fan** and the run parks once, after all the lanes finish, and you approve or reject **each item**. Rejecting is filtering, not failing — the lanes below a rejected item simply never run. See "When the work arrived once per item" above.
456
+
457
+ **Three rules the validator enforces — all three scoped to a Splitter's OPEN fan** (the chain running once per item, which ends at the Collector that closes it):
458
+
459
+ - **No split inside an open fan** (`nested-split`) — a Splitter can't sit inside another Splitter's open fan. An artifact carries exactly one item number, so items-of-items has nowhere to live. The way through is **sequential**: close the first fan with a Collector, then split the combined result again. That is fully legal.
460
+ - **One open fan per node** (`fan-overlap`) — no node may sit inside two different Splitters' open fans at once. Their item numbering is unrelated, so the lanes can't merge mid-fan. Two fans that have **each closed through their own Collector** may merge downstream freely.
461
+ - **Lane eligibility** (`lane-ineligible`) — every node inside the open fan must be a `bot`, `formatter`, `transform`, `checkpoint`, or `output` node. Video nodes, Call nodes, and the video member-gate kinds are blocked, because running them once per item would corrupt state they keep per node. The `collector` kind is **exempt** (it ends the lane rather than running in it), and **after** a Collector any kind at all is allowed again — video, Call, all of it.
462
+
463
+ **One lane failing doesn't sink the batch.** If a lane exhausts its retries, that item drops out and the others carry on; the step ends `done` with a warning naming the casualties. Only *every* item failing fails the step. (A Collector downstream can be told to be stricter than that — see the next section.)
464
+
465
+ **The old cards are gone, the old graphs are fine.** The `splitter` (Splitter (Text)) and `decomposer` **transform faces** are retired — they put N snippets on one wire and ran downstream **once**, which is a different thing entirely. Existing workflows that use them keep running exactly as before, and there is no migration; just don't author new ones. The Winner Flywheel starter has been rebuilt on the Splitter node, so its Output step now files each module separately.
466
+
467
+ **Blocks Run** (`bad-config`, so `validate` catches it): no/unknown `mode`; template mode with an empty template, over 20000 characters, or more than 12 distinct slots; extract mode with fewer than 1 or more than 12 fields, a bad/duplicate key, a bad type, a non-boolean `required`, a `temperature` outside 0..1, or a `maxRetries` outside 0..3. A template slot with no wire into it is `missing-required-input` — same block, different code.
468
+
469
+ ### The `collector` node — close the lanes
470
+
471
+ A **Collector** is the Splitter's other half. Where a Splitter opens N lanes, a Collector waits for all of them and folds them back into **one** result. That result carries no item number, which is the whole point: **after a Collector the run is ordinary again.** Anything may follow it, you may split again, and two fans that each closed through their own Collector may merge.
472
+
473
+ Its ports are fixed in every mode: one required input `items` (a whole fan lands on that single port), one output `combined`. Three modes, picked by `config.mode`:
474
+
475
+ - **`assemble`** — no model call. Stitches the surviving items into one block, in order, joined by `separator` (`newline`, `blank-line`, or `custom` plus a literal `customSeparator`; newline if you say nothing). An optional `itemTemplate` wraps each item, with the slots `{{item.value}}`, `{{item.index}}` (1-based), and `{{item.total}}`.
476
+ - **`list`** — no model call. Emits one artifact whose text is JSON: the items with their numbers, a count, and what dropped out. Use it when the next step wants structure, not prose. It is the only mode that puts the dropped detail *in* the artifact.
477
+ - **`synthesize`** — the only mode that calls a model. Your `instruction` says what the merge should produce; the model writes one new piece out of every item. Set `model` to pick which writing model does the merging, or leave it out for the default one; `temperature` and `maxRetries` work here too.
478
+
479
+ ```yaml
480
+ - id: fold-modules
481
+ kind: collector
482
+ config:
483
+ mode: assemble
484
+ separator: blank-line
485
+ itemTemplate: |
486
+ ### Module {{item.index}} of {{item.total}}
487
+ {{item.value}}
488
+ onBranchFailure: survivors
489
+ minItems: 2
138
490
  ```
139
491
 
140
- `--dry-run` validates and resolves refs without writing. It returns compiler-style issues — each names the **node id**, the **port id** where relevant, and a concrete **remedy** (the exact edit that fixes it). Codes you'll see include `dangling-edge`, `unknown-port`, `type-mismatch`, `missing-required-input`, `duplicate-input`, `cycle`, `unknown-kind`, and `bad-config`. Fix, re-run `--dry-run`, repeat until it's clean — same loop as fixing type errors.
492
+ **The lane policies ride every mode.** `onBranchFailure: survivors` (the default) carries on with whatever made it through and warns you who didn't; `strict` stops the run right there if any lane broke. `minItems` (default 1, up to 100) is a floor that applies under **both** policies — fewer survivors than that and the node fails, so a fan where everything died can never quietly hand you an empty result. `ordering` is `index` (the Splitter's original order, the default) or `arrival` (whichever lane finished first).
141
493
 
142
- ### Step 5 — Import (create or update)
494
+ **A rejection is never a failure.** An item the user rejected at a checkpoint was filtered on purpose, so it never trips `strict` — it only counts against `minItems` by not being there. The node's warning spells the difference out, and it's the sentence to quote back to a user asking why the run kept going: *"Collected 7 of 10 items — items 2 and 5 rejected at your checkpoint; item 8 failed."*
495
+
496
+ **Every wire into a Collector must come from inside ONE Splitter's open fan** (`collector-unpaired`, blocks Run) — the Splitter itself or a node running in its lanes. A Collector with nothing split upstream has no fan to close, and a Brief wired straight into one is merging unrelated branches, which is a Formatter's job in template mode, not a Collector's.
497
+
498
+ **The old card is gone, the old graphs are fine.** The `collector` **transform face** is retired — it compiled whatever text arrived on merged wires and ran once, which is a different job from closing a lane fan. Existing workflows using the face keep running unchanged and there is no migration; author the `collector` **node** instead. Note the vocabulary split while you're at it: the node reads `onBranchFailure`, and the legacy `onFailedInput` key is rejected on it outright.
499
+
500
+ **Blocks Run** (`bad-config`): no/unknown `mode`; assemble mode with an illegal `separator`, an empty or over-200-character `customSeparator` on `custom`, or an `itemTemplate` over 4000 characters; synthesize mode with an empty or over-2000-character `instruction`, a `temperature` outside 0..1, or a `maxRetries` outside 0..3; any mode with an illegal `onBranchFailure`/`ordering`, a `minItems` outside 1..100, or a stray `onFailedInput`.
501
+
502
+ ### Authoring gotchas
503
+
504
+ A hand-picked list of the traps that bite first. `workflow schema` is authoritative on any conflict with what's written here:
505
+
506
+ - **Edge handles are port ids** — `sourceHandle`/`targetHandle` name a **port**, not a node id or a label. An edge only connects if the target port `accepts` the source port's type.
507
+ - **Required inputs hard-block** — a run refuses to start until every required input is supplied; there's no "run anyway".
508
+ - **Transform faces are sealed** — each face has fixed, per-face **output port ids** you must wire from by name; `workflow schema --face <face>` prints them, along with whether the face is `retired`.
509
+ - **Don't author a retired face** — `parser`, `edit-shorten`, the old `formatter` face, `splitter` / `decomposer`, and now `collector` are no longer offered; author a `formatter` node (parser/formatter), a `splitter` node (splitter/decomposer), or a `collector` node instead. Existing graphs that use them keep running, so leave those alone.
510
+ - **The fan rules are scoped to the OPEN fan** — inside a Splitter's open fan (the chain up to the Collector that closes it) you may not place another Splitter (`nested-split`), every node must be a `bot`, `formatter`, `transform`, `checkpoint`, or `output` (`lane-ineligible`), and no node may sit in two Splitters' open fans at once (`fan-overlap`). Close the fan with a Collector and all three restrictions lift: split again, run any kind, merge two collected fans.
511
+ - **A Collector must be paired** — every wire into a `collector` node has to come from inside one Splitter's open fan, or it's `collector-unpaired` and the run is blocked. To merge branches that were never split, use a Formatter in template mode.
512
+ - **Don't aim a Splitter at a Formatter field** — a `structural` Splitter with `sourceField` set, fed from a Formatter field, is `splitter-field-conflict` and the run is blocked. That wire already hands over the values one at a time, so there's no payload left to reach into. Clear the field name, or feed the Splitter one whole JSON payload instead.
513
+ - **A Checkpoint's declared type must match the wire feeding it** — a `checkpoint` node's `config.passType` (default `text`) has to be the type arriving on `in`, or it's `bad-config` and the run is blocked. And never author the old per-node `checkpoint` switch key: it's retired, and any graph still carrying it is auto-converted into Checkpoint nodes on the way in.
514
+ - **A Formatter's handles come from its own config** — template slots name its inputs, field keys name its outputs. Change the config and old edges go `unknown-port`.
515
+ - **`session` outputs have nowhere to wire** — a session-typed output exists so you can re-open the chat from the run page; no node kind accepts a session wire.
516
+ - **Deposit shapes are strict** — a deposit's config must match the target bank's expected shape exactly, or import rejects it.
517
+ - **Empty defaults are deliberately invalid** — `rig`, `call`, `transform`, `bot`, and `formatter` nodes ship with empty config placeholders that fail validation on purpose; you MUST fill them, not leave them.
518
+ - **Quote YAML that looks numeric or boolean** — `"true"`, `"3.0"`, `"01"` need quotes or the parser coerces them to the wrong type.
519
+ - **A 409 on `--update` means re-export first** — reapply your edits onto the fresh contract; don't force.
520
+
521
+ ## 8. Chain — feed one workflow's output into the next
522
+
523
+ The chaining surface is the run's flattened outputs. After a `--wait` run (or once `status` shows terminal), pull the machine-readable outputs:
143
524
 
144
525
  ```bash
145
- # Create a new workflow
146
- npx @aicopycoders/exodus workflow import launch.json
526
+ npx @aicopycoders/exodus workflow status --id <runId> --json
527
+ ```
528
+
529
+ The response carries a top-level `outputs` array — each entry is `{ nodeId, botSlug?, type, label, text?, imageUrl?, imageId? }`. Extract the text you want (e.g. `.outputs[].text`) and pass it into the next workflow's input:
147
530
 
148
- # Update an existing workflow in place
149
- npx @aicopycoders/exodus workflow import launch.json --update wf_123
531
+ ```bash
532
+ # Inline, for short outputs
533
+ npx @aicopycoders/exodus workflow run "Ad Writer" --input hooks="<text from run A>" --wait
150
534
  ```
151
535
 
152
- Then **tell the user to review it on the canvas at `/workflows`** — the CLI writes the graph; the human eye confirms it reads right.
536
+ **The file-as-persistence convention.** For anything reusable or long — a brief, a sales letter, a hook pool — write the text to a **markdown file in the project** and pass it with `@file`. This is the deliberate v1 alternative to a server-side store: the file *is* the persistence layer, versioned in the project alongside the brand's other artifacts.
537
+
538
+ ```bash
539
+ # Save run A's text output to a project file, then feed it to workflow B
540
+ npx @aicopycoders/exodus workflow status --id <runId> --json # copy .outputs[].text into state/hooks.md
541
+ npx @aicopycoders/exodus workflow run "Ad Writer" --input hooks=@state/hooks.md --wait
542
+ ```
543
+
544
+ `@path` is resolved from the current directory. If the literal value you need actually starts with `@`, escape it as `@@` (e.g. `--input handle=@@brand` passes the string `@brand`).
545
+
546
+ For persistence that lives *inside* Exodus rather than in a file, remember **banks**: a run can deposit copy into a bank, and you read it back with `bank show <key>` — that's the server-side equivalent of the file convention.
547
+
548
+ ## Direct HTTP (if you're scripting past the CLI)
549
+
550
+ The same contract is on the v2 API, so a script can skip the CLI:
153
551
 
154
- **Two rules for edits (`--update`):**
552
+ - **Export:** `GET /api/v2/workflows/export?id=<workflowId>&format=yaml` returns the canonical YAML (`text/yaml`); drop `format` (or `format=json`) for the legacy JSON body.
553
+ - **Import:** `POST /api/v2/workflows/import`. A JSON object body carries the contract *plus* the control fields inline (`dryRun`, `targetWorkflowId`, `expectedUpdatedAt`). A raw **YAML** body is the contract text alone — pass the control fields as **query params** instead (`?dryRun=true&targetWorkflowId=<id>&expectedUpdatedAt=<ts>`).
155
554
 
156
- - **Always export a backup first.** v1 has no version history — a destructive `--update` overwrites the live workflow with no undo. `export --out backup.json` before you import over it.
157
- - **A 409 means the canvas moved.** `--update` sends the export's `updatedAt` as an optimistic-concurrency guard. If someone edited the workflow on the dashboard since your export, the import 409s. Re-export the current workflow, reapply your edits onto the fresh contract, and import again — never force past a 409, or you'll clobber their change.
555
+ The inbox/checkpoint/session/bank verbs have v2 routes too, but the CLI is the maintained path — reach for HTTP only when you're already scripting, and prefer the CLI's `--json` output over hand-rolling routes. Both are Bearer-auth'd (`EXODUS_API_KEY`) like every v2 route.
158
556
 
159
557
  ## Failure handling
160
558
 
161
559
  - **`describe` shows a ✗ prerequisite** — don't run yet. Fill the primer via `exodus-primer` / `exodus-foundation`, then re-check `describe`.
162
- - **Run comes back `partial` or `failed`** — read `status --id <runId>` (drop `--json` for a readable view) to see which node failed and why before re-running.
163
- - **`import --dry-run` issues** — fix the exact node/port each issue names using its remedy; don't guess.
164
- - **`import --update` 409** — re-export, reapply, re-import (see Step 5). This is a conflict, not a bug.
560
+ - **Run comes back `partial` or `failed`** — read `status --id <runId>` (drop `--json` for a readable view) to see which node failed and why **before re-running**. A partial still has usable outputs; harvest them first.
561
+ - **A verb refuses because the run isn't in that state** — the checkpoint/repair/answer verbs preflight the run and name its **actual** state (e.g. "parked for repair" when you tried a checkpoint verb). Read the state it reports and switch to the matching verb, or check `inbox` for what's actually pending.
562
+ - **Trigger fingerprint mismatch** — the workflow changed since you listed its triggers. Re-list (`workflow triggers <wf>`) and retry the enable/disable/fire against the fresh numbering.
563
+ - **`validate` / `import` errors** — fix the exact node/port each issue names using its remedy; don't guess. `validate` needs network + login — there is no offline check.
564
+ - **`import --update` 409** — re-export, reapply, re-import. This is a conflict, not a bug.
565
+ - **After a promote** — the success line includes the flywheel note; check `workflow inbox` for the background run it may have started.
165
566
  - **Auth / whoami failure** — run `npx @aicopycoders/exodus doctor` and confirm `EXODUS_API_KEY`.
166
- - **Wrong brand** — workflows are per-brand. `npx @aicopycoders/exodus brand current` to confirm you're on the brand you think you are; switch with the `exodus-brand` skill.
567
+ - **Wrong brand** — workflows, sessions, and banks are per-brand. `npx @aicopycoders/exodus brand current` to confirm you're on the brand you think you are; switch with the `exodus-brand` skill.
167
568
 
168
569
  ## Not the right skill for
169
570