Lab 06 — n8n Automations Calling AI Services
n8n 自動化串接AI服務 · Hands-on track: AI Infrastructure for Real Business Problems
Code:
labs/06/· Uses:data/reviews.csv,data/tickets.csv,data/leads.csv,docs/wholesale.md
Business Problem — 商業問題
Three streams of work arrive at Kettle & Kiln every day, and today someone reads all of them by hand:
- Reviews (20 in the sample, including a prompt-injection attempt R-0013 and a Traditional Chinese review R-0019). Negative reviews about damage need a fast, human-approved reply.
- Support tickets (T-501…T-512). A fire or injury report must reach the manager now, not after the weekend.
- Wholesale leads (L-01…L-07). Cafés asking for 200 units should get a reply within one business day; spam and unsupported countries should be archived.
The owner already uses n8n for order notifications. She wants AI to slot into those workflows without n8n holding the prompts or business rules. Our split:
- n8n owns the plumbing: webhooks, retries, routing, Slack.
- A small FastAPI service owns the AI and the rules, so they can be tested.
Architecture — 系統架構
flowchart LR
SRC[Shopify / review app / form] -->|POST /webhook/review-received| N8N
subgraph N8N["n8n :5678 (workflow per stream)"]
W[Webhook] --> H[HTTP Request<br/>X-API-Key, retry ×3] --> LOG[HTTP: /v1/events] --> IF{IF needs_reply / urgent / hot}
IF -- true --> SL[Slack incoming webhook<br/>onError: continue]
IF -- false --> RS[Respond to Webhook]
SL --> RS
end
H --> SVC[ai-services :8006<br/>/v1/reviews/analyze<br/>/v1/tickets/triage<br/>/v1/leads/score]
SVC --> LLM[llmkit → model]
SVC --> DB[(SQLite: idempotency cache + events)]
What lives in code, not prompts (ai_services/logic.py):
- Reviews:
urgencyis computed: rating ≤ 2 plus damage, or a safety keyword →high.needs_replymeans rating ≤ 3 or damage.- If an injection is suspected, the reply draft is blanked so it can never be auto-posted.
- Tickets:
- A safety keyword overrides the model's label to
safety. - Priority and route come from a fixed table (safety → urgent → manager; wholesale → sales …).
- Emails are stripped from summaries.
- A safety keyword overrides the model's label to
- Leads: tiering follows
docs/wholesale.md:- spam or unsupported country →
not_a_lead - consumer → route to support
- ≥ 50 units →
hot - otherwise
warm
- spam or unsupported country →
- Idempotency: n8n retries. The same payload (hash of endpoint + body) returns the cached result, so a retry never double-alerts.
- Auth:
X-API-Key, compared in constant time (hmac.compare_digest).
Prerequisites — 先決條件
- Labs 01–05. Docker + Compose (recommended for n8n), or Node.js 24 for
npx n8n/npm i n8n(n8n 2.x needs a recent Node; check n8n's docs for the supported version). - Optional: a Slack incoming webhook URL (Slack app → Incoming Webhooks). Without it the Slack node fails softly and the flow continues.
Step-by-Step Setup — 逐步設定
cd hands-on/labs/06
cp .env.example .env
python -c "import secrets;print(secrets.token_urlsafe(32))" # → SERVICE_API_KEY
python -c "import secrets;print(secrets.token_urlsafe(32))" # → N8N_ENCRYPTION_KEY (back it up!)
python n8n/build_workflows.py # (re)generates n8n/workflows/*.json
docker compose up -d --build # ai-services + n8n
docker compose --profile tools run --rm n8n-import # imports the 3 workflows
Open http://localhost:5678, create the owner account, then activate (n8n 2.x: Publish) the three workflows. Their production webhook URLs are:
POST http://localhost:5678/webhook/review-received
POST http://localhost:5678/webhook/ticket-received
POST http://localhost:5678/webhook/lead-received
Replay the sample data:
uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ../common -r requirements.txt
python replay.py --target n8n # every review, ticket, lead through n8n
python replay.py --target services # straight to ai-services (debug without n8n)
curl -s localhost:8006/v1/events -H "X-API-Key: $SERVICE_API_KEY" | python -m json.tool
Without Docker, run the service with uvicorn ai_services.main:app --port 8006. Start n8n with N8N_BLOCK_ENV_ACCESS_IN_NODE=false AI_SERVICES_URL=http://localhost:8006 SERVICE_API_KEY=... npx n8n start, and import the workflows with npx n8n import:workflow --separate --input=n8n/workflows.
Workflows as code. The JSON is generated by build_workflows.py (webhook v2, HTTP Request v4.2 with retries, IF v2, Respond to Webhook). Review diffs in git instead of clicking through the UI. If you edit in the UI, export and commit.
Secrets in n8n. For simplicity the workflows read $env.SERVICE_API_KEY, which requires N8N_BLOCK_ENV_ACCESS_IN_NODE=false. In production, prefer an n8n Header Auth credential on the HTTP node, which is encrypted with N8N_ENCRYPTION_KEY, and keep env access blocked.
How to Test — 測試方法
LLM_PROVIDER=mock pytest -q # 9 tests
The tests cover:
- auth required
- a negative damage review → high urgency
- an injection review gets no auto-reply
- the idempotent cache
- lead tiering
- the events log
- each workflow JSON is wired correctly (webhook → HTTP → log → IF → Slack/Respond), parametrised over the 3 files
What was run on the build box — 實測紀錄
- ✅
pytest: 9 passed. - ✅ Real n8n 2.43.7 (installed with npm on Node 24, no Docker) imported and published the 3 generated workflows. With
ai-servicesin mock-model mode,replay.py --target n8nsent all 39 samples (20 reviews + 12 tickets + 7 leads); every one returned HTTP 200 through the n8n webhooks, and 40 events were logged. - ✅ While testing on a real model, a review saying "Burned for ages… (a candle burn time)" was flagged as safety. The safety regex was tightened, and that case is now covered.
- ⚠️ Not completed: a full replay through n8n with a live model. With
qwen2.5:1.5bon the shared CPU, replay requests timed out (~120 s) while another lab's eval was competing for the same CPU. Use a GPU-served or cloud model, or raiseoptions.timeout. - ⚠️ Not run: n8n in Docker (no daemon), a real Slack webhook (no URL on the box; the node's
onError: continuepath was exercised).
Cost & Performance Notes — 成本與效能
- One model call per event (
chat_jsonwith up to 2 repairs). Idempotency means n8n retries are free after the first success. - n8n itself is light, but every execution is stored. Set
EXECUTIONS_DATA_PRUNE=trueandEXECUTIONS_DATA_MAX_AGE(hours) so the database doesn't grow forever. - If events arrive in bursts (a sale weekend), the model server is the bottleneck. n8n's HTTP node will retry; consider a queue (n8n queue mode with Redis) or batching low-priority reviews hourly.
Common Failures & Fixes — 常見錯誤與修正
| Symptom | Cause | Fix |
|---|---|---|
| Webhook 404 | workflow not active/published, or using /webhook-test/ URL without "listening" |
activate/publish; production path is /webhook/<path> |
access to env vars denied |
n8n blocks $env by default |
N8N_BLOCK_ENV_ACCESS_IN_NODE=false, or switch to a Header Auth credential |
| 401 from ai-services | key mismatch | same SERVICE_API_KEY in both containers; no quotes/whitespace |
n8n can't reach localhost:8006 |
inside Docker, localhost is n8n's container | use http://ai-services:8006 (compose service name) |
| Timeouts | slow model | raise HTTP node timeout, faster model, or async: respond 202 and post results back |
| Credentials unreadable after redeploy | N8N_ENCRYPTION_KEY changed/lost |
restore the original key; back it up with the volume |
| Duplicate Slack alerts | retries without idempotency | keep the service idempotent (it is); don't add random fields to the payload |
Stretch Exercise — 延伸練習
- Add a 4th workflow: Schedule Trigger (daily 8:00 America/Los_Angeles) →
GET /v1/events→ summarise yesterday's negative reviews into one Slack digest. - Add a human-approval step for review replies: Slack message with Approve/Reject buttons, or n8n's Wait node + form, and only then post the reply.
Deliverable Checklist — 交付清單
- [ ] Three workflows imported, active, and committed as JSON
- [ ]
replay.py --target n8noutput: all 200s - [ ] Urgent ticket (T-511 fire) produced a Slack alert (or the logged event)
- [ ] R-0013 (injection) has an empty
reply_draftandinjection_suspected: true - [ ]
SERVICE_API_KEYandN8N_ENCRYPTION_KEYstored in a password manager - [ ]
pytest -qpasses