coleam00/Archon · error · IncludeExpansionError

structureError (DAG structure validation message, wrapped in

Error message

structureError (DAG structure validation message, wrapped in IncludeExpansionError)

What it means

After inlining all includes, the expander validates the resulting DAG with `validateDagStructure` and wraps any structural problem in an `IncludeExpansionError`. This catches graphs that only exist after expansion — e.g. cycles or broken edges introduced by composing multiple workflows — rather than problems in any single source file.

Source

Thrown at packages/workflows/src/include-expander.ts:1275

    // Re-validate the fully-flattened DAG. Catches a namespaced id colliding with a
    // hand-written node, cycles introduced by edge rewiring, unknown deps, and the
    // equivalent failures inside every recursively expanded loop_group body.
    //
    // Deliberately NOT re-running the workflow-class placement check here (#2707 step
    // 2): a reusable block can legitimately author a native gate without declaring its
    // own `interactive: true` — it is only ever a load error for the workflow ACTUALLY
    // being loaded standalone (`parseWorkflow`'s own single-file check already covers
    // that), not for every name `expandWorkflowIncludes` happens to also process as a
    // `rawByName` entry. A composed gate's drivability stays exactly what it was before
    // this PR — an invocation-time question `assertComposedGateDriveable` answers
    // against the workflow actually being dispatched, because load time cannot tell
    // which discovered workflow will own a given run (see that function's doc comment;
    // `expandWorkflowIncludes — composed approval gates are stamped, not rejected
    // (#1764)` pins this down with a "non-interactive INTERMEDIATE block still expands"
    // case).
    const structureError = validateDagStructure(expanded.nodes);
    if (structureError) {
      throw new IncludeExpansionError(structureError);
    }

    const dedupedRequires = [...new Set(requires)];
    const result: WorkflowDefinition = {
      ...collapsed,
      nodes: expanded.nodes,
      // `returns:` may name an include directive that no longer exists after flattening.
      // Rebind it to the same primary sink used for `$includeId.output`; ordinary node ids
      // pass through unchanged. Without this, a nested reusable workflow can finish with a
      // dangling return id even though every node-level reference was rewritten correctly.
      ...(collapsed.returns !== undefined
        ? { returns: expanded.renameIncludeRef(collapsed.returns) }
        : {}),
      ...(dedupedRequires.length > 0 ? { requires: dedupedRequires } : {}),
    };
    const outcomeDeclarationError = validateWorkflowOutcomeDeclaration(result);
    if (outcomeDeclarationError !== null) {
      throw new IncludeExpansionError(outcomeDeclarationError);

View on GitHub (pinned to 0773b97458)

Solutions

  1. Read the wrapped structureError message — it names the specific structural defect and node ids
  2. Break any cycle: if A includes B and B includes A, extract the shared node(s) into a third workflow both can include
  3. Fix `depends_on` references to match node ids as they exist after expansion (check the included workflow's ids)
  4. Rename conflicting node ids in one of the included workflows

Example fix

# before: cycle across include boundary
# a.yaml includes b.yaml; b.yaml includes a.yaml
# after
# a.yaml includes shared.yaml; b.yaml includes shared.yaml
Defensive patterns

Strategy: validation

Validate before calling

// Detect include cycles before expansion.
function detectIncludeCycles(raws: Map<string, { includes?: string[] }>): string[] | null {
  const visiting = new Set<string>(), done = new Set<string>();
  let cycle: string[] | null = null;
  const visit = (n: string, stack: string[]) => {
    if (cycle) return;
    if (visiting.has(n)) { cycle = [...stack, n]; return; }
    if (done.has(n)) return;
    visiting.add(n);
    for (const inc of raws.get(n)?.includes ?? []) visit(inc, [...stack, n]);
    visiting.delete(n); done.add(n);
  };
  for (const name of raws.keys()) visit(name, []);
  return cycle;
}

Try / catch

try {
  const expanded = expandWorkflowIncludes(rawByName);
} catch (err) {
  if (err instanceof IncludeExpansionError) {
    // structureError is wrapped verbatim; surface it with the workflow name under expansion.
    throw new Error(`Include expansion produced an invalid DAG: ${err.message}`, { cause: err });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `expandWorkflowIncludes` (or `expandWorkflowIncludesWithDiscovery`) where the fully inlined node set produces an invalid DAG: cycles across include boundaries, duplicate node ids, or edges referencing nodes that no longer exist after inlining.

Common situations: Two included workflows that reference each other (cycle through includes), a parent wiring `depends_on:` a node id that the included workflow renamed, include composition creating duplicate node ids.

Related errors


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