coleam00/Archon · error

Node '${consumerId}' binding '${name}' reads '${directive.fr

Error message

Node '${consumerId}' binding '${name}' reads '${directive.from}', but node '${ref.nodeId}' did not run (skipped or pending), so it has no output to read. Declare 'if_skipped:' on the binding to supply a default for that branch, or guard '${consumerId}' with a 'when:' condition.

What it means

A binding directive reads one upstream node's output, but the producer is absent, skipped, or pending (typical under trigger_rule: all_done across a skipped branch). If the directive declares if_skipped (presence-keyed, so null/false/0/'' count as real defaults), that value is used; otherwise the node fails explaining both remediation options. A failed producer is not covered here — it routes through assertProducerNotFailed and never falls back to if_skipped.

Source

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

function resolveBindingDirective(
  consumerId: string,
  name: string,
  directive: BindingDirective,
  ctx: ShellInputContext
): JsonValue {
  const ref = parseWholeOutputRef(directive.from);
  if (ref === undefined) {
    throw new Error(
      `Node '${consumerId}' binding '${name}': 'from' must be exactly one whole ` +
        `'$node.output' or '$node.output.field' reference, got '${directive.from}'.`
    );
  }
  const producer = ctx.nodeOutputs.get(ref.nodeId);
  if (producer === undefined || producer.state === 'skipped' || producer.state === 'pending') {
    // Presence-keyed: `if_skipped: null` (or false/0/'') is a real declared default.
    if (Object.hasOwn(directive, 'if_skipped')) return directive.if_skipped as JsonValue;
    throw new Error(
      `Node '${consumerId}' binding '${name}' reads '${directive.from}', but node ` +
        `'${ref.nodeId}' did not run (skipped or pending), so it has no output to read. ` +
        "Declare 'if_skipped:' on the binding to supply a default for that branch, or " +
        `guard '${consumerId}' with a 'when:' condition.`
    );
  }
  // A failed producer never falls back to `if_skipped` (#2696): that default exists for
  // a branch that legitimately didn't run, not for a run that ran and produced a result
  // that can't be trusted (a loop_group's failure paths carry the last completed
  // iteration's real output text — non-empty, often valid JSON — which would otherwise
  // resolve here as if the group had succeeded). Routes through `assertProducerNotFailed`
  // in output-ref.ts (#2722, extending #2710's original guard to every whole-text reader
  // of nodeOutputs through one shared function).
  assertProducerNotFailed(
    producer,
    failed =>
      `Node '${consumerId}' binding '${name}' reads '${directive.from}', but node ` +
      `'${ref.nodeId}' failed (${failed.error}), so its output cannot be trusted. ` +

View on GitHub (pinned to 0773b97458)

Solutions

  1. Add if_skipped: to the binding with a default (including if_skipped: null if that is the intended value).
  2. Add a when: condition on the consumer so it also skips when the producer skipped.
  3. Restructure dependencies so the producer always runs before the consumer (adjust trigger_rule or depends_on).
  4. If the producer actually failed, fix the producer — if_skipped does not apply to failures by design.

Example fix

// before
with:
  coverage:
    from: '$unit_tests.output.coverage'   # unit_tests may be skipped
// after
with:
  coverage:
    from: '$unit_tests.output.coverage'
    if_skipped: null
Defensive patterns

Strategy: fallback

Validate before calling

function needsIfSkipped(directive: { from: string; if_skipped?: unknown }, skippable: Set<string>): string | null {
  const producer = directive.from.replace(/^\$/, '').replace(/\.output.*$/, '');
  return skippable.has(producer) && !('if_skipped' in directive)
    ? `Producer '${producer}' can be skipped; add if_skipped to binding reading '${directive.from}'`
    : null;
}

Try / catch

try {
  runWorkflow(wf);
} catch (e) {
  if (e instanceof Error && e.message.includes('did not run (skipped or pending)')) {
    console.error('Upstream skipped and no if_skipped default declared:', e.message);
  } else throw e;
}

Prevention

When it happens

Trigger: resolveBindingDirective with ctx.nodeOutputs lacking the producer or holding it with state 'skipped' or 'pending', and no if_skipped key on the directive — e.g. consumer joined on all_done while the producer's branch was skipped by its when: condition.

Common situations: Conditional deploy/test branches where one side skips but the join node always binds its output; fan-out or retry flows leaving a producer pending; forgetting if_skipped when tolerating skipped upstreams; misreading a failed node as skipped (failures do not take the default).

Related errors


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