coleam00/Archon · error

❌ **Composed fan-out failed** (node `${node.id}`): ${current

Error message

❌ **Composed fan-out failed** (node `${node.id}`): ${currentItems.error}

What it means

In the composed fan-out path (packages/workflows/src/dag-executor.ts:8962), resolveCurrentItems must resolve the node's fan_out.items into a JSON array. If resolution fails and no persisted snapshots exist (fresh execution), the executor notifies '❌ Composed fan-out failed' with the resolution error and fails the node; when snapshots exist it warns and falls back to them instead.

Source

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

      const itemsResolved = substituteNodeOutputRefs(itemsVarsResolved, ctx.nodeOutputs);
      const parsed: unknown = JSON.parse(itemsResolved);
      if (!Array.isArray(parsed)) {
        return {
          error:
            `fan_out.items on '${node.id}' resolved to ${typeof parsed}, not a JSON array. ` +
            `'${fanOut.items}' must reference a node output that produces a JSON array.`,
        };
      }
      return { items: parsed };
    } catch (err) {
      return {
        error: `fan_out.items on '${node.id}' could not be resolved to a JSON array: ${(err as Error).message}`,
      };
    }
  };

  const currentItems = resolveCurrentItems();
  if ('error' in currentItems && persistedSnapshots === undefined) {
    await notify(`❌ **Composed fan-out failed** (node \`${node.id}\`): ${currentItems.error}`);
    return failResult(currentItems.error);
  }
  if ('error' in currentItems) {
    getLog().warn(
      { parentRunId: parentRun.id, nodeId: node.id, error: currentItems.error },
      'workflow.compose_fan_out_item_drift_unreadable'
    );
  }

  // Preflight BEFORE any instance spend: the target must still resolve (it may have been
  // renamed or deleted since load), and its complete closure must remain suspension-free.
  const resolved = await resolveFanOutChildDefinition(
    deps,
    cwd,
    node.include,
    ctx.workflowSourceRoots
  );

View on GitHub (pinned to 0773b97458)

Solutions

  1. Read currentItems.error — it names the node and why items could not be resolved to a JSON array
  2. Fix the upstream node so fan_out.items resolves to a JSON array (use structured output, not prose)
  3. Correct the fan_out.items expression to reference the right output field
  4. Re-run with persisted snapshots if a prior successful snapshot exists, so the warn-and-fallback path applies

Example fix

# workflow yaml
# before
fan_out:
  items: "${steps.parse.output}"        # parse node returned an object
# after
fan_out:
  items: "${steps.parse.output.records}"  # reference the array field
Defensive patterns

Strategy: type-guard

Validate before calling

// Verify the fan-out source is an array before the node runs:
const items = JSON.parse(upstreamOutput);
if (!Array.isArray(items)) throw new Error('fan_out.items source must be a JSON array, got ' + typeof items);

Type guard

function isJsonArray(v: unknown): v is unknown[] {
  return Array.isArray(v);
}
// use: if (isJsonArray(resolvedItems)) { /* safe to fan out */ }

Try / catch

try {
  await runFanOut(node);
} catch (e) {
  const m = String(e.message ?? e).match(/Composed fan-out failed.*: (.+)/s);
  if (m) console.error('fix fan_out.items resolution:', m[1]);
  else throw e;
}

Prevention

When it happens

Trigger: The fan_out.items expression on a node resolves to something that cannot be parsed as a JSON array — the expression yields a non-array value, an object, undefined, or malformed JSON — during a composed (e.g. resumed/child) fan-out execution without persisted item snapshots.

Common situations: An upstream node outputting an object where an array was expected; a fan_out.items expression with a typo so it resolves to undefined; JSON output from a prompt node wrapped in prose so parsing fails; rerunning a composed node whose parent's item shape changed.

Related errors


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