data/skills/n8n-agents/STRUCTURED_OUTPUT.md
Non-negotiable: the output parser must parse AND retry on failure. Without retry, one malformed model response halts the entire workflow.
The parser is the @n8n/n8n-nodes-langchain.outputParserStructured node, wired into the agent (or Basic LLM Chain) via the ai_outputParser connection.
The parser, with autoFix and its own fixer model:
{
"parameters": {
"schemaType": "manual",
"inputSchema": "{ \"type\": \"object\", \"properties\": { \"score\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 5 }, \"reason\": { \"type\": \"string\" } }, \"required\": [\"score\", \"reason\"] }",
"autoFix": true
},
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"typeVersion": 1.3,
"name": "Structured Output Parser"
}
Wire the parser to the agent, and a coding-capable fixer model to the parser:
"Structured Output Parser": {
"ai_outputParser": [[{ "node": "AI Agent", "type": "ai_outputParser", "index": 0 }]]
},
"Fixer LLM": {
"ai_languageModel": [[{ "node": "Structured Output Parser", "type": "ai_languageModel", "index": 0 }]]
}
On the agent, set hasOutputParser: true so the slot is active.
schemaType: 'manual' with a real JSON Schema is the default. jsonSchemaExample (schemaType: 'fromJson') looks easier, but an example cannot express:
"category": "compliance" doesn't constrain the model to compliance | history | risk; it will invent new categories."score": 3 doesn't say 1-5; the model returns 7 or 0.85 and passes.A schema gives the model clearer rules and the parser real validation:
{
"type": "object",
"properties": {
"decision": { "type": "string", "enum": ["approve", "reject", "escalate"] },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"reasons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["compliance", "history", "risk"] },
"weight": { "type": "number", "minimum": 0, "maximum": 1 },
"note": { "type": "string" }
},
"required": ["category", "weight"]
}
},
"follow_up_required": { "type": "boolean" }
},
"required": ["decision", "confidence", "reasons", "follow_up_required"]
}
Reach for fromJson + jsonSchemaExample only for one-off shapes you're certain will never grow constraints. Once a field needs to be optional, enum-ed, or range-bounded, you're rewriting the parser anyway — start with the schema.
autoFix: true and the fixer modelThe model can produce almost-but-not-quite-valid JSON: trailing comma, missing field, wrong type, or JSON wrapped in a markdown code block. Without autoFix, the workflow halts. With it, the parser sends the bad output to a model with a "fix this" prompt, retries, and continues.
The fixer is wired as a separate sub-node into the parser's ai_languageModel slot. Use a coding-capable model (Sonnet-class or better). Reconciling broken JSON against a schema with enums, ranges, and required fields is a structured-output / coding task — a weak or generic model routinely produces another malformed retry, defeating the point and burning tokens.
When you want to customize the retry prompt, set customizeRetryPrompt: true and provide prompt. The placeholders {instructions}, {completion}, {error} are filled at retry time:
Instructions:
--------------
{instructions}
--------------
Completion:
--------------
{completion}
--------------
Above, the Completion did not satisfy the constraints in the Instructions.
Error:
--------------
{error}
--------------
Please try again with an answer that satisfies the constraints.
This is a structured output parser tool in n8n. Ensure the output format is correct to pass parsing.
DO NOT wrap the output in a markdown code block.
Generally, leave the retry prompt as default unless you have a specific reason to override it.
This line is load-bearing. Models default to wrapping JSON in triple-backtick json fences, which breaks the parser. If you see parse failures on output that's clearly valid JSON inside a code block, this instruction is the fix — in both the retry prompt and, if the main model wraps aggressively, the main system prompt:
When responding with structured output, return raw JSON only. DO NOT wrap in markdown code blocks. DO NOT include any prose before or after the JSON.
The parser tells the model the schema; the system prompt should ALSO state the shape:
## Output Format
Respond with a JSON object matching this exact shape:
{ "score": 1-5 integer, "reason": "brief explanation" }
ONLY output the JSON. No prose, no markdown wrapping.
It's repetition, but the model takes the system prompt seriously and reinforcement helps. The parser catches what slips through.
| Symptom | Likely cause | Fix |
|---|---|---|
| "Failed to parse output" but the text looks like JSON | Wrapped in a markdown code block | Add "DO NOT wrap in markdown" to retry prompt and system prompt |
| Empty fields where the schema expects values | Model thinks it can omit unknowns | "Use empty string '' or null for unknown fields, never omit" |
| Wrong types (number as string) | Schema/example wasn't typed clearly | Use a real number in the schema, not a string |
| Truncated JSON (unclosed brace) | Hit max tokens mid-response | Increase max tokens, tighten the prompt to produce shorter output |
| Field names paraphrased ("Score" vs "score") | Schema didn't pin the name | "Field names are exactly as shown" in the system prompt |
autoFix retries forever | Fixer model too weak for the schema | Swap in a coding-capable (Sonnet-class) fixer; tighten the retry prompt |
JSON.parse($json.output) covers it.The parser is for when downstream nodes must consume strict JSON.