To build AI workflow automation n8n, you need three things: a trigger, a model call, and a side effect. This tutorial wires a webhook to an OpenAI-compatible chat completion, classifies inbound support tickets, and posts high-priority items to Slack—no custom server required. You’ll get a copy-paste workflow JSON and the exact node configurations so you can ship this in an afternoon.
Prerequisites
- A running n8n instance.
docker run -it -p 5678:5678 n8nio/n8ngets you a local instance at http://localhost:5678. - An API key from an OpenAI-compatible LLM provider. If you want one endpoint that fronts 240+ models with automatic fallback when a provider is degraded, n4n.ai exposes a single OpenAI-compatible base URL you can paste into the credential.
- A Slack workspace and either an incoming webhook URL or a bot token with
chat:write. - Comfort editing JSON. n8n’s UI is fine, but the fastest way to reproduce a workflow is importing JSON.
Step 1: Create the trigger
Start a new workflow. Add a Webhook node, method POST, path ticket. Set Response Mode to On Received so the caller doesn’t hang while the LLM thinks. This node emits the raw request body under body.
{
"nodes": [
{
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"parameters": {
"httpMethod": "POST",
"path": "ticket",
"responseMode": "onReceived"
}
}
]
}
Test it with curl:
curl -X POST http://localhost:5678/webhook/ticket \
-H 'content-type: application/json' \
-d '{"text":"Your API is throwing 500s and we cannot process payments."}'
Expected node output:
{ "body": { "text": "Your API is throwing 500s and we cannot process payments." } }
Step 2: Call the LLM
Add an HTTP Request node (not the branded OpenAI node—HTTP Request avoids version drift and works with any OpenAI-compatible gateway). Configure:
- Method:
POST - URL:
https://api.n4n.ai/v1/chat/completions(orhttps://api.openai.com/v1/chat/completions) - Headers:
Authorization: Bearer <YOUR_KEY>,Content-Type: application/json - Body:
JSONwith the following:
{
"model": "anthropic/claude-3-haiku",
"messages": [
{
"role": "system",
"content": "You are a support triage agent. Return strict JSON with keys: sentiment (positive|neutral|negative), priority (low|medium|high), summary (string, max 12 words)."
},
{
"role": "user",
"content": "{{$json.body.text}}"
}
],
"response_format": { "type": "json_object" }
}
n8n interpolates {{$json.body.text}} from the Webhook. The response_format flag is honored by most modern chat endpoints; if your provider ignores it, enforce JSON in the system prompt and parse defensively. If you used n4n.ai, the same request works against any of its routed models; the gateway forwards cache-control hints and falls back on rate limits without changing node config.
Expected raw response (truncated):
{
"choices": [
{
"message": {
"content": "{\"sentiment\":\"negative\",\"priority\":\"high\",\"summary\":\"API 500s block payment processing\"}"
}
}
]
}
Step 3: Parse and validate
The model returns a JSON string inside content. Add a Function node to parse and fail loudly on bad shape:
const raw = items[0].json.choices[0].message.content;
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw new Error('LLM did not return JSON: ' + raw);
}
if (!['positive','neutral','negative'].includes(parsed.sentiment)) {
throw new Error('Bad sentiment: ' + parsed.sentiment);
}
if (!['low','medium','high'].includes(parsed.priority)) {
throw new Error('Bad priority: ' + parsed.priority);
}
return [{ json: parsed }];
After this node, the item looks like:
{
"sentiment": "negative",
"priority": "high",
"summary": "API 500s block payment processing"
}
That’s your checkpoint. If you see that, the hard part works. Do not skip validation—LLMs hallucinate keys under load.
Step 4: Conditional routing
Drop an IF node. Condition: priority equals high OR sentiment equals negative. n8n’s IF node supports multiple conditions with OR.
{
"name": "Route",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"options": { "caseSensitive": true },
"combinator": "or",
"conditions": [
{ "left": "={{$json.priority}}", "operator": "equals", "right": "high" },
{ "left": "={{$json.sentiment}}", "operator": "equals", "right": "negative" }
]
}
}
}
True branch goes to Slack; false branch can go to a low-priority logger or just end. In production, send the false branch to a weekly digest rather than dropping it—silent丢弃 of neutral tickets hides trends.
Step 5: Post to Slack
Add a Slack node on the true branch. Use Send a Message with:
- Channel:
#support-escalations - Text:
"🚨 New ticket: {{$json.priority}} / {{$json.sentiment}}\n{{$json.summary}}"
If you used an incoming webhook instead, swap in an HTTP Request node to https://hooks.slack.com/services/XXX with {"text": "..."}. Keep the message under 4KB; Slack rejects larger payloads.
Full workflow JSON
Import this via Workflows > Import from File. It chains all steps; replace credentials and Slack channel.
{
"nodes": [
{ "name": "Webhook", "type": "n8n-nodes-base.webhook", "parameters": { "httpMethod": "POST", "path": "ticket", "responseMode": "onReceived" }, "position": [0,0] },
{ "name": "LLM", "type": "n8n-nodes-base.httpRequest", "parameters": {
"method": "POST",
"url": "https://api.n4n.ai/v1/chat/completions",
"headers": { "Authorization": "Bearer <KEY>", "Content-Type": "application/json" },
"body": { "model": "anthropic/claude-3-haiku", "messages": [ {"role":"system","content":"You are a support triage agent. Return strict JSON with keys: sentiment (positive|neutral|negative), priority (low|medium|high), summary (string, max 12 words)."}, {"role":"user","content":"{{$json.body.text}}"} ], "response_format": {"type":"json_object"} },
"bodyContentType": "json"
}, "position": [250,0] },
{ "name": "Parse", "type": "n8n-nodes-base.function", "parameters": { "functionCode": "const raw=items[0].json.choices[0].message.content; let p=JSON.parse(raw); if(!['positive','neutral','negative'].includes(p.sentiment)) throw new Error('bad'); return [{json:p}];" }, "position": [500,0] },
{ "name": "Route", "type": "n8n-nodes-base.if", "parameters": { "conditions": { "combinator":"or", "conditions": [ {"left":"={{$json.priority}}","operator":"equals","right":"high"}, {"left":"={{$json.sentiment}}","operator":"equals","right":"negative"} ] } }, "position": [750,0] },
{ "name": "Slack", "type": "n8n-nodes-base.slack", "parameters": { "resource":"message", "operation":"post", "channel":"#support-escalations", "text":"🚨 New ticket: {{$json.priority}} / {{$json.sentiment}}\\n{{$json.summary}}" }, "position": [1000,0] }
],
"connections": {
"Webhook": { "main": [ [ { "node": "LLM", "type": "main", "index": 0 } ] ] },
"LLM": { "main": [ [ { "node": "Parse", "type": "main", "index": 0 } ] ] },
"Parse": { "main": [ [ { "node": "Route", "type": "main", "index": 0 } ] ] },
"Route": { "main": [ [ { "node": "Slack", "type": "main", "index": 0 } ] ] }
}
}
End-to-end test
Run the curl from Step 1. In n8n’s execution log you should see:
- Webhook received.
- LLM node 200 OK.
- Parse node outputs the three fields.
- Route evaluates true.
- Slack node posts:
🚨 New ticket: high / negative
API 500s block payment processing
Why this architecture
When you build AI workflow automation n8n, keep the LLM call stateless and isolated. The Webhook node should never call the model directly; a middle HTTP node lets you swap providers, add retries, and inspect raw payloads. Parsing in a Function node—rather than trusting the model—keeps the rest of the graph typed.
Avoid chaining multiple LLM calls in the same path without checkpointing. If the second call fails, you’ve lost the first’s output. Write intermediate results to a database node or at least to n8n’s execution data.
Extending the pattern
The same skeleton supports richer agents: replace the Parse node with a second LLM call that drafts a reply, then a third that translates to French based on {{$json.lang}}. Because n8n expressions are just JavaScript strings, you can parameterize the model name from the webhook body ({{$json.model}}) to A/B test providers.
If you build AI workflow automation n8n around a gateway that meters per-token usage, add a tiny Function node to extract usage.total_tokens and append it to a Google Sheet. That gives you cost per ticket without leaving the workflow.
Operational notes
- Timeouts: LLM calls can exceed n8n’s default 10s. Set the HTTP Request node timeout to 30s.
- Idempotency: Webhook retries will double-post. Add a Filter node keyed on a ticket ID if your source retries.
- Model drift: If you pin
gpt-4o-miniand OpenAI deprecates it, the workflow breaks. Using a gateway that honors client routing directives means you can switch model by changing one field without touching nodes. - Secrets: Store the API key in n8n Credentials, not inline in the JSON you commit to Git.
That’s the whole pattern. Once you can build AI workflow automation n8n with a webhook, a model call, and a branch, you can extend it to database writes, email, or multi-step agent loops. The workflow above is production-shaped; strip the placeholders and point it at your own queue.