This workflow corresponds to n8n.io template #4316 — we link there as the canonical source.
This workflow follows the Agent → Chat Trigger recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "",
"name": "Reliable Structured Output from AI Agent Without the Structured Output Parser - with OpenAI & Switch node",
"tags": [],
"nodes": [
{
"id": "",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-400,
760
],
"parameters": {
"text": "={{ $json.chatInput }}",
"options": {
"maxIterations": 10,
"systemMessage": "=You are a helpful assistant specialized in nutrition. \nYour task is to provide accurate nutritional information for a given food item. \nYou must return your answer strictly in the form of a JSON object matching the following schema:\n\n{\n \"type\": \"object\",\n \"properties\": {\n \"alimentName\": {\n \"type\": \"string\",\n \"description\": \"The name of the food item, in English\"\n },\n \"averageCalories\": {\n \"type\": \"number\",\n \"description\": \"Average calories per 100g or standard portion (kcal)\"\n },\n \"proteins\": {\n \"type\": \"number\",\n \"description\": \"Amount of protein per 100g or portion (grams)\"\n },\n \"carbohydrates\": {\n \"type\": \"number\",\n \"description\": \"Total amount of carbohydrates per 100g or portion (grams), including fiber and sugars\"\n },\n \"sugar\": {\n \"type\": \"number\",\n \"description\": \"Amount of total sugars (subset of carbohydrates) per 100g or portion (grams)\"\n },\n \"fiber\": {\n \"type\": \"number\",\n \"description\": \"Amount of dietary fiber (subset of carbohydrates) per 100g or portion (grams)\"\n },\n \"fat\": {\n \"type\": \"number\",\n \"description\": \"Amount of fat per 100g or portion (grams)\"\n },\n \"sodium\": {\n \"type\": \"number\",\n \"description\": \"Amount of sodium per 100g or portion (milligrams)\"\n },\n \"healthyScore\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 10,\n \"description\": \"A healthiness score from 0 (very unhealthy) to 10 (very healthy), based on nutritional guidelines\"\n }\n },\n \"required\": [\n \"alimentName\",\n \"averageCalories\",\n \"proteins\",\n \"carbohydrates\",\n \"sugar\",\n \"fiber\",\n \"fat\",\n \"sodium\",\n \"healthyScore\"\n ]\n}\n\n\nIf the user input is not a valid food item, or if you are unsure whether it is a real food, then instead return:\n\n{\n \"error\": \"invalid_input\",\n \"message\": \"The provided input does not appear to be a valid food item.\"\n}\n\n----\n\n\u26a0\ufe0f If you fail to produce output in the correct schema, the Schema Error Prompt below will contain an error message. You will need to follow the instructions it provides:\n\n## Schema Error Prompt:\n\n{{ $json.schemaErrorPrompt }}\n\n",
"returnIntermediateSteps": true
},
"promptType": "define"
},
"executeOnce": false,
"typeVersion": 1.8
},
{
"id": "",
"name": "When chat message received",
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
"position": [
-800,
760
],
"parameters": {
"mode": "webhook",
"public": true,
"options": {
"responseMode": "responseNode"
},
"authentication": "basicAuth"
},
"credentials": {
"httpBasicAuth": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-400,
920
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "=gpt-4.1-nano"
},
"options": {
"temperature": 0.8,
"responseFormat": "json_object"
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "",
"name": "Simple Memory",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"position": [
-260,
920
],
"parameters": {},
"typeVersion": 1.3
},
{
"id": "",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"position": [
860,
760
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "invalidSchema",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.output.error !== undefined && $json.aiRunIndex < 3 }}",
"rightValue": ""
}
]
},
"renameOutput": true
},
{
"outputKey": "validSchema",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.output.alimentName }}",
"rightValue": ""
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"typeVersion": 3.2
},
{
"id": "",
"name": "Validate Output + Set `aiRunIndex`",
"type": "n8n-nodes-base.set",
"onError": "continueRegularOutput",
"position": [
260,
760
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "",
"name": "output",
"type": "object",
"value": "={{ \n(() => {\n\tlet raw = $json.output;\n\n\tif (typeof raw === 'string') {\n\t\traw = raw\n\t\t\t.replace(/^\\s*```json/i, '')\n\t\t\t.replace(/```$/i, '')\n\t\t\t.trim();\n\t\ttry { raw = JSON.parse(raw); }\n\t\tcatch { return { error: 'invalid_json' }; }\n\t}\n\n\t// Allow alternative valid response when input is not a valid food item\n\tif (\n\t\traw.error === 'invalid_input' &&\n\t\traw.message === 'The provided input does not appear to be a valid food item.'\n\t) {\n\t\treturn JSON.stringify(raw, null, 2);\n\t}\n\n\t// Check required keys\n\tconst requiredKeys = [\n\t\t'alimentName',\n\t\t'averageCalories',\n\t\t'proteins',\n\t\t'carbohydrates',\n\t\t'sugar',\n\t\t'fiber',\n\t\t'fat',\n\t\t'sodium',\n\t\t'healthyScore'\n\t];\n\n\tfor (const key of requiredKeys) {\n\t\tif (!(key in raw)) {\n\t\t\treturn { error: 'missing_key', key };\n\t\t}\n\t}\n\n\t// Type checks\n\tif (typeof raw.alimentName !== 'string')\n\t\treturn { error: 'invalid_type', key: 'alimentName', expected: 'string' };\n\n\tif (typeof raw.averageCalories !== 'number')\n\t\treturn { error: 'invalid_type', key: 'averageCalories', expected: 'number' };\n\n\tif (typeof raw.proteins !== 'number')\n\t\treturn { error: 'invalid_type', key: 'proteins', expected: 'number' };\n\n\tif (typeof raw.carbohydrates !== 'number')\n\t\treturn { error: 'invalid_type', key: 'carbohydrates', expected: 'number' };\n\n\tif (typeof raw.sugar !== 'number')\n\t\treturn { error: 'invalid_type', key: 'sugar', expected: 'number' };\n\n\tif (typeof raw.fiber !== 'number')\n\t\treturn { error: 'invalid_type', key: 'fiber', expected: 'number' };\n\n\tif (typeof raw.fat !== 'number')\n\t\treturn { error: 'invalid_type', key: 'fat', expected: 'number' };\n\n\tif (typeof raw.sodium !== 'number')\n\t\treturn { error: 'invalid_type', key: 'sodium', expected: 'number' };\n\n\tif (typeof raw.healthyScore !== 'number')\n\t\treturn { error: 'invalid_type', key: 'healthyScore', expected: 'number' };\n\n\tif (raw.healthyScore < 0 || raw.healthyScore > 10)\n\t\treturn { error: 'invalid_range', key: 'healthyScore', expected: 'number between 0 and 10' };\n\n\t// If all checks pass, return the parsed and formatted JSON\n\treturn JSON.stringify(raw, null, 2);\n})() \n}}"
},
{
"id": "",
"name": "=aiRunIndex",
"type": "number",
"value": "={{ $node[\"AI Agent\"].runIndex }} "
}
]
}
},
"typeVersion": 3.4
},
{
"id": "",
"name": "Format Schema Error Prompt",
"type": "n8n-nodes-base.set",
"position": [
920,
1020
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "",
"name": "schemaErrorPrompt",
"type": "string",
"value": "=If you're seeing this message, it means your previous response did not follow the required output schema defined in your prompt:\n\n{\n \"type\": \"object\",\n \"properties\": {\n \"alimentName\": {\n \"type\": \"string\",\n \"description\": \"The name of the food item, in English\"\n },\n \"averageCalories\": {\n \"type\": \"number\",\n \"description\": \"Average calories per 100g or standard portion (kcal)\"\n },\n \"proteins\": {\n \"type\": \"number\",\n \"description\": \"Amount of protein per 100g or portion (grams)\"\n },\n \"carbohydrates\": {\n \"type\": \"number\",\n \"description\": \"Total amount of carbohydrates per 100g or portion (grams), including fiber and sugars\"\n },\n \"sugar\": {\n \"type\": \"number\",\n \"description\": \"Amount of total sugars (subset of carbohydrates) per 100g or portion (grams)\"\n },\n \"fiber\": {\n \"type\": \"number\",\n \"description\": \"Amount of dietary fiber (subset of carbohydrates) per 100g or portion (grams)\"\n },\n \"fat\": {\n \"type\": \"number\",\n \"description\": \"Amount of fat per 100g or portion (grams)\"\n },\n \"sodium\": {\n \"type\": \"number\",\n \"description\": \"Amount of sodium per 100g or portion (milligrams)\"\n },\n \"healthyScore\": {\n \"type\": \"number\",\n \"minimum\": 0,\n \"maximum\": 10,\n \"description\": \"A healthiness score from 0 (very unhealthy) to 10 (very healthy), based on nutritional guidelines\"\n }\n },\n \"required\": [\n \"alimentName\",\n \"averageCalories\",\n \"proteins\",\n \"carbohydrates\",\n \"sugar\",\n \"fiber\",\n \"fat\",\n \"sodium\",\n \"healthyScore\"\n ]\n}\n\n\nPlease revise your output to strictly match this structure.\n\nFor reference, the last user message was:\n{{ $('When chat message received').item.json.chatInput }}\n\nAnd your response was:\n{{ $('AI Agent').item.json.output }}\n\nThis does not conform to the expected schema. Please correct your output accordingly."
},
{
"id": "",
"name": "sessionId",
"type": "string",
"value": "={{ $('When chat message received').item.json.sessionId }}"
},
{
"id": "",
"name": "chatInput",
"type": "string",
"value": "={{ $('When chat message received').item.json.chatInput }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "",
"name": "Valid Schema Output",
"type": "n8n-nodes-base.set",
"position": [
1460,
760
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "",
"name": "nutritionalValues",
"type": "object",
"value": "={{ $json.output }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
600,
-260
],
"parameters": {
"color": 7,
"width": 520,
"height": 1140,
"content": "## Switch\n\n## \u26a0\ufe0f Warning : \ud83d\udd01 **Infinite loop risk**\n\nIf you **modify this node without understanding** the expressions inside, it can cause an infinite loop, which is **exactly what you don\u2019t want** for your n8n instance, your results\u2026 or your API credits ($). \ud83d\ude09\n\nThe logic uses **$json.output.error !== undefined && $json.aiRunIndex < 3** to control retries. \nChanging this without adjusting the flow logic may cause endless retries or silent failures.\n\n\n----\n\n## How it Works\n\nThis `Switch` node routes the data flow based on validation outcomes:\n\n### \ud83d\udfe5 `invalidSchema`\n- **Condition:** \n **{{ $json.output.error !== undefined && $json.aiRunIndex < 3 }}**\n- **Purpose:** \n Detects if there's an error in the output **and** the number of AI attempts (`aiRunIndex`) is less than 3. \n Used to trigger a retry or a corrective step before giving up.\nIf it gives up, the **Fallback route** with an error message will be used.\n\n### \ud83d\udfe9 `validSchema`\n- **Condition:** \n `{{ $json.output.alimentName }} exists`\n- **Purpose:** \n Validates that the `alimentName` field is present in the output, implying a successful extraction or transformation.\n\n### \ud83d\udfe8 Fallback (named `extra`)\n- **Purpose:** \n Any case not matching the above two will be routed here. \n Useful for handling unexpected or unvalidated scenarios.\n\n---\n\n**Good practice:** \nBefore modifying this node, trace the flow downstream and ensure your logic won't bypass validation or introduce unintended retry loops.\n"
},
"typeVersion": 1
},
{
"id": "",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-480,
-160
],
"parameters": {
"color": 7,
"width": 500,
"height": 1040,
"content": "## AI Agent\n\n\nThis node sets an **AI agent** to generate nutritional information based on a food name provided via `chatInput`.\n\n\n---\n\n## How it works:\n\n- \ud83d\udcac **Prompt definition:**\n The agent is instructed to behave like a nutrition expert. It must return a response **strictly in JSON format**, matching a predefined schema with fields like:\n - `alimentName`\n - `averageCalories`\n - `proteins`, `carbohydrates`, `sugar`, `fiber`, `fat`, `sodium`\n - `healthyScore` (from 0 to 10)\n\n\nHowever, **structured output parser is not used here**, because the node tends to throw errors frequently when parsing is enforced. Instead, the output is manually validated in later steps.\n\n**Check the System Prompt** : it\u2019s designed to **allow the AI Agent** to **receive an \"Error Prompt\"** if the response doesn\u2019t follow the expected schema.\n\n\n\n```\n\u26a0\ufe0f If you fail to produce output in the correct schema, the Schema Error Prompt below will contain an error message. You will need to follow the instructions it provides:\n## Schema Error Prompt:\n{{ $json.schemaErrorPrompt }}\n```\n- **Error handling:**\n If the input is not a valid food, the agent must return:\n ```json\n {\n \"error\": \"invalid_input\",\n \"message\": \"The provided input does not appear to be a valid food item.\"\n }\n"
},
"typeVersion": 1
},
{
"id": "",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
60,
-160
],
"parameters": {
"color": 7,
"width": 500,
"height": 1040,
"content": "## Validate Output + Set `aiRunIndex`\n\nThis node **validates** the AI output to ensure it matches the expected nutrition schema, \nand sets a helper variable `aiRunIndex` for retry tracking.\n\nOf course, you'll need to **edit the IIFE that checks the schema** to match the structure you expect. \nIf you're not comfortable with coding, **OpenAI's o3 model does a great job helping with that**.\n\n\n\n----\n\n## What it does:\n\n- \u2705 **Parses AI response** (if it's a string with ` ```json ` wrapper).\n- \u2705 **Validates structure**:\n - Checks presence of all required keys (e.g. `alimentName`, `averageCalories`, etc.).\n - Checks types (`string` for names, `number` for nutrition values).\n - Validates that `healthyScore` is between `0` and `10`.\n\n- \u274c **If anything fails**, it returns a structured error:\n - `invalid_json`, `missing_key`, `invalid_type`, or `invalid_range`.\n\n- \ud83d\udd01 **Sets `aiRunIndex`** based on how many times the AI node has run (used later to limit retries).\n\n---\n\n**Note:** \nThis replaces a structured parser and gives full control over error handling without crashing the workflow."
},
"typeVersion": 1
},
{
"id": "",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1160,
360
],
"parameters": {
"color": 7,
"width": 700,
"height": 520,
"content": "## Valid Schema Output\n\n\nThis node stores the **validated nutritional data** under a clean variable name: `nutritionalValues`.\n\n\n---\n\n## What it does:\n\n- Takes the parsed and validated `output` from earlier steps.\n- Assigns it to a new variable called `nutritionalValues` for easier access and downstream processing.\n\n---\n\n**Note:** \nAt this point, the data is assumed to be valid \u2014 all schema and type checks have already been performed.\n"
},
"typeVersion": 1
},
{
"id": "",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1160,
940
],
"parameters": {
"color": 7,
"width": 700,
"height": 600,
"content": "## Output Handling (Valid & Invalid Schema)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Set schemaValidationError & lastAgentOutput\n- Stores the output of the node \"Validate Output + Set `aiRunIndex`\" in a variable called `schemaValidationError`.\n- Stores the output of the node \"AI Agent\" in a variable called `lastAgentOutput`.\n\n## Set chat Output\n- Constructs a user-facing message in the `output` field using the two previously stored variables.\n- The message explains that a schema validation error occurred and provides the last response from the AI agent as fallback context.\n"
},
"typeVersion": 1
},
{
"id": "",
"name": "Set schemaValidationError & lastAgentOutput",
"type": "n8n-nodes-base.set",
"position": [
1280,
1020
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "",
"name": "schemaValidationError",
"type": "string",
"value": "={{ $('Validate Output + Set `aiRunIndex`').item.json.output }}"
},
{
"id": "",
"name": "lastAgentOutput",
"type": "string",
"value": "={{ $('AI Agent').item.json.output }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "",
"name": "Set chat Output",
"type": "n8n-nodes-base.set",
"position": [
1640,
1020
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "",
"name": "output",
"type": "string",
"value": "=This output was sent because a schema validation error occurred:\n\n{{ $json.schemaValidationError }}\n\nHowever, here is the last AI agent response:\n\n{{ $json.lastAgentOutput }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1580,
-60
],
"parameters": {
"color": 5,
"width": 720,
"height": 940,
"content": "## Reliable Structured Output from AI Agent *Without* the Structured Output Parser - with OpenAI & Switch node\n\nThis workflow serves as a **solid foundation** when you need an **AI Agent to return output in a specific JSON schema**, without relying on the often-unreliable **Structured Output Parser**.\n\n## What It Does\nThe example workflow takes a simple input (like a food item) and expects a JSON-formatted output containing its nutritional values.\n\n## Why Use This Instead of Structured Output Parser?\n\nThe built-in [Structured Output Parser](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/common-issues/) node is known to be unreliable when working with AI Agents.\n\nWhile the **n8n documentation recommends using a \u201cBasic LLM Chain\u201d** followed by a **Structured Output Parser**, this alternative workflow **completely avoids using the Structured Output Parser node**. \nInstead, it implements a custom loop that manually validates the AI Agent's output.\n\nThis method has **proven especially reliable** with OpenAI's `gpt-4.1` series (`gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`), which tend to **produce correctly structured JSON** on the first try, as long as the **System Prompt is well defined**.\nIn this template, `gpt-4.1-nano` is set by default.\n\n### How It Works\n\nInstead of using the *Structured Output Parser*, this workflow loops the AI Agent through a manual schema validation process:\n\n- A **custom schema check** is performed after the AI Agent response.\n- A **runIndex counter** tracks the number of retries.\n- A **Switch node**:\n - If the output does **not** match the expected schema, it routes back to the AI Agent with an updated prompt asking it to return the correct format. The process allows up to **4 retries** to avoid infinite loops.\n - If the output **does** match the schema, it continues to a **Set node** that serves as chat response (you can customize this part to fit your use case).\n\n\nThis approach ensures schema consistency, offers flexibility, and avoids the brittleness of the default parser.\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"callerPolicy": "",
"executionOrder": "v1",
"executionTimeout": 60
},
"versionId": "",
"connections": {
"Switch": {
"main": [
[
{
"node": "Format Schema Error Prompt",
"type": "main",
"index": 0
}
],
[
{
"node": "Valid Schema Output",
"type": "main",
"index": 0
}
],
[
{
"node": "Set schemaValidationError & lastAgentOutput",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Validate Output + Set `aiRunIndex`",
"type": "main",
"index": 0
}
]
]
},
"Simple Memory": {
"ai_memory": [
[
{
"node": "AI Agent",
"type": "ai_memory",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Format Schema Error Prompt": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"When chat message received": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Validate Output + Set `aiRunIndex`": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Set schemaValidationError & lastAgentOutput": {
"main": [
[
{
"node": "Set chat Output",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpBasicAuthopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow serves as a solid foundation when you need an AI Agent to return output in a specific JSON schema, without relying on the often-unreliable Structured Output Parser.
Source: https://n8n.io/workflows/4316/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
HDW Lead Geländewagen. Uses chatTrigger, lmChatOpenAi, memoryBufferWindow, outputParserStructured. Chat trigger; 92 nodes.
router-agent. Uses chatTrigger, agent, lmChatOpenAi, executeWorkflow. Chat trigger; 59 nodes.
by Varritech Technologies
Community Node Disclaimer: This workflow uses KlickTipp community nodes.
Who is this workflow for? This workflow is designed for SEO analysts, content creators, marketing agencies, and developers who need to index a website and then interact with its content as if it were