{"record":{"id":"d123d5bc747f22ef","repo":"coleam00/Archon","slug":"node-consumerid-binding-name-reads-di","errorCode":null,"errorMessage":"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.","messagePattern":"Node '(.+?)' binding '(.+?)' reads '(.+?)', but node '(.+?)' 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 '(.+?)' with a 'when:' condition\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/workflows/src/dag-executor.ts","lineNumber":484,"sourceCode":"\nfunction resolveBindingDirective(\n  consumerId: string,\n  name: string,\n  directive: BindingDirective,\n  ctx: ShellInputContext\n): JsonValue {\n  const ref = parseWholeOutputRef(directive.from);\n  if (ref === undefined) {\n    throw new Error(\n      `Node '${consumerId}' binding '${name}': 'from' must be exactly one whole ` +\n        `'$node.output' or '$node.output.field' reference, got '${directive.from}'.`\n    );\n  }\n  const producer = ctx.nodeOutputs.get(ref.nodeId);\n  if (producer === undefined || producer.state === 'skipped' || producer.state === 'pending') {\n    // Presence-keyed: `if_skipped: null` (or false/0/'') is a real declared default.\n    if (Object.hasOwn(directive, 'if_skipped')) return directive.if_skipped as JsonValue;\n    throw new Error(\n      `Node '${consumerId}' binding '${name}' reads '${directive.from}', but node ` +\n        `'${ref.nodeId}' did not run (skipped or pending), so it has no output to read. ` +\n        \"Declare 'if_skipped:' on the binding to supply a default for that branch, or \" +\n        `guard '${consumerId}' with a 'when:' condition.`\n    );\n  }\n  // A failed producer never falls back to `if_skipped` (#2696): that default exists for\n  // a branch that legitimately didn't run, not for a run that ran and produced a result\n  // that can't be trusted (a loop_group's failure paths carry the last completed\n  // iteration's real output text — non-empty, often valid JSON — which would otherwise\n  // resolve here as if the group had succeeded). Routes through `assertProducerNotFailed`\n  // in output-ref.ts (#2722, extending #2710's original guard to every whole-text reader\n  // of nodeOutputs through one shared function).\n  assertProducerNotFailed(\n    producer,\n    failed =>\n      `Node '${consumerId}' binding '${name}' reads '${directive.from}', but node ` +\n      `'${ref.nodeId}' failed (${failed.error}), so its output cannot be trusted. ` +","sourceCodeStart":466,"sourceCodeEnd":502,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/dag-executor.ts#L466-L502","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Add if_skipped: to the binding with a default (including if_skipped: null if that is the intended value).","Add a when: condition on the consumer so it also skips when the producer skipped.","Restructure dependencies so the producer always runs before the consumer (adjust trigger_rule or depends_on).","If the producer actually failed, fix the producer — if_skipped does not apply to failures by design."],"exampleFix":"// before\nwith:\n  coverage:\n    from: '$unit_tests.output.coverage'   # unit_tests may be skipped\n// after\nwith:\n  coverage:\n    from: '$unit_tests.output.coverage'\n    if_skipped: null","handlingStrategy":"fallback","validationCode":"function needsIfSkipped(directive: { from: string; if_skipped?: unknown }, skippable: Set<string>): string | null {\n  const producer = directive.from.replace(/^\\$/, '').replace(/\\.output.*$/, '');\n  return skippable.has(producer) && !('if_skipped' in directive)\n    ? `Producer '${producer}' can be skipped; add if_skipped to binding reading '${directive.from}'`\n    : null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  runWorkflow(wf);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('did not run (skipped or pending)')) {\n    console.error('Upstream skipped and no if_skipped default declared:', e.message);\n  } else throw e;\n}","preventionTips":["Whenever a producer has a when: condition, give every binding on its output an if_skipped default.","Remember if_skipped is presence-keyed: if_skipped: null is a valid declared default.","Remember failures do not consume if_skipped — handle producer failures separately.","Prefer when: guards on consumers over all_done joins that read skipped outputs without defaults."],"tags":["workflow","binding","skipped-node","dependency-ordering"],"backgroundTag":"producer-node-skipped","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}