n8n: An AI Call and Ticket Router That Returns Strict JSON (Category, Urgency, Summary) and a Switch Node That Acts on It
A call or a ticket that lands with the wrong person costs everyone. The customer repeats themselves. Someone forwards it. Sometimes it sits in a shared inbox until Monday, and the one that said "our office flooded" waited behind three password resets. That's what call routing is about: the rules th

Posted on Oct 23 • Originally published at learn.ynteractive.com
A call or a ticket that lands with the wrong person costs everyone. The customer repeats themselves. Someone forwards it. Sometimes it sits in a shared inbox until Monday, and the one that said "our office flooded" waited behind three password resets.
That's what call routing is about: the rules that send each incoming request to the right place, a person, a team, a voicemail box or an automated answer, based on why they're calling, how urgent it is, the time of day or who is free.
Phone menus route by keypress ("press 1 for billing"). An AI router routes by what the person actually said. It sorts the request into a category, applies your rule, and passes it on with a short summary, so nobody asks the customer to start over.
This post builds the sorting half in n8n. The input can be the text of a voicemail, the summary an AI receptionist writes after a call, a web form or a support email. The output is a strict JSON object your workflow can trust.
Any office of 5 to 50 people where requests come in through one door and need to reach four or five different people: an accounting firm (billing, new clients, tax deadlines), a property manager (maintenance, leasing, emergencies), an agency (support, sales, invoices). If one person spends part of every morning reading and forwarding, that's the cost. Put your own minutes and hourly rate on it.
The tip I give everyone: list the five most common reasons people contact you, where each should go, and one fallback. For a made-up accounting office:
The other row is the most important one. It's the fallback to a person, and the router should use it whenever it isn't sure.
Structured output means the model has to answer in the shape you define. With OpenAI's API that's response_format with a JSON schema. With strict: true, every property must be listed in required and additionalProperties must be false:
{ "type": "json_schema", "json_schema": { "name": "route_request", "strict": true, "schema": { "type": "object", "properties": { "category": { "type": "string", "enum": [ "billing", "new_client", "deadline", "documents", "other" ] }, "urgent": { "type": "boolean" }, "summary": { "type": "string" }, "caller_name": { "type": "string" }, "callback_number": { "type": "string" }, "confidence": { "type": "string", "enum": [ "high", "low" ] } }, "required": [ "category", "urgent", "summary", "caller_name", "callback_number", "confidence" ], "additionalProperties": false } } } Enter fullscreen mode Exit fullscreen mode The enum on category is what makes the Switch node reliable later. The model can't invent a sixth category called "Billing question" with a capital B.
Before n8n, I check the prompt and schema with one curl call:
curl -s https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY " \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [ {"role": "system", "content": "You route incoming messages for a small accounting office. Pick exactly one category. Use only what the message says.
Set urgent to true only if it mentions a deadline, a notice from a tax agency, or a time limit within 7 days. If a name or number is not stated, use an empty string. If you are unsure of the category, choose other and set confidence to low.
The summary is one sentence for the person who will call back."}, {"role": "user", "content": "Hi, this is Alana Brooks, 555-0142. I got a letter from the state saying my sales tax return is late and I have 10 days to respond. Can someone call me?"} ], "response_format": { "type": "json_schema", "json_schema": { "name": "route_request", "strict": true, "schema": { "type": "object", "properties": { "category": {"type": "string", "enum": ["billing", "new_client", "deadline", "documents", "other"]}, "urgent": {"type": "boolean"}, "summary": {"type": "string"}, "caller_name": {"type": "string"}, "callback_number": {"type": "string"}, "confidence": {"type": "string", "enum": ["high", "low"]} }, "required": ["category", "urgent", "summary", "caller_name", "callback_number", "confidence"], "additionalProperties": false } } } }' Enter fullscreen mode Exit fullscreen mode The answer comes back as a JSON string in choices.message.content.
Run five or six real messages through it, including a vague one, and check that vague ones land in other.
Add an HTTP Request node after your trigger (a Webhook, a Gmail trigger, a form, whatever brings the text in). Method POST, URL https://api.openai.com/v1/chat/completions, authentication with your OpenAI credential, Send Body on, Body Content Type JSON, Specify Body Using JSON. In the JSON field, I build the body with JSON.stringify so quotes and line breaks in the message can't break it:
{{ JSON. stringify ( { model: "gpt-4o-mini", messages: [ { role: "system", content: "You route incoming messages for a small accounting office. Pick exactly one category. Use only what the message says.
Set urgent to true only if it mentions a deadline, a notice from a tax agency, or a time limit within 7 days. If a name or number is not stated, use an empty string. If you are unsure of the category, choose other and set confidence to low.
The summary is one sentence for the person who will call back." }, { role: "user", content: $json. message } ], response_format: { type: "json_schema", json_schema: { name: "route_request", strict: true, schema: { type: "object", properties: { category: { type: "string", enum: [ "billing", "new_client", "deadline", "documents", "other" ] }, urgent: { type: "boolean" }, summary: { type: "string" }, caller_name: { type: "string" }, callback_number: { type: "string" }, confidence: { type: "string", enum: [ "high", "low" ] } }, required: [ "category", "urgent", "summary", "caller_name", "callback_number", "confidence" ], additionalProperties: false } } } } ) }} Enter fullscreen mode Exit fullscreen mode Change $json.message to whatever field your trigger puts the text in.
Add a Code node, mode Run Once for Each Item. Its job is to turn the string into fields, and to send anything strange to a person instead of guessing:
const original = $ ( ' Webhook ' ). item. json; // rename to your trigger node let route; try { const msg = $json. choices?.[ 0 ]?. message; if ( msg?. refusal ) throw new Error ( ' Model refused: ' + msg. refusal ); route = JSON. parse ( msg. content ); } catch ( err ) { route = { category: ' other ', urgent: false, summary: ' Could not classify automatically. Please read the original message. ', caller_name: '', callback_number: '', confidence: ' low ', parse_error: String ( err. message || err ), }; } // Low confidence always goes to a person if ( route. confidence === ' low ' ) route. category = ' other '; return { json: {... route, original_message: original. message } }; Enter fullscreen mode Exit fullscreen mode Keeping original_message on the item matters. Whoever gets the request should be able to read what the customer actually said, not only the AI's one-line version.
Add a Switch node in Rules mode on {{ $json.category }}. One rule per category, each with Rename Output on so the canvas reads billing, new_client, deadline, documents. Under Options, add Fallback Output and set it to Extra Output. That's where other and anything unexpected go: the front desk.
For urgency, put an If node on the deadline and new_client outputs: {{ $json.urgent }}, Boolean, is true. True sends a text or a Slack message to whoever's on duty now; false sends an email for the morning.
Each output ends in a message to the right person with the summary, the callback number and the original message.
Call your own line, or send your own test messages, after every change and try each path.
If you're setting this up on n8n, you can try n8n here (affiliate link: I may earn a commission, at no cost to you).
The call routing glossary page covers the routing types (menu, time-based, skills-based) and links a free lesson where you build an after-hours voice agent that routes by reason and urgency. For the Switch and fallback mechanics on their own, there's IF and Switch in n8n: Route Work by Rules.
Practice it step by step, free: https://learn.ynteractive.com/content/glossary/call-routing