# Advanced workflow — job completion handler (anonymized)

> A production-grade n8n workflow that closes field jobs over WhatsApp: 99 nodes, 136 connections, two LLM calls with tool-use, deterministic guards, a photo pipeline and reusable API "hubs". Masked copy of a system I built for a contractor; company, people, endpoints and credentials are placeholders and the workflow is intentionally non-functional.

**Files**

- [`completion-handler-demo.json`](completion-handler-demo.json) — importable into n8n to inspect (inactive; hosts point to `*.example.com`, credentials do not exist).
- [`graph.html`](graph.html) — interactive graph viewer: auto-layout, colour by node type, click a node to read its code or request. Live: [/advanced-workflow/graph.html](https://mining-operations-n8n-automation-2d.vercel.app/advanced-workflow/graph.html)
- [`graph.mmd`](graph.mmd) — the Mermaid source of the diagram below.

## What it does

A field technician messages the bot when a job is done (or can't be). The workflow:

1. **Intake & dedupe** — verifies the webhook, answers 200 immediately, extracts the message and drops retries via an idempotency RPC.
2. **Context loading** — identifies the technician by phone, loads today's jobs and the conversation state (locked job, history, stage).
3. **Photo-only path** — photos without text are stored, debounced (7 s) so a burst gets one acknowledgement, and, when the last job is already closed, the bot asks which job they belong to instead of guessing.
4. **LLM reasoning** — Claude classifies the message into one of six outcomes using tools (`confirm_job_match`, `capture_completion`, `capture_incomplete_*`, `escalate_issue`, `attach_photos`, …). A second, cheaper call drafts the English note and the customer message. Deterministic guards override the model when the history makes the answer unambiguous (late photos for a closed job, confirmation loops, checklist order).
5. **Routing & hubs** — one `Switch` routes by tool. Instead of duplicating API nodes per scenario, five reusable hubs (note, task, job update, outcome log, WhatsApp send) are fed by small `Prep *` nodes that build a `__p` payload with a `return_to` key; a `* Return` switch after each hub sends the flow back to the right scenario.
6. **S2: completion with additionals** — creates a quote job for the extra work with a DB claim/guard so a retry can never create it twice, waits for the new job id, cross-links both jobs with notes, and attaches the photos to both.
7. **Persistence & audit** — saves the conversation (one RPC), the pending photo rows and an audit log; a 10 s sweep then uploads any photos that arrived with the closing message.
8. **Photo upload pipeline** — resolves each media id, downloads the binary, creates the attachment record and uploads the file; successes are marked so nothing is uploaded twice. A re-entry guard makes the pipeline safe to reach from two origins.

## Diagram

Shapes: stadium = trigger/response · rectangle = HTTP request · rounded = Code · diamond = IF/Switch · hexagon = Wait. Edge labels are IF/Switch outputs.

```mermaid
flowchart LR
  classDef http fill:#e8f1ff,stroke:#2f6fdb,color:#0b2a5b;
  classDef code fill:#fff4e0,stroke:#d98a00,color:#5b3a00;
  classDef cond fill:#f3e8ff,stroke:#7c3aed,color:#3b0764;
  classDef wait fill:#e6fbf0,stroke:#0f9d58,color:#064e3b;
  classDef trig fill:#ffe8e8,stroke:#d93025,color:#5b0b0b;
  classDef other fill:#f1f5f9,stroke:#64748b,color:#0f172a;
  subgraph G0["Intake & dedupe"]
    direction LR
    n0(["Webhook Inbound"]):::trig
    n1[/"Respond 200 OK"/]:::trig
    n2{"Is New Message"}:::cond
    n58{"WA Verify?"}:::cond
    n59[/"WA Challenge"/]:::trig
    n66("Extract & Collect"):::code
    n92["Dedupe RPC"]:::http
  end
  subgraph G1["Context loading"]
    direction LR
    n3("Parse Config"):::code
    n4["Lookup Staff FSM"]:::http
    n5("Match Staff Phone"):::code
    n6{"Staff Found"}:::cond
    n7["Reply Unknown Number"]:::http
    n8["Get Staff Jobs"]:::http
    n9("Match Jobs to Staff"):::code
    n10{"Jobs Found"}:::cond
    n11["Reply No Jobs"]:::http
    n12["Load Conversation"]:::http
    n13("Prepare History"):::code
    n80["Load Config"]:::http
  end
  subgraph G2["Photo-only path"]
    direction LR
    n16{"Photo Only?"}:::cond
    n17("Photo Save Media"):::code
    n18["Photo Insert Pending"]:::http
    n19["Photo Save Conversation"]:::http
    n20{{"Photo Debounce Wait"}}:::wait
    n21["Photo Latest"]:::http
    n22("Photo Decide"):::code
    n23{"Photo Is Last?"}:::cond
    n24["Photo Ack Reply"]:::http
  end
  subgraph G3["LLM reasoning & routing"]
    direction LR
    n14("Build Claude Request"):::code
    n15["Get Local Now"]:::http
    n25["Call Claude Reason"]:::http
    n26("Parse Tool Response"):::code
    n27("Prepare for Routing"):::code
    n28{"Need Draft?"}:::cond
    n29["Call Claude Draft"]:::http
    n30("Merge Draft"):::code
    n31{"Route by Tool"}:::cond
    n65("Fallback Capture1"):::code
    n67("Build Params"):::code
    n98["Pending Photos Check"]:::http
  end
  subgraph G4["S2: completion with additionals (new job)"]
    direction LR
    n41{"S2 Check"}:::cond
    n42("S2 Prepare"):::code
    n43["S2 Create New Job"]:::http
    n44("S2 Get New UUID"):::code
    n45["S2 Charge New"]:::http
    n46{{"S2 Wait for ID"}}:::wait
    n47["S2 Fetch New Job"]:::http
    n48("S2 Extract Job Number"):::code
    n49["S2 Note Orig Link"]:::http
    n54["S2 Claim"]:::http
    n55("S2 Claim Decide"):::code
    n56{"S2 Already Created?"}:::cond
    n57["S2 Guard Update"]:::http
    n60["S2 Note New Link"]:::http
  end
  subgraph G5["Scenario-specific steps"]
    direction LR
    n32("S1 Badge Check"):::code
    n33{"S1 Has Badge?"}:::cond
    n34["Additionals FSM"]:::http
    n35["Additionals Reply"]:::http
    n36("S3 Prepare"):::code
    n37["S3 Schedule"]:::http
    n38["S3 Reply"]:::http
    n39["S4 Reply"]:::http
    n40["S5 Ack"]:::http
  end
  subgraph G6["Action hubs (reused API calls)"]
    direction LR
    n68["Log Outcome"]:::http
    n69["FSM Note"]:::http
    n70["WA Send"]:::http
    n71["FSM Task"]:::http
    n72["FSM Job Update"]:::http
    n74{"Log Return"}:::cond
    n75{"Note Return"}:::cond
    n76{"WA Return"}:::cond
    n77{"Task Return"}:::cond
    n78{"Job Return"}:::cond
    n93("Prep Note"):::code
    n94("Prep Task"):::code
    n95("Prep Log"):::code
    n96("Prep Job"):::code
    n97("Prep WA"):::code
  end
  subgraph G7["Persistence & audit"]
    direction LR
    n50("Prepare Save"):::code
    n51["Save Conversation"]:::http
    n52["Save Photos Normal"]:::http
    n53["Insert Audit Msgs"]:::http
    n64{{"Sweep Wait"}}:::wait
  end
  subgraph G8["Photo upload pipeline"]
    direction LR
    n61("Upload Decision"):::code
    n62{"Upload Photos?"}:::cond
    n63["Claim Orphan Photos"]:::http
    n73("lib Prep Targets"):::code
    n79{"Fotos Return"}:::cond
    n81("lib Ctx Fotos"):::code
    n82["lib Load Photos"]:::http
    n83("lib Count Photos"):::code
    n84{"lib Has Photos?"}:::cond
    n85["lib Resolve URL"]:::http
    n86["lib Download"]:::http
    n87["lib Att Create"]:::http
    n88("lib Prep Upload"):::code
    n89["lib Upload File"]:::http
    n90("lib Collect Uploaded"):::code
    n91["lib Mark"]:::http
  end
  n0 --> n58
  n0 --> n1
  n0 --> n66
  n2 -- "true" --> n80
  n3 --> n4
  n4 --> n5
  n5 --> n6
  n6 -- "true" --> n8
  n6 -- "false" --> n7
  n8 --> n9
  n9 --> n10
  n10 -- "true" --> n12
  n10 -- "false" --> n11
  n12 --> n13
  n13 --> n16
  n16 -- "true" --> n17
  n16 -- "false" --> n15
  n15 --> n98
  n17 --> n18
  n18 --> n19
  n19 --> n20
  n20 --> n21
  n21 --> n22
  n22 --> n23
  n23 -- "true" --> n24
  n14 --> n25
  n25 --> n26
  n26 --> n27
  n27 --> n28
  n28 -- "true" --> n29
  n28 -- "false" --> n67
  n29 --> n30
  n30 --> n67
  n31 -- "confirm" --> n97
  n31 -- "completion" --> n41
  n31 -- "additionals" --> n34
  n31 -- "photos" --> n97
  n31 -- "inc_clear" --> n93
  n31 -- "inc_unclear" --> n93
  n31 -- "escalate" --> n94
  n31 -- "msg_staff" --> n97
  n31 -- "attach" --> n97
  n31 -- "no_action" --> n93
  n31 -- "fallback" --> n65
  n41 -- "true" --> n42
  n41 -- "false" --> n93
  n42 --> n54
  n43 --> n44
  n44 --> n45
  n45 --> n57
  n46 --> n47
  n47 --> n48
  n48 --> n49
  n49 --> n60
  n32 --> n33
  n33 -- "true" --> n94
  n33 -- "false" --> n95
  n34 --> n35
  n35 --> n50
  n36 --> n94
  n37 --> n95
  n38 --> n97
  n39 --> n97
  n40 --> n97
  n50 --> n51
  n51 --> n52
  n52 --> n53
  n54 --> n55
  n55 --> n56
  n56 -- "true" --> n43
  n57 --> n46
  n58 -- "true" --> n59
  n58 -- "false" --> n1
  n60 --> n81
  n53 --> n64
  n24 --> n61
  n61 --> n62
  n62 -- "true" --> n63
  n63 --> n81
  n64 --> n61
  n65 --> n97
  n66 --> n92
  n67 --> n31
  n68 --> n74
  n74 -- "s1" --> n97
  n74 -- "s3" --> n38
  n74 -- "s4" --> n39
  n74 -- "s5" --> n40
  n74 -- "s2" --> n97
  n69 --> n75
  n75 -- "s1" --> n96
  n75 -- "s3" --> n36
  n75 -- "s4" --> n94
  n75 -- "s5" --> n95
  n75 -- "s6" --> n97
  n70 --> n76
  n76 -- "confirm" --> n50
  n76 -- "photos" --> n50
  n76 -- "msg" --> n50
  n76 -- "s1" --> n50
  n76 -- "s6" --> n50
  n76 -- "s3" --> n50
  n76 -- "s4" --> n50
  n76 -- "s5" --> n50
  n76 -- "s2" --> n50
  n76 -- "attach" --> n50
  n71 --> n77
  n77 -- "s1" --> n95
  n77 -- "s3" --> n37
  n77 -- "s4" --> n95
  n77 -- "s5" --> n93
  n72 --> n78
  n78 -- "s1" --> n32
  n78 -- "s2" --> n95
  n73 --> n85
  n85 --> n86
  n86 --> n87
  n79 -- "s2" --> n96
  n87 --> n88
  n89 --> n90
  n91 --> n79
  n88 --> n89
  n80 --> n3
  n81 --> n82
  n82 --> n83
  n83 --> n84
  n84 -- "true" --> n73
  n84 -- "false" --> n79
  n90 --> n91
  n92 --> n2
  n93 --> n69
  n94 --> n71
  n95 --> n68
  n96 --> n72
  n97 --> n70
  n98 --> n14
```

## By the numbers

| Stage | Nodes |
|---|---|
| Intake & dedupe | 7 |
| Context loading | 12 |
| Photo-only path | 9 |
| LLM reasoning & routing | 12 |
| S2: completion with additionals (new job) | 14 |
| Scenario-specific steps | 9 |
| Action hubs (reused API calls) | 15 |
| Persistence & audit | 5 |
| Photo upload pipeline | 16 |

| Node type | Count |
|---|---|
| `httpRequest` | 43 |
| `code` | 31 |
| `if` | 12 |
| `switch` | 7 |
| `wait` | 3 |
| `respondToWebhook` | 2 |
| `webhook` | 1 |

## Patterns worth copying

- **Hubs with a return key** — `Prep X` builds the payload and `return_to`; the hub is a plain HTTP node; `X Return` is a `Switch` on `return_to`. Five hubs serve seven scenarios.
- **Guards after the model, not instead of it** — the LLM decides; small Code nodes rewrite its tool call only when the conversation history makes the decision unambiguous, and every override is logged.
- **Debounce with the database** — a burst of photos is acknowledged once by writing each row, waiting 7 s and checking whether the row is still the latest for that phone.
- **Idempotent side effects** — webhook dedupe by message id; a claim table before creating a job; `uploaded` flags before uploading files; `ignore-duplicates` on inserts.
- **Honest acknowledgements** — the bot never tells the technician something was saved or attached unless the step that does it ran.

## Anonymization

Generated from the real workflow by a script that replaces company, assistant, people, places, sector, endpoints, credential types and IDs, tokens and phone numbers, regenerates node IDs, and then fails if any term from a blacklist survives. Technology names (WhatsApp, n8n, Anthropic API) are kept on purpose.
