Skip to content
Zarif Automates

Build an n8n Ticket-Triage Workflow and Test Its Failure Paths

ZarifZarif
Published Updated

A workflow that labels one clean support ticket is easy to show off. The real test is what happens when the same event arrives twice, the ticket doesn't exist, or the model sends back something your app can't use.

This lesson gives you an n8n workflow with those cases already built in. You import it, run it, and watch eight made-up events go through it. Each one gets checked, matched to a ticket, given a canned model answer, and either held for a person to review or rejected. Two end up waiting for review. Six are rejected, each with a reason. Nothing sends a message or changes a ticket.

Download the inactive workflow JSON or get the workflow, input fixtures and recorded results together.

The workflow was executed locally with n8n 2.39.7 and Node 24.19.0 on September 17, 2026. It runs on the real n8n engine with JavaScript Code nodes. The model's answers are written in advance, not generated, so the results show how the workflow routes things, not how good any model is.

1. Import it without publishing

If you already use n8n's editor, create a new workflow, open its menu and choose Import from File. Select the downloaded JSON, look over the ten nodes, and leave the workflow unpublished. The file has active: false, one Manual Trigger, no credentials, no HTTP Request node and no schedule, so it can't reach anything outside n8n. The official import guide explains the import options.

The Manual Trigger means it only runs when you click. Select Execute Workflow, then open the three nodes at the end of the flow: Review items, Rejected events, and Rejected proposals. They should hold two, two and four items.

To run it from the command line instead, make a new empty folder on a machine with Node 24 installed. Save the JSON there as ticket-triage.json. These macOS/Linux commands install the tested n8n version and keep its data in its own folder:

node --version
npm init -y
npm install n8n@2.39.7
export N8N_USER_FOLDER="$PWD/n8n-test-state"
export N8N_DIAGNOSTICS_ENABLED=false
export N8N_VERSION_NOTIFICATIONS_ENABLED=false
export N8N_TEMPLATES_ENABLED=false
export N8N_PERSONALIZATION_ENABLED=false
export N8N_RUNNERS_BROKER_LISTEN_ADDRESS=127.0.0.1
export N8N_RUNNERS_BROKER_PORT=15689
npx n8n import:workflow --input=ticket-triage.json
npx n8n execute --id=ZarifTicketLab01 --rawOutput

Keep that separate folder for this exercise. If you import into an n8n setup that already has a workflow with the same ID, the import can overwrite it. If port 15689 is taken, pick another local port. You don't need a public server or tunnel.

The Server CLI documentation covers importing a saved workflow and running it by ID. The tested release rejects the older execute --file shortcut, so import first. You may see startup messages before the JSON results. The recorded run also logged that the optional Python runner was unavailable. That's expected here. The JS Task Runner started and ran the Code nodes.

2. Follow the data through the nodes

Manual test → Synthetic events → Validate events → Event valid?
                                                   ├─ false → Rejected events
                                                   └─ true  → Lookup and model fixture
                                                              → Validate proposals
                                                              → Proposal valid?
                                                                 ├─ true  → Review items
                                                                 └─ false → Rejected proposals

Every Code node is set to Run Once for All Items, so it sees the whole batch at once. In the Code node's all-items mode, $input.all() hands you every input item. Each node returns objects with json data and uses pairedItem to remember which input each output came from.

NodeConfiguration to inspect
Synthetic eventsEight explicit input objects, including failures
Validate eventsRequired field types; integer version; duplicate IDs within this batch
Event valid?Strict Boolean condition on event_valid; false output goes to Rejected events
Lookup and model fixtureTwo synthetic tickets at version 3; controlled good, invalid and malformed model output
Validate proposalsTicket exists; version matches; JSON has exactly one allowed label
Proposal valid?Strict Boolean condition on proposal_valid
Review itemsEmits a proposal with state: awaiting_review and applied: false
Rejected events / proposalsRetain event ID and reason; never mark the change applied

The lookup and model step is a fixture on purpose, meaning it returns answers written in advance. You can see every input that reaches it and make it fail on demand, without paying for a model call or touching another system.

3. Read the eight results

The event evt-1 appears twice. The first copy is accepted for review. The second is rejected as a repeat in the same run. Here's what the recorded run produced:

EventCaseResult
evt-1, first copyValid billing proposalAwaiting review
evt-1, second copyRepeated event IDduplicate_event_in_batch
evt-2Missing ticketticket_not_found
evt-3Label delete-accountinvalid_label_contract
evt-4Version supplied as a stringinvalid_event
evt-5Valid technical proposalAwaiting review
evt-6Text that is not JSONinvalid_json
evt-7Version 2 against current version 3stale_version

An accepted review item looks like this:

{
  "event_id": "evt-1",
  "ticket_id": "DEMO-42",
  "expected_version": 3,
  "proposed_label": "billing",
  "state": "awaiting_review",
  "applied": false
}

That's eight input events, two review items, six rejections, and zero applied changes. Those are counts for this run, not an accuracy score. A workflow can route every item exactly as designed while the model behind it still picks the wrong labels.

4. Change one failure and rerun

Open Synthetic events and change evt-3 from the invalid-label fixture to the default case by removing its fixture field. Execute the workflow again. You should now see three review items and five rejections.

Next, put evt-3 back and change evt-7 to expected_version: 3. Its stale-version rejection should turn into a review item. Keep a copy of the original export so you can get back to the eight-case starting point.

Then open Validate proposals. It parses the model's JSON and checks the label against the allowed list. That's what decides whether an answer moves forward. If you wired the model straight to a node that writes to your ticket system, nothing would check it. Keep this step in place even after everything works on clean input.

5. Know what must change for a real integration

The repeat check here is a JavaScript Set that only lives for one run. Run the workflow again and it starts empty, so the same review items come back. It is not durable deduplication across executions or restarts.

Picture a real ticket system sending the same event twice, a few seconds apart, to two copies of your workflow. Both check, both see nothing, both act. To stop that, save each event ID in a database that rejects duplicates, and have each run claim the event in a single step before doing any work. Test it by sending the same event twice at the same time.

The review items also only exist in the run's output. Nobody is watching a queue. To take this further, send them somewhere that lasts and record the exact proposal, the ticket version, the decision, who reviewed it and when it expires. Whatever applies an approved change should check the version again first, in case the ticket changed while it waited.

Swap the fixed lookup and model answers for real nodes only after you've decided which credentials they use and which records they can touch. Keep the same rules for what a valid label looks like, and run the failure cases again. Test the model's accuracy separately, with realistic data you're allowed to use. Passing these fixture cases says nothing about how a live model will do.

Last, add your provider's webhook verification and a limit on retries. n8n's error-handling guide covers error workflows, but a rejected input and an unexpected crash need different handling. And if a write to another system already went through but you lost the response, don't just retry it. You could make the change twice.

When you're done, you should have the imported workflow, the eight-case baseline, and one changed fixture with the new counts you expected. Keep it unpublished until you've designed and tested every connection to the outside. Then continue to the agent capstone, which connects the course's pieces into one app you can inspect.

Zarif Choudhury

Zarif Choudhury

Zarif builds AI agents and automation workflows. He writes about what keeps working once real people use it: who is worth reading, the new jobs AI is creating, and agent workflows you can check step by step.