AutomationFlowsAI & RAG › Production AI Playbook: Deterministic Steps & AI Steps (2 of 5)

Production AI Playbook: Deterministic Steps & AI Steps (2 of 5)

ByElvis Sarvia @elvissaravia on n8n.io

Validate AI-generated outputs before your workflow acts on them. This template sends a support ticket through AI classification, parses the JSON response, and checks that categories, urgency levels, and confidence scores are all within valid ranges.

Webhook trigger★★★★☆ complexityAI-powered13 nodesAgentOpenRouter ChatOutput Parser Structured
AI & RAG Trigger: Webhook Nodes: 13 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #13851 — we link there as the canonical source.

This workflow follows the Agent → OpenRouter Chat 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 →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "7d7a6746-f791-496b-bdab-459812bfd96a",
      "name": "Webhook - Receive Support Ticket",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        288
      ],
      "parameters": {
        "path": "classify-ticket",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "8abe42fe-476d-4786-af1f-629037a5d995",
      "name": "Clean Ticket Data",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        288
      ],
      "parameters": {
        "jsCode": "// Pre-process: clean the ticket text\nconst raw = $input.first().json.body;\nreturn {\n  json: {\n    ticketId: raw.ticketId || raw.id || 'UNKNOWN',\n    subject: (raw.subject || '').trim(),\n    body: (raw.body || raw.message || raw.text || '').trim().substring(0, 2000),\n    customerEmail: (raw.email || '').toLowerCase().trim(),\n    receivedAt: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "9c0b5bb3-454b-4aff-b77c-9e14b3893399",
      "name": "AI - Classify Ticket",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        672,
        288
      ],
      "parameters": {
        "text": "=Classify the following support ticket.\n\nTicket ID: {{ $json.ticketId }}\nSubject: {{ $json.subject }}\nBody: {{ $json.body }}\n\nClassify into exactly one category: billing, technical, sales, or general.\nRate urgency as: low, normal, or urgent.\nProvide a confidence score between 0 and 1.\nWrite a brief summary (max 100 words).",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "92bc20de-3dd6-45a2-8ae5-3bb6de645cf3",
      "name": "Validate AI Output",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        288
      ],
      "parameters": {
        "jsCode": "// Semantic validation (structure enforced by Structured Output Parser)\n// AI Agent with Structured Output Parser nests parsed fields under .output\nconst raw = $input.first().json;\nconst parsed = raw.output || raw;\nconst errors = [];\n\nif (typeof parsed.confidence !== \"number\" || parsed.confidence < 0 || parsed.confidence > 1) {\n  errors.push(\"Confidence must be between 0 and 1, got: \" + parsed.confidence);\n}\nif (!parsed.summary || parsed.summary.length > 500) {\n  errors.push(\"Summary missing or exceeds 500 characters\");\n}\n\nreturn {\n  json: {\n    category: parsed.category,\n    urgency: parsed.urgency,\n    confidence: parsed.confidence,\n    summary: parsed.summary,\n    isValid: errors.length === 0,\n    validationErrors: errors\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "5633f342-11d1-4de2-938a-baec25ffd599",
      "name": "Is Output Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        1216,
        288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "valid-check",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.isValid }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3e0528fe-cdb1-493c-800b-97cdcecb8a12",
      "name": "Respond - Classification Result",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1472,
        192
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ category: $json.category, urgency: $json.urgency, confidence: $json.confidence, summary: $json.summary }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "62c7be56-d1d7-4bff-88c3-8173f8563ef6",
      "name": "Respond - Needs Review",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1472,
        416
      ],
      "parameters": {
        "options": {
          "responseCode": 422
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'validation_failed', errors: $json.validationErrors, needsHumanReview: true }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "0d2e0779-daf3-441e-925d-498b04338c52",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        656,
        496
      ],
      "parameters": {
        "model": "google/gemini-3-flash-preview",
        "options": {}
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d9d387a9-70bd-4605-8f86-740d7305e975",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        80
      ],
      "parameters": {
        "width": 576,
        "height": 736,
        "content": "## Structured Output Parsing + Validation\n\n### How it works\n1. **Webhook** receives a support ticket via POST request.\n2. **Clean Ticket Data** (Code node) strips HTML, trims whitespace, and standardizes fields before AI processing.\n3. **AI - Classify Ticket** (AI Agent + OpenRouter) classifies the ticket by category, urgency, and confidence score, returning structured JSON.\n4. **Validate AI Output** (Code node) checks that the category is from a known list, urgency is valid, and confidence is between 0 and 1. Structural correctness alone is not enough; this checks semantic correctness.\n5. **Is Output Valid?** (IF node) routes valid classifications to the success response and invalid ones to a \"needs review\" path.\n\n### Setup\n- Connect your **LLM credentials** (e.g., OpenRouter, OpenAI) to the Chat Model node\n- Copy the Webhook test URL and send a POST with a JSON body containing a support ticket (fields: subject, body, customerEmail)\n- Review the valid categories and urgency levels in the **Validate AI Output** code node and adjust for your system\n\n### Customization\n- Add retry logic by looping invalid outputs back to the AI with the validation error message\n- Swap the Respond nodes for actual downstream actions (create Jira ticket, send Slack alert)\n\n\nThis template is a learning companion to the Production AI Playbook, a series that explores strategies, shares best practices, and provides practical examples for building reliable AI systems in n8n.  \n\nhttps://go.n8n.io/PAP-D&A-Blog"
      },
      "typeVersion": 1
    },
    {
      "id": "81fad750-4b67-4cd7-b6a6-52c2d11936df",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 496,
        "content": "## Receive & Clean\n"
      },
      "typeVersion": 1
    },
    {
      "id": "bb0017a6-ad26-4ebc-bc81-e8b7b31a7013",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 496,
        "content": "## AI Classification\n"
      },
      "typeVersion": 1
    },
    {
      "id": "825d373a-2c5b-471a-a7ae-da4f750c4619",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        944,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 496,
        "content": "## Validate & Route\n"
      },
      "typeVersion": 1
    },
    {
      "id": "bd61fc72-0250-460b-9947-b19b358f041f",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        816,
        496
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"category\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"billing\",\n        \"technical\",\n        \"sales\",\n        \"general\"\n      ],\n      \"description\": \"The support ticket category\"\n    },\n    \"urgency\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"low\",\n        \"normal\",\n        \"urgent\"\n      ],\n      \"description\": \"How urgent the ticket is\"\n    },\n    \"confidence\": {\n      \"type\": \"number\",\n      \"description\": \"Confidence score between 0 and 1\"\n    },\n    \"summary\": {\n      \"type\": \"string\",\n      \"description\": \"Brief summary of the ticket, max 100 words\"\n    }\n  },\n  \"required\": [\n    \"category\",\n    \"urgency\",\n    \"confidence\",\n    \"summary\"\n  ]\n}"
      },
      "typeVersion": 1.3
    }
  ],
  "connections": {
    "Is Output Valid?": {
      "main": [
        [
          {
            "node": "Respond - Classification Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond - Needs Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Ticket Data": {
      "main": [
        [
          {
            "node": "AI - Classify Ticket",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate AI Output": {
      "main": [
        [
          {
            "node": "Is Output Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Classify Ticket": {
      "main": [
        [
          {
            "node": "Validate AI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Classify Ticket",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "AI - Classify Ticket",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Support Ticket": {
      "main": [
        [
          {
            "node": "Clean Ticket Data",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Validate AI-generated outputs before your workflow acts on them. This template sends a support ticket through AI classification, parses the JSON response, and checks that categories, urgency levels, and confidence scores are all within valid ranges.

Source: https://n8n.io/workflows/13851/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.

Supabase, Gmail, Form Trigger +13
AI & RAG

Transform your WhatsApp group conversations into actionable business intelligence through automated AI analysis and daily reporting. This workflow eliminates manual conversation monitoring by capturin

OpenRouter Chat, Output Parser Autofixing, Agent +6
AI & RAG

MiniBear Webhook. Uses httpRequest, stickyNote, lmChatOpenRouter, agent. Webhook trigger; 45 nodes.

HTTP Request, OpenRouter Chat, Agent +4
AI & RAG

This workflow template, "Personal Assistant to Note Messages and Extract Namecard Information" is designed to streamline the processing of incoming messages on the LINE messaging platform. It integrat

HTTP Request, OpenRouter Chat, Agent +4
AI & RAG

This workflow is designed for legal professionals, policy analysts, and compliance teams who need to: Research case law, legislation, and regulatory developments on specific topics Build comprehensive

HTTP Request Tool, Tool Http Request, Tool Think +5