n8n 教學:建立以 Strict JSON 分類來電及工單、再由 Switch 節點分流的 AI 路由器
n8n: An AI Call and Ticket Router That Returns Strict JSON (Category, Urgency, Summary) and a Switch Node That Acts on It
作者示範如何在 n8n 建立 AI 來電及工單路由流程,以 Strict JSON 輸出分類、緊急程度和摘要,再由 Switch 節點分流。流程設有五個分類,低信心度或 JSON 解析失敗時會轉交人工,並保留原始訊息;作者亦建議記錄分流結果供人工覆核,修改後重測各路徑。
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.
Who has this problem
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.
Before you build: write the rules in plain words
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:
| Category | Goes to | Notes |
|---|---|---|
| billing | Billing inbox | Invoices, payments, refunds |
| new_client | Owner | Anyone asking to become a client |
| deadline | Owner, flagged | Mentions a filing deadline or a notice from a tax agency |
| documents | Bookkeeper | Sending or asking for documents |
| other | Front desk | Anything else, or unclear |
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.
Step 1: the schema
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
}
}
}
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.
Step 2: test the call from a terminal
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
}
}
}
}'
The answer comes back as a JSON string in choices[0].message.content. Run five or six real messages through it, including a vague one, and check that vague ones land in other.
Step 3: the same call in n8n
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
}
}
}
}) }}
Change $json.message to whatever field your trigger puts the text in.
Step 4: parse it, and fail safe
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 } };
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.
Step 5: the Switch node
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.
What I would not let it do
- Answer the customer. This router sorts. It doesn't reply, quote prices or give advice. In my after-hours intake lesson the voice agent is told the same thing: when the caller asks a question that needs a professional, it routes the question instead of answering.
- Drop anything. Every path, including the fallback, ends with a person being told.
- Run unchecked for the first week. Log every routing decision to a sheet next to the original message, and read it. Fix the prompt where it's wrong.
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).
Try it free
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
來源:DEV Community · dev.to