coleam00/Archon · error

Node '${context.consumerId}' field '${context.field}' cannot

Error message

Node '${context.consumerId}' field '${context.field}' cannot resolve '${ref}': ${error.message} (from OutputRefError)

What it means

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.

Source

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

            'failed branch.'
        );
        return escapedForBash
          ? shellQuoteOrFile(nodeOutput.output, nodeId, undefined, artifactsDir)
          : nodeOutput.output;
      }
      // No-silent-drop field access (resolveNodeOutputField): prefers the parsed
      // structuredOutput payload, falls back to parsing `output`, and THROWS an
      // OutputRefError for an unresolvable reference (field not in the producer's
      // declared schema, or a schemaless node whose output isn't JSON / lacks the
      // key). The throw propagates to the dag-executor's per-node catch → the
      // consuming node fails visibly instead of receiving a poisoned ''. The only
      // value that resolves to empty is an author-declared-optional field.
      let resolution: ReturnType<typeof resolveNodeOutputField>;
      try {
        resolution = resolveNodeOutputField(nodeOutput, nodeId, field);
      } catch (error) {
        if (requiredContext && error instanceof OutputRefError) {
          throw requiredOutputRefError(requiredContext, match, error.message);
        }
        throw error;
      }
      if (resolution.kind === 'empty') return escapedForBash ? "''" : '';
      const value = resolution.value;
      // numbers and booleans are shell-safe without quoting: JSON disallows
      // NaN/Infinity so String(number) is digits/sign/'.', and String(boolean) is
      // 'true'/'false' — no shell metacharacters.
      if (typeof value === 'number' || typeof value === 'boolean') return String(value);
      // Everything else takes the one value→text rule (strings raw; arrays/objects/
      // null as canonical JSON so downstream tools like jq get one JSON literal),
      // with the bash-escaping decision staying here at the call site.
      const text = canonicalValueText(value);
      return escapedForBash ? shellQuoteOrFile(text, nodeId, field, artifactsDir) : text;
    }
  );
}

View on GitHub (pinned to 0773b97458)

Solutions

  1. 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.
  2. Verify the field name exists in the producer node's declared structuredOutput schema; add it to the schema or read an actual declared field.
  3. 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.
  4. 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.
  5. 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.

Example fix

// before (until_bash condition referencing undeclared field)
until_bash: |
  [ "$reviewer.output.approved" = "true" ]
// after (producer declares the field in structuredOutput)
output_schema:
  type: object
  properties:
    approved: { type: boolean }
  required: [approved]
until_bash: |
  [ "$reviewer.output.approved" = "true" ]
Defensive patterns

Strategy: validation

Validate before calling

// Before declaring the until/loop condition, verify every $id.output.field ref
// targets a node id present in the workflow and a field declared in its schema:
function validateOutputRefs(
  body: string,
  nodes: Map<string, { fields?: readonly string[] }>
): string[] {
  const errors: string[] = [];
  const re = /\$([a-zA-Z_][a-zA-Z0-9_-]*)\.output(?:\.([a-zA-Z_][a-zA-Z0-9_]*))?/g;
  for (const m of body.matchAll(re)) {
    const [, nodeId, field] = m;
    const node = nodes.get(nodeId);
    if (!node) errors.push(`unknown node '${nodeId}'`);
    else if (field && node.fields && !node.fields.includes(field))
      errors.push(`field '${field}' not declared on node '${nodeId}'`);
  }
  return errors;
}

Type guard

function isResolvableOutputRef(
  nodeId: string,
  field: string | undefined,
  nodeOutputs: Map<string, NodeOutput>
): boolean {
  const out = nodeOutputs.get(nodeId);
  if (!out) return false;
  if (!field) return out.state === 'succeeded';
  try { resolveNodeOutputField(out, nodeId, field); return true; }
  catch { return false; }
}

Try / catch

try {
  substituteNodeOutputRefs(prompt, nodeOutputs, true, artifactsDir, ctx);
} catch (err) {
  if (err instanceof Error && /cannot resolve '/.test(err.message)) {
    // Surface which consumer/field/ref failed; fix YAML id or schema — do not retry.
    log.error({ message: err.message }, 'output_ref_unresolvable');
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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