{"record":{"id":"9d0707bfbccda803","repo":"coleam00/Archon","slug":"node-context-consumerid-field-context-fiel-9d0707","errorCode":null,"errorMessage":"Node '${context.consumerId}' field '${context.field}' cannot resolve '${ref}': ${error.message} (from OutputRefError)","messagePattern":"Node '(.+?)' field '(.+?)' cannot resolve '(.+?)': (.+?) \\(from OutputRefError\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/workflows/src/dag-executor.ts","lineNumber":1495,"sourceCode":"            'failed branch.'\n        );\n        return escapedForBash\n          ? shellQuoteOrFile(nodeOutput.output, nodeId, undefined, artifactsDir)\n          : nodeOutput.output;\n      }\n      // No-silent-drop field access (resolveNodeOutputField): prefers the parsed\n      // structuredOutput payload, falls back to parsing `output`, and THROWS an\n      // OutputRefError for an unresolvable reference (field not in the producer's\n      // declared schema, or a schemaless node whose output isn't JSON / lacks the\n      // key). The throw propagates to the dag-executor's per-node catch → the\n      // consuming node fails visibly instead of receiving a poisoned ''. The only\n      // value that resolves to empty is an author-declared-optional field.\n      let resolution: ReturnType<typeof resolveNodeOutputField>;\n      try {\n        resolution = resolveNodeOutputField(nodeOutput, nodeId, field);\n      } catch (error) {\n        if (requiredContext && error instanceof OutputRefError) {\n          throw requiredOutputRefError(requiredContext, match, error.message);\n        }\n        throw error;\n      }\n      if (resolution.kind === 'empty') return escapedForBash ? \"''\" : '';\n      const value = resolution.value;\n      // numbers and booleans are shell-safe without quoting: JSON disallows\n      // NaN/Infinity so String(number) is digits/sign/'.', and String(boolean) is\n      // 'true'/'false' — no shell metacharacters.\n      if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n      // Everything else takes the one value→text rule (strings raw; arrays/objects/\n      // null as canonical JSON so downstream tools like jq get one JSON literal),\n      // with the bash-escaping decision staying here at the call site.\n      const text = canonicalValueText(value);\n      return escapedForBash ? shellQuoteOrFile(text, nodeId, field, artifactsDir) : text;\n    }\n  );\n}\n","sourceCodeStart":1477,"sourceCodeEnd":1513,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/dag-executor.ts#L1477-L1513","documentation":"This error is thrown by `substituteNodeOutputRefs` in the workflow DAG executor when a consumer node's text references a producer node's output field via `$node_id.output.field`, but the field cannot be resolved. With `requiredContext` set (the `until_bash` completion-decision path), the executor converts the underlying `OutputRefError` into this wrapped error naming the consuming node, the field, the raw ref, and the cause — because an empty substitution would silently poison a loop-termination decision. It enforces the engine's no-silent-drop posture: unresolvable output references fail the consuming node loudly instead of splicing in an empty string.","triggerScenarios":"A node's prompt/script contains `$producer.output.field` where: (1) the producer node id is unknown (typo, or node hasn't produced output on this execution path); (2) the field is not in the producer's declared structuredOutput schema; (3) the producer is schemaless and its output is not parseable JSON or lacks the key; (4) the ref appears in an `until_bash` condition (requiredContext) where field refs are always strict, even for optional-looking paths.","commonSituations":"Typoed node id in an until_bash loop condition (near-miss hints like 'Did you mean: ...' appear in the underlying detail); referencing a field the producer's structuredOutput schema doesn't declare; expecting free-text AI output to be JSON with a field when the model returned prose; renaming a node id or field in the YAML without updating dependent until/loop conditions.","solutions":["Check the referenced node id in the error against your workflow YAML; fix the typo or ensure the node runs (and completes) before the consuming node/loop condition.","Verify the field name exists in the producer node's declared structuredOutput schema; add it to the schema or read an actual declared field.","If the producer is a prompt/AI node without a schema, add a structuredOutput schema so `$id.output.field` has a defined source, or switch the condition to parse the whole output deliberately in the bash body.","For until_bash conditions, guard the check: test whether the output parses/contains the field in bash before dereferencing, or ensure the producer is guarded by a `when:` condition so the ref is only evaluated on paths where it ran.","If the empty-value fallback is genuinely intended, move the ref out of the requiredContext surface (until_bash) into a prompt or script where `$id.output.field` strictness is not required."],"exampleFix":"// before (until_bash condition referencing undeclared field)\nuntil_bash: |\n  [ \"$reviewer.output.approved\" = \"true\" ]\n// after (producer declares the field in structuredOutput)\noutput_schema:\n  type: object\n  properties:\n    approved: { type: boolean }\n  required: [approved]\nuntil_bash: |\n  [ \"$reviewer.output.approved\" = \"true\" ]","handlingStrategy":"validation","validationCode":"// Before declaring the until/loop condition, verify every $id.output.field ref\n// targets a node id present in the workflow and a field declared in its schema:\nfunction validateOutputRefs(\n  body: string,\n  nodes: Map<string, { fields?: readonly string[] }>\n): string[] {\n  const errors: string[] = [];\n  const re = /\\$([a-zA-Z_][a-zA-Z0-9_-]*)\\.output(?:\\.([a-zA-Z_][a-zA-Z0-9_]*))?/g;\n  for (const m of body.matchAll(re)) {\n    const [, nodeId, field] = m;\n    const node = nodes.get(nodeId);\n    if (!node) errors.push(`unknown node '${nodeId}'`);\n    else if (field && node.fields && !node.fields.includes(field))\n      errors.push(`field '${field}' not declared on node '${nodeId}'`);\n  }\n  return errors;\n}","typeGuard":"function isResolvableOutputRef(\n  nodeId: string,\n  field: string | undefined,\n  nodeOutputs: Map<string, NodeOutput>\n): boolean {\n  const out = nodeOutputs.get(nodeId);\n  if (!out) return false;\n  if (!field) return out.state === 'succeeded';\n  try { resolveNodeOutputField(out, nodeId, field); return true; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  substituteNodeOutputRefs(prompt, nodeOutputs, true, artifactsDir, ctx);\n} catch (err) {\n  if (err instanceof Error && /cannot resolve '/.test(err.message)) {\n    // Surface which consumer/field/ref failed; fix YAML id or schema — do not retry.\n    log.error({ message: err.message }, 'output_ref_unresolvable');\n  }\n  throw err;\n}","preventionTips":["Declare a structuredOutput schema on every producer node whose fields are read via $id.output.field.","Copy node ids exactly; run workflow validation/lint before executing to catch typo'd refs early.","In until_bash conditions, only dereference fields on nodes guaranteed to have run and succeeded on that path.","When a producer can legitimately skip or fail, guard consumers with `when:` conditions excluding the failed branch.","After renaming a node id or schema field, grep all workflow YAML for the old identifier."],"tags":["workflow","yaml","template-substitution","dag-execution"],"backgroundTag":"unresolved-output-reference","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}