{"record":{"id":"8183f5af4db31f70","repo":"coleam00/Archon","slug":"workflow-compose-fan-out-suspension-rejected","errorCode":"workflow.compose_fan_out_suspension_rejected","errorMessage":"composed fan-out node '${node.id}': the composed block '${node.include}' contains suspension-capable path${bodySuspensions.length === 1 ? '' : 's'} ${names}. An in-parent fan-out has no per-instance pause cursor, so it cannot safely resume a partly completed body. Remove the suspension path, or invoke the block once through 'include:' or 'workflow:'.","messagePattern":"composed fan-out node '(.+?)': the composed block '(.+?)' contains suspension-capable path(.+?) (.+?)\\. An in-parent fan-out has no per-instance pause cursor, so it cannot safely resume a partly completed body\\. Remove the suspension path, or invoke the block once through 'include:' or 'workflow:'\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/workflows/src/dag-executor.ts","lineNumber":8996,"sourceCode":"    node.include,\n    ctx.workflowSourceRoots\n  );\n  if ('unresolved' in resolved) {\n    const msg =\n      `composed fan-out node '${node.id}': cannot resolve the composed block '${node.include}' — ` +\n      `${resolved.unresolved}. Fix the include target name.`;\n    await notify(`❌ **Composed fan-out blocked** (node \\`${node.id}\\`): ${msg}`);\n    return failResult(msg);\n  }\n  const bodySuspensions = collectComposedSuspensionPaths(resolved.definition, resolved.definitions);\n  if (bodySuspensions.length > 0) {\n    const names = bodySuspensions.map(entry => `'${entry.id}' (${entry.reason})`).join(', ');\n    const msg =\n      `composed fan-out node '${node.id}': the composed block '${node.include}' contains ` +\n      `suspension-capable path${bodySuspensions.length === 1 ? '' : 's'} ${names}. An in-parent ` +\n      'fan-out has no per-instance pause cursor, so it cannot safely resume a partly completed ' +\n      \"body. Remove the suspension path, or invoke the block once through 'include:' or 'workflow:'.\";\n    getLog().warn(\n      { parentRunId: parentRun.id, nodeId: node.id, include: node.include },\n      'workflow.compose_fan_out_suspension_rejected'\n    );\n    await notify(`❌ **Composed fan-out blocked** (node \\`${node.id}\\`): ${msg}`);\n    return failResult(msg);\n  }\n\n  let computedSnapshots: ReturnType<typeof buildInstanceSnapshots>;\n  if (persistedSnapshots === undefined) {\n    // Freeze every resolved binding together with the item before any instance starts.\n    // A resumed run must never combine completed work from the old bindings with a\n    // retried instance resolved from changed upstream output.\n    let staticInputs: Record<string, JsonValue> = {};\n    const parentInputs = resolveRunInputs(parentRun);\n    try {\n      if (node.with !== undefined) {\n        const resolutionCtx: ShellInputContext = {\n          workflowRun: parentRun,","sourceCodeStart":8978,"sourceCodeEnd":9014,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/dag-executor.ts#L8978-L9014","documentation":"A composed fan-out node (`include:` block expanded per item in the parent run) is refused because the composed body contains a suspension-capable path (e.g. an approval gate). In-parent fan-out instances share the parent's execution and have no per-instance pause cursor, so a partly completed body could not be resumed safely.","triggerScenarios":"Executing a composed fan-out node whose `node.include` block resolves to a body containing suspension-capable nodes; detected before instances run.","commonSituations":"Reusing a block that includes a human-approval step inside a per-item fan-out; a block gained a suspension path after the fan-out was authored.","solutions":["Remove the suspension-capable path from the composed block","Invoke the block once (not per-instance) via plain `include:` or `workflow:` so the engine's pause cursor applies","Split the block: non-suspending part fans out, suspending part runs once"],"exampleFix":"# before\n- id: loop\n  include: gated-batch   # contains an approval node\n  fan_out: { over: items }\n# after\n- id: loop\n  include: plain-batch   # suspension path removed\n  fan_out: { over: items }","handlingStrategy":"validation","validationCode":"// Ensure the composed block has no suspension-capable nodes before fan-out\nconst body = await composer.expand(node.include);\nconst suspensions = body.filter(n => n.type === 'approval' || n.canSuspend);\nif (suspensions.length > 0) throw new Error(`${node.include} cannot fan out: contains ${suspensions.map(s => s.id).join(', ')}`);","typeGuard":"function isFanOutSafeBlock(body: { id: string; canSuspend?: boolean }[]): boolean {\n  return !body.some(n => n.canSuspend === true);\n}","tryCatchPattern":null,"preventionTips":["Keep approval/human-gate nodes out of blocks intended for fan-out reuse","Review blocks that gain suspension paths for existing fan-out consumers","Run suspension-capable blocks once via include:/workflow: instead of per-instance"],"tags":["workflow","fan-out","composition","suspension"],"backgroundTag":"compose-fan-out-suspension-rejected","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}