Skip to content

Commit 77056ad

Browse files
Copilotpelikhan
andauthored
Document Claude dynamic-workflow conversion and add port sample
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent e49cc69 commit 77056ad

5 files changed

Lines changed: 227 additions & 8 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,7 @@ cat program.ts | node skills/rig/rig.ts --typecheck
6565

6666
## Docs
6767

68-
See [skills/rig/SKILL.md](skills/rig/SKILL.md) for construction rules and
69-
[skills/rig/references/runtime.md](skills/rig/references/runtime.md) for launcher and engine details.
68+
See [skills/rig/SKILL.md](skills/rig/SKILL.md) for construction rules,
69+
[skills/rig/references/runtime.md](skills/rig/references/runtime.md) for launcher and engine details, and
70+
[skills/rig/references/claude-workflow-conversion.md](skills/rig/references/claude-workflow-conversion.md)
71+
for porting Claude Code dynamic workflows to rig.

skills/rig/SKILL.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ Defaults: `name: "agent"`, `model: "small"`, `maxTurns: 4`, string input/output,
6060
| Numeric schema choice | `s.int` for counts/line numbers; `s.number` for measurements and ratios |
6161
| Optional versus nullable | `s.optional(shape)` for omission; `s.nullable(shape)` for explicit `null` |
6262
| Deterministic TypeScript fan-out | `workflow({ meta, input?, body })` + `export default`; use `call`, `pipeline`, `parallel`, `until` inside `body` |
63+
| One-off prompt inside a workflow | `call.text(prompt)` for a string, `call.json(prompt, schema)` for structured output |
64+
| Reusable workflow step | Define an `agent({ input, output })` and `call(worker, input, { label, phase })` |
6365
| Custom model-callable operation | `defineTool(name, { description, parameters, handler })` |
6466
| Structured-output retries | `maxTurns` on the agent plus `addons: [repair()]` |
6567
| Retry with final-turn warning | `addons: [steering(), repair()]` in that order |
@@ -103,6 +105,7 @@ Read only when the task needs the listed detail:
103105
- [Agent API and schemas](references/agent-api.md) — spec fields, schema overloads, tools, and invocation options.
104106
- [Prompt intents](references/prompt-intents.md) — complete helper semantics, dynamic inputs, writes, and failure behavior.
105107
- [Composition and addons](references/composition.md) — delegation patterns, dynamic sets, repair, steering, and addon lifecycle.
106-
- [Dynamic workflows](references/dynamic-workflows.md) — bounded fan-out, failure semantics, limits, events, and convergence loops.
108+
- [Dynamic workflows](references/dynamic-workflows.md) — bounded fan-out, failure semantics, limits, budget, events, and convergence loops.
109+
- [Claude workflow conversion](references/claude-workflow-conversion.md) — mapping Claude Code dynamic-workflow scripts onto rig primitives.
107110
- [Running and engines](references/runtime.md) — markdown/file launch modes, typechecking, Agentic Workflows, and SDK adapters.
108111
- [Linting](references/linting.md) — linter usage, autofixes, rules, and rule development.
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.

skills/rig/references/dynamic-workflows.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ const results = await runWorkflow(audit, {
3737
});
3838
```
3939

40-
`meta` describes the workflow for tools and progress displays. `input` preserves
41-
Rig schema inference in both `body` and `runWorkflow({ args })`.
40+
`meta` describes the workflow for tools and progress displays: `name`,
41+
`description`, optional `phases` (each `"Title"` or `{ title, detail }`), and
42+
optional `whenToUse`. `input` preserves Rig schema inference in both `body` and
43+
`runWorkflow({ args })`.
4244

4345
## Workflow as default export
4446

@@ -82,15 +84,33 @@ export default linter;
8284
| `input` | Typed workflow arguments |
8385
| `call(worker, input, options?)` | Runs a typed agent; returns its output or `null` on agent failure |
8486
| `call.text(prompt, options?)` | Runs a one-off string-output agent |
85-
| `pipeline(items, fn)` | Starts every item immediately; agent calls flow through the shared limiter |
87+
| `call.json(prompt, schema, options?)` | Runs a one-off agent constrained to `schema`; returns typed output or `null` |
88+
| `call.workflow(child, args?, options?)` | Runs another workflow inline on the same limiter, budget, and event stream |
89+
| `pipeline(items, ...stages)` | Streams each item through every stage independently; agent calls flow through the shared limiter |
8690
| `parallel(thunks)` | Runs all thunks as a barrier and preserves their order |
8791
| `until(options, step)` | Runs a bounded convergence loop |
8892
| `phase(name)` | Sets the phase attached to subsequent events |
8993
| `log(message)` | Emits a structured log event |
94+
| `budget` | Agent-call meter: `total`, `spent()`, `remaining()` |
9095
| `signal` | Run cancellation signal for non-agent work |
9196

92-
Call options support `label` plus the normal per-call `model`, `timeout`,
93-
`maxTurns`, and `signal` overrides.
97+
Call options support `label` and `phase` (a per-call phase override) plus the
98+
normal per-call `model`, `timeout`, `maxTurns`, and `signal` overrides.
99+
100+
Each `pipeline` stage receives `(previous, item, index)`. The first stage's
101+
`previous` is the item itself, so a single-stage pipeline is just
102+
`pipeline(items, (item) => ...)`. Stages run per item with no barrier between
103+
them, so one item can be in stage 3 while another is still in stage 1.
104+
105+
`budget` is denominated in agent calls, not tokens: `budget.total` is the
106+
effective `limits.maxAgents`, `spent()` counts started calls, and `remaining()`
107+
is what is left before the run fails. Use it to scale depth:
108+
`while (budget.remaining() > 10) { ... }`.
109+
110+
`call.workflow` runs a child `workflow()` inline. It shares the parent's
111+
concurrency limiter, agent budget, cancellation signal, and `onEvent` stream, and
112+
brackets the child with `log` events. Restore a phase after the nested run if the
113+
child called `phase()`.
94114

95115
`parallel` turns rejected thunks into `null` holes. Agent failures passed through
96116
`pipeline` are already `null` because `call` handles them; other pipeline callback
@@ -146,3 +166,10 @@ const final = await until(
146166

147167
The loop stops when `done` is true, after `max` rounds, or after
148168
`noProgressRounds` consecutive equal defined progress keys.
169+
170+
## Porting from Claude dynamic workflows
171+
172+
The rig primitives mirror the Claude Code dynamic-workflow globals (`meta`,
173+
`args`, `agent`, `parallel`, `pipeline`, `phase`, `log`, `budget`, nested
174+
`workflow`). See [Converting Claude dynamic workflows to rig](claude-workflow-conversion.md)
175+
for the full mapping, schema translation table, and behavior differences.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# 310 - Audit and Verify (Dynamic Workflow Port)
2+
3+
Mirrors a Claude Code dynamic workflow: fan out finders, then stream each finding
4+
through a verifier stage. See
5+
[claude-workflow-conversion.md](../references/claude-workflow-conversion.md).
6+
7+
```rig
8+
import { s, workflow } from "rig";
9+
10+
const finding = s.object({ title: s.string, file: s.path });
11+
12+
// Workflow role: audit source areas in parallel, then verify each finding.
13+
const audit = workflow({
14+
meta: {
15+
name: "audit",
16+
description: "Find and verify repository issues",
17+
phases: [{ title: "Find" }, { title: "Verify", detail: "one verifier per finding" }],
18+
whenToUse: "Auditing several areas that each need independent verification.",
19+
},
20+
body: async ({ call, parallel, phase, pipeline }) => {
21+
phase("Find");
22+
const areas = ["skills", "src", "scripts"];
23+
const found = await parallel(areas.map((area) => () =>
24+
call.json(`Audit ${area}/ for risky patterns.`, s.object({ findings: s.array(finding) }), { label: area })));
25+
26+
phase("Verify");
27+
const verdicts = await pipeline(
28+
found.flatMap((result) => result?.findings ?? []),
29+
(item: { title: string; file: string }) =>
30+
call.json(`Verify "${item.title}" in ${item.file}.`, s.object({ real: s.boolean }), { phase: "Verify" }),
31+
);
32+
return verdicts.filter((verdict) => verdict?.real).length;
33+
},
34+
});
35+
36+
export default audit;
37+
```

0 commit comments

Comments
 (0)