|
| 1 | +# Converting Claude dynamic workflows to rig |
| 2 | + |
| 3 | +Read this reference when porting a Claude Code dynamic workflow script |
| 4 | +(`.claude/workflows/*.workflow.js`) to a rig `workflow()` program, or when |
| 5 | +comparing the two API surfaces. |
| 6 | + |
| 7 | +A dynamic workflow is a sandboxed JavaScript module that exports a literal |
| 8 | +`meta`, reads `args`, and orchestrates subagents with injected globals |
| 9 | +(`agent`, `parallel`, `pipeline`, `phase`, `log`, `budget`). rig covers the same |
| 10 | +shape with a typed `workflow({ meta, input, body })` whose `body` receives those |
| 11 | +primitives from its context instead of from globals. |
| 12 | + |
| 13 | +## Primitive mapping |
| 14 | + |
| 15 | +| Dynamic workflow | rig | Notes | |
| 16 | +| --- | --- | --- | |
| 17 | +| `export const meta = { name, description, phases, whenToUse }` | `workflow({ meta: { name, description, phases, whenToUse } })` | `phases` accepts `"Title"` or `{ title, detail }`; `meta` may reference variables | |
| 18 | +| `args` (JSON string or object) | `input` schema + `context.input` | Parsed and validated by the launcher; no defensive `JSON.parse` | |
| 19 | +| `await agent(prompt)` | `await call.text(prompt, options?)` | Returns `string \| null` | |
| 20 | +| `await agent(prompt, { schema })` | `await call.json(prompt, schema, options?)` | `schema` is `s.object({ ... })`; result is typed and validated | |
| 21 | +| Reused prompt + schema pair | `agent({ input, output, instructions })` then `call(worker, input, options?)` | Preferred for anything invoked more than once | |
| 22 | +| `parallel(thunks)` | `parallel(thunks)` | Same barrier semantics; failures become `null` holes | |
| 23 | +| `pipeline(items, ...stages)` | `pipeline(items, ...stages)` | Stages receive `(previous, item, index)`; the first stage's `previous` is the item | |
| 24 | +| `phase(title)` | `phase(title)` | Same | |
| 25 | +| `{ phase: "Verify" }` on a call | `{ phase: "Verify" }` in call options | Overrides the ambient phase for that call only | |
| 26 | +| `{ label: "verify:x" }` | `{ label: "verify:x" }` | Appears in `agent_start`/`agent_done` events | |
| 27 | +| `{ model: "sonnet" }` | `{ model: "sonnet" }` | rig passes the id straight to the engine | |
| 28 | +| `{ effort: "high" }` | — | Not modeled; encode importance structurally or in the model id | |
| 29 | +| `{ agentType: "Explore" }` | Prompt wording plus a narrow `tools` list | rig has no built-in read-only agent type | |
| 30 | +| `{ timeoutMs }` / `{ retries }` | `{ timeout }` on the call; `maxTurns` + `repair()` on the agent | rig retries are turn-based, not process-based | |
| 31 | +| `log(message)` | `log(message)` | Same | |
| 32 | +| `budget.total / spent() / remaining()` | `budget.total / spent() / remaining()` | rig meters **agent calls** (`limits.maxAgents`), not tokens | |
| 33 | +| `workflow(ref, args)` | `call.workflow(child, args, options?)` | Shares the limiter, budget, phase, and event stream | |
| 34 | +| top-level `return value` | `return value` from `body` | Same | |
| 35 | +| `Workflow({ scriptPath, args })` from a session | `cat args.json \| node skills/rig/rig.ts program.ts` | See [runtime](runtime.md) | |
| 36 | + |
| 37 | +## Schema conversion |
| 38 | + |
| 39 | +Dynamic workflows pass OpenAI-strict JSON Schema literals. rig schemas are |
| 40 | +`s.*` values that compile to the same JSON Schema, so conversion is mechanical: |
| 41 | + |
| 42 | +| JSON Schema | rig | |
| 43 | +| --- | --- | |
| 44 | +| `{ type: "string" }` | `s.string` | |
| 45 | +| `{ type: "string", description: "d" }` | `s.string("d")` | |
| 46 | +| `{ type: "integer" }` | `s.int` | |
| 47 | +| `{ type: "boolean" }` | `s.boolean` | |
| 48 | +| `{ type: "string", enum: [...] }` | `s.enum("a", "b")` | |
| 49 | +| `{ type: "array", items: X }` | `s.array(X)` | |
| 50 | +| `{ type: "object", properties, required, additionalProperties: false }` | `s.object({ ... })` | |
| 51 | +| `{ anyOf: [X, { type: "null" }] }` | `s.nullable(X)` | |
| 52 | +| omitted from `required` | `s.optional(X)` | |
| 53 | + |
| 54 | +`additionalProperties: false` and a full `required` list are implicit in |
| 55 | +`s.object`, so drop them. Use `s.path` for file paths and `s.url` for URIs. |
| 56 | + |
| 57 | +## Worked conversion |
| 58 | + |
| 59 | +Original dynamic workflow: |
| 60 | + |
| 61 | +```js |
| 62 | +export const meta = { |
| 63 | + name: 'audit', |
| 64 | + description: 'Find and verify issues', |
| 65 | + phases: [{ title: 'Find' }, { title: 'Verify' }], |
| 66 | +} |
| 67 | + |
| 68 | +const FINDINGS = { |
| 69 | + type: 'object', additionalProperties: false, |
| 70 | + required: ['findings'], |
| 71 | + properties: { |
| 72 | + findings: { type: 'array', items: { |
| 73 | + type: 'object', additionalProperties: false, |
| 74 | + required: ['title', 'file'], |
| 75 | + properties: { title: { type: 'string' }, file: { type: 'string' } }, |
| 76 | + } }, |
| 77 | + }, |
| 78 | +} |
| 79 | + |
| 80 | +phase('Find') |
| 81 | +const found = await parallel(args.areas.map((area) => () => |
| 82 | + agent(`Audit ${area}. Report findings.`, { label: area, schema: FINDINGS }))) |
| 83 | + |
| 84 | +phase('Verify') |
| 85 | +const verified = await pipeline( |
| 86 | + found.filter(Boolean).flatMap((r) => r.findings), |
| 87 | + (finding) => agent(`Verify: ${finding.title} in ${finding.file}.`, { |
| 88 | + phase: 'Verify', schema: VERDICT, |
| 89 | + }), |
| 90 | +) |
| 91 | +return verified.filter(Boolean).filter((v) => v.real) |
| 92 | +``` |
| 93 | + |
| 94 | +Ported to rig: |
| 95 | + |
| 96 | +```ts |
| 97 | +import { s, workflow } from "rig"; |
| 98 | + |
| 99 | +const finding = s.object({ title: s.string, file: s.path }); |
| 100 | + |
| 101 | +// Workflow role: audit areas in parallel, then verify each finding. |
| 102 | +const audit = workflow({ |
| 103 | + meta: { |
| 104 | + name: "audit", |
| 105 | + description: "Find and verify issues", |
| 106 | + phases: [{ title: "Find" }, { title: "Verify", detail: "one verifier per finding" }], |
| 107 | + }, |
| 108 | + input: s.object({ areas: s.array(s.string) }), |
| 109 | + body: async ({ call, input, parallel, phase, pipeline }) => { |
| 110 | + phase("Find"); |
| 111 | + const found = await parallel(input.areas.map((area) => () => |
| 112 | + call.json(`Audit ${area}. Report findings.`, s.object({ findings: s.array(finding) }), { label: area }))); |
| 113 | + |
| 114 | + phase("Verify"); |
| 115 | + const verified = await pipeline( |
| 116 | + found.flatMap((result) => result?.findings ?? []), |
| 117 | + (f: { title: string; file: string }) => |
| 118 | + call.json(`Verify: ${f.title} in ${f.file}.`, s.object({ real: s.boolean }), { phase: "Verify" }), |
| 119 | + ); |
| 120 | + return verified.filter((v) => v?.real); |
| 121 | + }, |
| 122 | +}); |
| 123 | + |
| 124 | +export default audit; |
| 125 | +``` |
| 126 | + |
| 127 | +## Behavior differences to keep in mind |
| 128 | + |
| 129 | +- **Failure holes.** `call` returns `null` when an agent fails, and `parallel` |
| 130 | + turns rejected thunks into `null`. A rig `pipeline` stage that throws fails the |
| 131 | + whole run instead of dropping that item to `null`, so programming bugs stay |
| 132 | + visible; wrap a stage in `try`/`catch` when you want the Claude behavior. |
| 133 | +- **Budget units.** rig counts agent calls, not tokens, so guard loops with |
| 134 | + `budget.remaining() > n` where `n` is a call count. |
| 135 | +- **Nesting depth.** `call.workflow` has no one-level restriction, but it shares |
| 136 | + the parent's `maxAgents` and concurrency, so a nested run cannot escape the |
| 137 | + parent's limits. |
| 138 | +- **No sandbox restrictions.** rig programs are normal TypeScript modules: |
| 139 | + `Date.now()`, imports, and Node built-ins are allowed, and the launcher owns |
| 140 | + isolation instead of the runtime. |
| 141 | +- **No resume journal, worktree isolation, or human checkpoints.** These are |
| 142 | + runtime features of Claude Code, not API surface; a rig workflow that needs a |
| 143 | + human decision should return a structured `needs_human` result and be re-run |
| 144 | + with the answer in its input. |
| 145 | + |
| 146 | +## Related references |
| 147 | + |
| 148 | +- [Dynamic workflows](dynamic-workflows.md) — full rig workflow API. |
| 149 | +- [Agent API and schemas](agent-api.md) — `s.*` helpers and call options. |
| 150 | +- [Running and engines](runtime.md) — launching a workflow program. |
0 commit comments