coleam00/Archon · error

workflow.compose_fan_out_suspension_rejected

workflow.compose_fan_out_suspension_rejected

Error message

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:'.

What it means

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.

Source

Thrown at packages/workflows/src/dag-executor.ts:8996

    node.include,
    ctx.workflowSourceRoots
  );
  if ('unresolved' in resolved) {
    const msg =
      `composed fan-out node '${node.id}': cannot resolve the composed block '${node.include}' — ` +
      `${resolved.unresolved}. Fix the include target name.`;
    await notify(`❌ **Composed fan-out blocked** (node \`${node.id}\`): ${msg}`);
    return failResult(msg);
  }
  const bodySuspensions = collectComposedSuspensionPaths(resolved.definition, resolved.definitions);
  if (bodySuspensions.length > 0) {
    const names = bodySuspensions.map(entry => `'${entry.id}' (${entry.reason})`).join(', ');
    const msg =
      `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:'.";
    getLog().warn(
      { parentRunId: parentRun.id, nodeId: node.id, include: node.include },
      'workflow.compose_fan_out_suspension_rejected'
    );
    await notify(`❌ **Composed fan-out blocked** (node \`${node.id}\`): ${msg}`);
    return failResult(msg);
  }

  let computedSnapshots: ReturnType<typeof buildInstanceSnapshots>;
  if (persistedSnapshots === undefined) {
    // Freeze every resolved binding together with the item before any instance starts.
    // A resumed run must never combine completed work from the old bindings with a
    // retried instance resolved from changed upstream output.
    let staticInputs: Record<string, JsonValue> = {};
    const parentInputs = resolveRunInputs(parentRun);
    try {
      if (node.with !== undefined) {
        const resolutionCtx: ShellInputContext = {
          workflowRun: parentRun,

View on GitHub (pinned to 0773b97458)

Solutions

  1. Remove the suspension-capable path from the composed block
  2. Invoke the block once (not per-instance) via plain `include:` or `workflow:` so the engine's pause cursor applies
  3. Split the block: non-suspending part fans out, suspending part runs once

Example fix

# before
- id: loop
  include: gated-batch   # contains an approval node
  fan_out: { over: items }
# after
- id: loop
  include: plain-batch   # suspension path removed
  fan_out: { over: items }
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the composed block has no suspension-capable nodes before fan-out
const body = await composer.expand(node.include);
const suspensions = body.filter(n => n.type === 'approval' || n.canSuspend);
if (suspensions.length > 0) throw new Error(`${node.include} cannot fan out: contains ${suspensions.map(s => s.id).join(', ')}`);

Type guard

function isFanOutSafeBlock(body: { id: string; canSuspend?: boolean }[]): boolean {
  return !body.some(n => n.canSuspend === true);
}

Prevention

When it happens

Trigger: Executing a composed fan-out node whose `node.include` block resolves to a body containing suspension-capable nodes; detected before instances run.

Common situations: 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.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/8183f5af4db31f70. Report an issue: GitHub.