164 lines
6.8 KiB
Markdown
164 lines
6.8 KiB
Markdown
# Structured output
|
|
|
|
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 pattern (node objects)
|
|
|
|
The parser, with `autoFix` and its own fixer model:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
"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.
|
|
|
|
---
|
|
|
|
## Why a schema, not an example
|
|
|
|
`schemaType: 'manual'` with a real JSON Schema is the default. `jsonSchemaExample` (`schemaType: 'fromJson'`) looks easier, but an example **cannot** express:
|
|
|
|
- **Required vs optional fields** — an example is one snapshot; the parser can't tell which keys are mandatory.
|
|
- **Enums** — `"category": "compliance"` doesn't constrain the model to `compliance | history | risk`; it will invent new categories.
|
|
- **Numeric ranges** — `"score": 3` doesn't say `1-5`; the model returns `7` or `0.85` and passes.
|
|
- **Array constraints** — min/max items, item-type uniformity.
|
|
- **String formats** — email, UUID, ISO date, regex.
|
|
|
|
A schema gives the model clearer rules and the parser real validation:
|
|
|
|
```json
|
|
{
|
|
"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 model
|
|
|
|
The 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.
|
|
|
|
---
|
|
|
|
## "DO NOT wrap the output in a markdown code block"
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## System prompt + parser: belt and suspenders
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## Common parse failures and fixes
|
|
|
|
| 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 |
|
|
|
|
---
|
|
|
|
## When NOT to use a parser
|
|
|
|
- **Free-form chat replies to the user** — conversational text doesn't need parsing.
|
|
- **Tool calls only, no final structured output** — if the user-visible output is text, skip it.
|
|
- **Trivial key-value extraction** — a Set node with `JSON.parse($json.output)` covers it.
|
|
|
|
The parser is for when downstream nodes must consume strict JSON.
|
|
|
|
---
|
|
|
|
## Cross-references
|
|
|
|
- Why and where to use agents at all → parent **SKILL.md**
|
|
- The system-prompt half of structured output → **SYSTEM_PROMPT.md**
|
|
- Block Kit / adaptive cards need the manual schema even more (union types) → **CHAT_AGENT_PATTERNS.md**
|