Skip to main content
These five rules keep biting workflow-authoring agents at runtime, hours after a workflow was written, instead of in the first second of bunx smithers-orchestrator graph. Each is already documented elsewhere in full; this page exists so a builder (human or agent) hits all five in one read before writing JSX, not one at a time across several failed runs. See the postmortem this page was written to close.

1. Output schemas cannot reuse the reserved key columns

Every output table gets a fixed run_id / node_id / iteration key prefix; an input table gets run_id only. Why: those columns are how Smithers correlates a row back to the run, node, and loop iteration that produced it, so a field named runId, nodeId, or iteration in your own schema would collide with the reserved one.
A collision throws INVALID_INPUT at construction (schema build time), not at run time; see zodToTable.

2. No nested loops, use the queue-based backfill pattern instead

A <Loop>/<Ralph> as the literal immediate JSX child of another <Loop>/<Ralph> throws NESTED_LOOP at graph-extraction time (before any agent runs). Why: with nothing between the two loops, there’s no clear “whose iteration is this” semantics for the inner one, the same gap the Effect combinator builder’s G.loop rejects unconditionally.
The fix the error message suggests: run the inner work through a queue such as <MergeQueue> and re-enter via the outer loop’s next iteration, instead of nesting loops. This is narrower than it sounds. A <Loop> reached through a <Sequence>/<Parallel>/<Worktree> wrapper (not the literal immediate child) is a different, genuinely-supported shape: each <Parallel>-forked lane (one per array item, one per isolated <Worktree>) may run its own bounded correction loop, scoped to the outer loop’s iteration. That’s exactly what close-issues.tsx and studio-parity-swarm.tsx do (outer discover step, then a <Parallel> of per-item <Worktree> lanes, each with its own correction <Loop>), and it’s regression-guarded by nested-loop-runtime.test.jsx (issue #117): the inner loop’s state and cache correctly reset per outer iteration. Don’t avoid that shape; it’s the sanctioned pattern for “per-item lanes, each with a correction loop.” What to avoid is a second loop governing the same lane with nothing forking between them.

3. ctx.latest vs outputMaybe({ nodeId, iteration }) for loop bindings

Inside a <Loop>’s until, read the most recent iteration with ctx.latest:
Why ctx.latest and not bare ctx.outputMaybe: ctx.outputMaybe(schema, { nodeId }) with no explicit iteration resolves the current render iteration, which equals the loop’s iteration only for a single, non-nested loop, and is 0 when several loops coexist (sibling loops, or a loop nested under a <Parallel>/<Worktree> fork per rule 2). An outputMaybe-based until built on that ambient iteration can silently never advance: the loop just spins to maxIterations and returns the last result, with no error telling you why. If you need outputMaybe directly (not ctx.latest), pass the loop’s own scoped node id and iteration explicitly: ctx.outputMaybe(schema, { nodeId: "review", iteration: N }).

4. Workflow tests must render the real graph via renderWorkflow, not a hand-built one

A workflow test imports the actual workflow module and drives it through renderWorkflow (smithers-orchestrator/testing):
Why: a test that hand-builds its own plan/graph object (bypassing extractGraph/buildPlanTree) can pass while the real workflow file has a typo, a NESTED_LOOP, or a reserved-column collision; it’s testing a fiction that happens to resemble the workflow, not the workflow. renderWorkflow exercises the same extraction path bunx smithers-orchestrator graph/bunx smithers-orchestrator up do, so a graph-level defect fails the test the same way it would fail a real run.

5. New .smithers test files must be registered in .smithers/package.json

.smithers/package.json’s test script is an explicit, space-separated list of test file paths, not a glob. Why: pack tests run outside the normal per-package bun test tests convention (they share fixtures/agents across many workflow files), so there’s no directory-wide default to fall back on. A new test file that isn’t appended to that list is silently never run by pnpm test or CI; it can sit green-looking in the repo indefinitely while contributing zero coverage. node scripts/check-smithers-test-script.mjs (part of the root pnpm test gate) catches an unregistered test file; run it after adding one, or just add the new path to the list yourself.

See also