{"record":{"id":"6d6fe47c756c9527","repo":"coleam00/Archon","slug":"unparseablereason-nodeoutput-output-dynamic-reas","errorCode":null,"errorMessage":"unparseableReason(nodeOutput.output) (dynamic reason code for unparseable producer output)","messagePattern":"unparseableReason\\(nodeOutput\\.output\\) \\(dynamic reason code for unparseable producer output\\)","errorType":"error_code","errorClass":"OutputRefError","httpStatus":null,"severity":"error","filePath":"packages/workflows/src/output-ref.ts","lineNumber":375,"sourceCode":"\n  // 1. Declared-schema producer — the declared property set IS the contract.\n  if (declaredFields !== undefined) {\n    if (!declaredFields.includes(field)) {\n      throw new OutputRefError(nodeId, field, 'not-in-schema');\n    }\n    // Prefer the parsed payload; fall back to parsing the JSON-serialized output.\n    // The fallback covers older NodeOutput rows that predate `structuredOutput`,\n    // and resumes of runs persisted before `structured_output` rode along in\n    // `node_completed` events (#2637) — current resumes rehydrate the payload.\n    const obj = structuredObj ?? parseOutputObject(nodeOutput.output);\n    // No parseable object AT ALL is not a declared-optional field — it is a producer\n    // that did not honour its schema, and it must fail exactly as loudly as the\n    // schemaless path below (#2456). Returning empty here made declaring\n    // `output_format` QUIETER than declaring nothing, which is backwards: a\n    // `workflow:` node's output_format is never validated against the child (it only\n    // populates declaredFields), so every declared field silently became ''.\n    if (obj === undefined) {\n      throw new OutputRefError(nodeId, field, unparseableReason(nodeOutput.output));\n    }\n    const value = obj[field];\n    // Required fields are guaranteed present (the producer validated post-parse),\n    // so a missing/explicit-null value here is a declared-optional field → empty.\n    if (value === undefined || value === null) return { kind: 'empty' };\n    return { kind: 'value', value };\n  }\n\n  // 2. Structured payload without a declared schema (legacy rows / non-object\n  //    schema): prefer it, but stay lenient — with no schema we cannot tell an\n  //    optional-absent field from a typo, so an absent field is '' (not a throw).\n  //    A present null value is kept (callers stringify it to \"null\"), matching\n  //    the historical structuredOutput-preference behavior.\n  if (structuredObj !== undefined) {\n    const value = structuredObj[field];\n    if (value === undefined) return { kind: 'empty' };\n    return { kind: 'value', value };\n  }","sourceCodeStart":357,"sourceCodeEnd":393,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/output-ref.ts#L357-L393","documentation":"For a declared-schema producer, if the output exists in the declared schema but cannot be parsed into a JSON object, the engine throws an OutputRefError whose reason comes from unparseableReason(): `unparseable`, `truncated` (a truncation marker indicates clipped persisted output), or `array-aggregate` (a fan-out aggregate that is a JSON array, not an object). Declaring a schema must fail at least as loudly as the schemaless path (#2456) — returning empty would make declared fields silently become ''.","triggerScenarios":"A declared-fields producer (its structuredOutput is absent/not an object, so the engine falls back to parsing `nodeOutput.output`) emitted text that is not a JSON object: prose, fenced non-JSON, clipped/truncated JSON on resume, or a JSON array from a fan-out aggregate.","commonSituations":"AI node ignoring output_format and returning prose; output too large and truncated before persistence, then read on resume; a loop_group/fan-out aggregate stored as an array while a consumer reads `.field` from it; model wrapping JSON in code fences the fence-stripper cannot parse.","solutions":["If reason is `truncated`: re-run/resume with the current binary so full structuredOutput is rehydrated; reduce output size or field count.","If reason is `array-aggregate`: read the array via the aggregate form rather than `$group.field`; the engine fixes array shape at consumption.","If reason is `unparseable`: strengthen the producer prompt/script to emit a JSON object matching output_format, or add a post-process step that coerces output to JSON.","Inspect the producer's raw output in the run to see what was actually emitted."],"exampleFix":"// before (producer emits prose)\nprompt: Summarize the diff.\n// after\nprompt: |\n  Summarize the diff.\n  Respond ONLY with JSON matching: {\"summary\": string}","handlingStrategy":"try-catch","validationCode":"// pre-validate the stored output before resolving fields\nconst parsed = parseOutputObject(nodeOutput.output);\nif (parsed === undefined && nodeOutput.structuredOutput === undefined) {\n  console.warn(`Producer ${nodeId} output is not a JSON object:`, unparseableReason(nodeOutput.output));\n}","typeGuard":"function producerOutputIsObject(o: { output: string; structuredOutput?: unknown }): boolean {\n  return asPlainObject(o.structuredOutput) !== undefined || parseOutputObject(o.output) !== undefined;\n}","tryCatchPattern":"try {\n  const { value } = resolveNodeOutputField(nodeOutput, nodeId, field);\n} catch (err) {\n  if (err instanceof OutputRefError && ['unparseable','truncated','array-aggregate'].includes(err.reason)) {\n    if (err.reason === 'truncated') return rehydrateOrResume();\n    if (err.reason === 'array-aggregate') return consumeAggregateForm();\n    return repairOrReRunProducer(nodeId);\n  }\n  throw err;\n}","preventionTips":["Pair output_format with prompts that say 'respond ONLY with JSON matching <schema>'.","Post-process AI output to strip code fences before it is persisted.","Keep node outputs under persistence size limits to avoid truncation markers.","For aggregates (loop_group), use the aggregate consumption form, not `$group.field`."],"tags":["workflow","output-refs","json-parsing"],"backgroundTag":"unparseable-node-output","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}