coleam00/Archon · error
workflow.fan_out_shared_checkout_collision
workflow.fan_out_shared_checkout_collision
Error message
fan_out node '${node.id}': up to ${String(concurrency)} children of '${node.workflow}' would run at once in the parent checkout, and that workflow does not declare `mutates_checkout: false`. Concurrent runs on one checkout take a path-exclusive lock, so all but the first would cancel themselves — and a lock-cancelled child is not recoverable by resume (#2180). Choose one: add `mutates_checkout: false` to '${node.workflow}' if it only reads the repo; set `isolation: worktree` on '${node.id}' if the children write to it; or set `fan_out.max_parallel: 1` to run them one at a time. What it means
Refused at planning time: a fan-out would run more than one child of a workflow that can mutate the shared parent checkout. Concurrent runs on one checkout take a path-exclusive lock, so all but the first child would self-cancel, and lock-cancelled children are not recoverable by resume (#2180). The executor blocks the node before creating children.
Source
Thrown at packages/workflows/src/dag-executor.ts:8504
`fan_out node '${node.id}': target workflow '${node.workflow}' is interactive-class ` +
"('interactive: true') and may pause for human input — a fan-out has a single " +
'approval-gate slot for N children, so this is refused before any child is created. ' +
`Remove the pause capability from '${node.workflow}', or invoke it as a single ` +
"(non-fan-out) 'workflow:' node instead.";
getLog().warn(
{ parentRunId: parentRun.id, nodeId: node.id, childWorkflow: node.workflow },
'workflow.fan_out_interactive_target'
);
await notify(`❌ **Fan-out blocked** (node \`${node.id}\`): ${msg}`);
return failResult(msg);
}
if (
node.isolation !== 'worktree' &&
plannedConcurrency > 1 &&
resolved.definition.mutates_checkout !== false
) {
const msg = fanOutSharedCheckoutMessage(node, plannedConcurrency);
getLog().warn(
{
parentRunId: parentRun.id,
nodeId: node.id,
childWorkflow: node.workflow,
plannedConcurrency,
},
'workflow.fan_out_shared_checkout_collision'
);
await notify(`❌ **Fan-out blocked** (node \`${node.id}\`): ${msg}`);
return failResult(msg);
}
}
// 6. Execute EVERY index through a bounded sliding window. Classification per index: an
// existing completed child threads its recorded outcome (resume skip); an existing
// failed OR fan-out-cancelled (recoverable) child is re-driven; a user-cancelled child
// stays terminal; a missing index spawns fresh.
//View on GitHub (pinned to 0773b97458)
Solutions
- Add `mutates_checkout: false` to the target workflow if it only reads the repo
- Set `isolation: worktree` on the fan-out node so each child gets its own checkout
- Set `fan_out.max_parallel: 1` to run children sequentially under the lock
Example fix
# before
- id: batch
workflow: repo-scan
fan_out: { over: repos }
# after (read-only target)
workflow: repo-scan # frontmatter now: mutates_checkout: false
fan_out: { over: repos } Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: fan-out into a shared checkout is only safe if read-only or serialized
const def = await workflows.resolve(node.workflow);
const parallel = node.fan_out?.max_parallel ?? Infinity;
if (node.isolation !== 'worktree' && parallel > 1 && def.frontmatter.mutates_checkout !== false) {
throw new Error(`${node.workflow} mutates the checkout; use isolation: worktree, max_parallel: 1, or mutates_checkout: false`);
} Type guard
function isCheckoutSafeFanOut(node: { isolation?: string; fan_out?: { max_parallel?: number } }, def: { frontmatter: { mutates_checkout?: boolean } }): boolean {
return node.isolation === 'worktree' || (node.fan_out?.max_parallel ?? 2) <= 1 || def.frontmatter.mutates_checkout === false;
} Prevention
- Declare `mutates_checkout: false` on read-only workflows so they are fan-out friendly
- Default write-heavy fan-outs to `isolation: worktree`
- Set `fan_out.max_parallel: 1` whenever children share a checkout
When it happens
Trigger: Executing a `fan_out` node where `node.isolation !== 'worktree'`, plannedConcurrency > 1, and the target workflow definition does not set `mutates_checkout: false`.
Common situations: Fanning out a workflow that runs `git commit`/build steps; forgetting `mutates_checkout: false` on a read-only workflow; forgetting `isolation: worktree` on write-heavy fan-outs.
Related errors
- workflow.compose_fan_out_shared_checkout_collision
- Failed to claim write-back apply: ${err.message}
- Workflow run ${runId} was already resolved and is awaiting r
- ❌ **Composed fan-out failed** (node `${node.id}`): ${current
- workflow.fan_out_interactive_target
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/b2dfe8891af97452.
Report an issue: GitHub.