Ein n8n-Workflow, der das Lead-Routing in Salesforce von außen überwacht und einen Menschen alarmiert, bevor die Reps es merken. Drei Detektoren laufen alle 15 Minuten über eine einzige SOQL-Abfrage — Datensätze, die in einer Warteschlange feststecken, Datensätze mit deaktiviertem Owner und Datensätze jenseits des Erstkontakt-SLA — dazu eine Prüfung auf Round-Robin-Schieflage an jedem Werktagmorgen. Findings werden dedupliziert, lösen sich selbst auf, sobald die Bedingung wegfällt, und werden nach einer Regel, die der Workflow ausdrücklich benennt, zwischen PagerDuty und Slack aufgeteilt. Das Bundle unter apps/web/public/artifacts/routing-failure-watchdog-n8n/ liefert den vollständigen Export mit 20 Nodes plus eine _README.md zu Import, den zwei Credentials, der kompletten Tabelle der Umgebungsvariablen, einer fünfstufigen Verifikation und den laufenden Kosten.
Die zwei Uhren
Die meisten Speed-to-Lead-Dashboards melden eine einzige Zahl: die Zeit von der Lead-Erstellung bis zum Erstkontakt. Diese Zahl ist die Summe zweier unabhängiger Fehler, und genau dieses Summieren sorgt dafür, dass der resultierende Alarm die falsche Person weckt.
Routing-Latenz ist erstellt → zugewiesen. Sie bricht, wenn eine Zuweisungsregel nicht mehr greift, ein Node im Routing-Graph einen Fehler wirft, eine Queue volläuft oder ein Credential hinter einem Anreicherungsschritt mitten im Graph abläuft. Das ist ein Ops-Incident, die Bereitschaft kann ihn um 02:00 Uhr beheben, und in diesem Workflow löst er einen Anruf aus.
Response-Latenz ist zugewiesen → erster protokollierter Kontakt. Sie bricht, wenn ein Rep in einem Termin sitzt, im Urlaub ist oder die Queue ignoriert. Das ist ein Führungsgespräch, um 02:00 Uhr kann das niemand beheben, und in diesem Workflow geht es nach Slack und niemals an die Bereitschaft.
Parse Routing State berechnet beide getrennt und weigert sich, die erste zu erfinden. Hat ein Datensatz keinen Routing-Zeitstempel — kein Zuweisungsfeld, keine LeanData-Log-Zeile —, wird er als routedAtSource: 'none' markiert statt auf CreatedDate zurückzufallen, was für jeden Datensatz der Org eine Routing-Latenz von null melden und den Detektor dauerhaft stummschalten würde.
Die publizierte Evidenz dafür, warum das zählt, ist älter und dünner als die Folklore darum herum. Nachvollziehbar ist die Lead-Response-Management-Studie von 2007 (Oldroyd, mit InsideSales.com), die rund 15.000 Leads und 100.000 Anrufversuche auswertete und berichtete, dass die Chance, einen Lead zu erreichen, um etwa das 100-fache und die Chance, ihn zu qualifizieren, um etwa das 21-fache sinkt, wenn der Anruf nach 30 statt nach 5 Minuten rausgeht. Das sind fast zwanzig Jahre alte Daten aus sechs Unternehmen, und die viel zitierte Zahl, “78% kaufen bei dem, der zuerst antwortet”, geht auf keine publizierte Methodik zurück. Setzen Sie RESPONSE_SLA_MINUTES aus Ihrem eigenen Funnel, wenn Sie ihn messen können; die 5 Minuten sind eine Konvention, kein Gesetz.
Wann Sie das einsetzen
Setzen Sie es ein, wenn Routing automatisiert ist und ein Ausfall lautlos passiert. Diese Kombination ist die ganze Bedingung. Ein regelbasiertes Zuweisungs-Setup in Salesforce, ein LeanData-Graph oder ein Round-Robin-Pool teilen die Eigenschaft, dass beim Bruch nichts einen Fehler wirft: Der Datensatz hat weiterhin einen Owner, alle Dashboards rendern weiterhin, und das erste Signal ist ein Rep, der fragt, warum seine Queue leer ist — oder ein Interessent, der einem Wettbewerber antwortet.
Es passt zu Teams mit genug Volumen, dass eine kaputte Stunde teuer ist: ab rund 100 Inbound-Datensätzen pro Tag aufwärts, wo ein zweistündiger Ausfall 25 Leads sind und niemand die Warteschlange per Auge kontrolliert.
Es ergänzt die Inbound-Lead-Triage, die entscheidet, wohin Datensätze gehen, und den LeanData-Routing-MCP-Server, mit dem ein Agent beantworten kann, warum ein Lead genau dort gelandet ist — nachdem der Watchdog gemeldet hat, dass etwas falsch gelandet ist. Dieser Workflow ist der Alarm; jener ist die Untersuchung.
Wann Sie das NICHT einsetzen
Lassen Sie es, wenn ein Mensch Leads von Hand zuweist. Manuelle Zuweisung scheitert sichtbar: Jemand merkt, dass die Liste lang ist. Der Fehlerfall, den dieser Workflow fängt, ist ausdrücklich die Automatisierung, die still bricht.
Lassen Sie es, wenn Sie Ihre Parkplatz-Owner nicht benennen können. Der Unrouted-Detektor ist ein Mengentest gegen PARKING_OWNER_IDS, kein Null-Test — aus einem Grund, der weiter unten steht. Wenn niemand sagen kann, welche Queue die Datensätze hält, die keiner Regel entsprachen, muss diese Frage beantwortet werden, bevor sich irgendein Monitoring lohnt — und der Workflow wird Ihnen das bei jedem Durchlauf sagen, statt eine saubere Org zu melden.
Lassen Sie die 15-Minuten-Taktung, wenn Sie n8n Cloud Starter fahren. Eine Execution pro Auslösung sind 96 pro Tag, rund 2.950 pro Monat, gegen die 2.500 Executions, die Starter enthält (n8n-Preisseite, geprüft am 2026-08-12, 20 € pro Monat bei jährlicher Abrechnung). Entweder Pro mit 10.000 Executions, oder Self-Hosting, oder */30 mit bis zu 15 Minuten zusätzlicher Erkennungslatenz.
Warum der Unrouted-Detektor ein Mengentest ist
Lead.OwnerId ist niemals null. Greift keine Zuweisungsregel, übergibt Salesforce den Datensatz an den Default Lead Owner aus den Lead Settings. Es gibt kein Feld mit der Bedeutung “wurde nicht geroutet”: Ein nicht gerouteter und ein korrekt gerouteter Lead sind strukturell identische Datensätze, unterscheidbar nur daran, wem sie gehören.
Also führt PARKING_OWNER_IDS den Default-Owner plus jede Halte- und Catch-all-Queue, und der Detektor fragt, ob ein Datensatz in einer davon länger als UNROUTED_GRACE_MINUTES Geschäftszeit liegt. Verglichen wird auf dem 15-stelligen Id-Präfix, denn Admins kopieren 15-stellige Ids aus der Salesforce-Adressleiste, während die REST-API 18-stellige zurückgibt — beide Formen direkt zu vergleichen ist der häufigste Weg, auf dem ein sauber durchdachter Detektor dauerhaft auf nichts matcht.
Die Owner-Prüfung braucht noch ein Stück SOQL. OwnerId ist polymorph und kann auf einen User oder eine Group zeigen, deshalb ist Owner.IsActive für sich allein kein gültiger Feldpfad. Build Sweep Query nutzt TYPEOF Owner WHEN User THEN Id, Name, IsActive WHEN Group THEN Id, Name, Type END (SOQL, API-Version 46.0 und höher), um Aktivstatus für User-Owner und Queue-Typ für Queue-Owner in einem Roundtrip zu holen.
Setup
Importieren Sieapps/web/public/artifacts/routing-failure-watchdog-n8n/routing-failure-watchdog-n8n.json über Workflows → Import from File. Setzen Sie die Workflow-Zeitzone — beide Cron-Ausdrücke lesen sie.
Verdrahten Sie das Salesforce-Credential. Eine Connected App mit Client-Credentials-Grant und einem lesenden Run-As-Integrationsnutzer. Der Watchdog ist ein Job, keine Person, und jeder Salesforce-Aufruf im Export ist ein GET gegen /query/ oder /limits.
Setzen Sie PARKING_OWNER_IDS. Abschnitt 4 der _README.md erklärt, wo Sie die Ids finden. Nichts anderes im Setup zählt so viel.
Setzen Sie die beiden SLAs bewusst.ROUTING_SLA_MINUTES (Standard 2) alarmiert die Bereitschaft; RESPONSE_SLA_MINUTES (5) postet. Bestätigen Sie diese Trennung in Verifikationsschritt 4, bevor Sie aktivieren — eine Response-Verletzung, die an einem Dienstagnachmittag PagerDuty erreicht, erreicht es auch samstags um 02:00 Uhr.
Setzen Sie die Geschäftsuhr.BUSINESS_HOURS_TZ ist unabhängig von der Workflow-Zeitzone: Die eine entscheidet, wann der Workflow aufwacht, die andere, welche Minuten gegen ein SLA zählen.
Führen Sie die fünfstufige Verifikation aus der _README.md vor der Aktivierung aus. Schritt 1 ist der entscheidende: Zerstören Sie das Salesforce-Credential absichtlich und prüfen Sie, dass der Workflow alarmiert, statt einen sauberen Durchlauf zu melden.
Fehlerbilder und Guards
Null Zeilen liest sich wie Entwarnung. Ein Tippfehler im Filter, eine Rechteänderung am Integrationsnutzer oder ein abgelaufenes Credential erzeugen alle ein leeres Ergebnis, und jeder Detektor meldet daraufhin, dass nichts falsch ist. Guard: Parse Routing State gibt ein Nenner-Item sweep_summary aus, und Run Detectors liefert ein no_denominator-Finding mit Severity error — es ersetzt die gesamte Detektorausgabe —, wenn ein Durchlauf in Geschäftszeit nichts gesampelt hat. Die HTTP-Nodes laufen mit neverError und fullResponse, damit 401 und 403 als Daten ankommen und nicht als fehlgeschlagene Execution, die niemand liest.
Ein Massenimport sieht exakt aus wie ein Routing-Ausfall. Eine Marketingliste mit 40.000 Datensätzen parkt legitim alles für Minuten. Auf absolute Zahlen zu alarmieren macht jeden Import zum Incident. Guard: Das Unterscheidungsmerkmal ist Quellenkonzentration — ein echter Routing-Ausfall verteilt sich über LeadSource-Werte, ein Import nicht. Jenseits von STAMPEDE_MIN_BATCH (250) mit 90% der geparkten Datensätze auf einer einzigen Quelle sinkt das Finding auf info, und die Alarmierung wird unterdrückt, mit dem Grund im Text.
SLA-Arithmetik nach Wanduhr flutet den Montagmorgen. Ein Lead, der freitags um 18:55 Uhr eintrifft, hat montags um 09:00 Uhr kein 5-Minuten-SLA verletzt, aber die naive Rechnung sagt, er habe es um 3.725 Minuten verletzt. Guard: Verstrichene Zeit wird in Geschäftsminuten gegen BUSINESS_HOURS_TZ, BUSINESS_DAYS und BUSINESS_HOLIDAYS gerechnet, mit Intl.DateTimeFormat statt der Uhr des Workers, damit die Zeitzone des n8n-Hosts nicht ins Ergebnis durchsickert.
Eine Ursache, 900 Alarme. Ein kaputter Routing-Graph verletzt das SLA jedes Datensatzes, den er berührt. Guard: Findings werden nach Ursache gruppiert und tragen eine exakte Anzahl mit einer auf MAX_ITEMS_PER_ALERT (25) begrenzten Stichprobe; der dedup_key von PagerDuty faltet Wiederholungen zu einem Incident, und RENOTIFY_MINUTES (120) unterdrückt Nachalarmierung, sofern die Severity nicht eskaliert.
Incidents, die nie schließen. Eine Bedingung, die ohne expliziten Resolve wegfällt, lässt PagerDuty-Incidents offen, bis jemand einen abgestandenen Alarm quittiert — so wird ein Kanal stummgeschaltet. Guard: Alert Gate + Resolve sendet event_action: 'resolve' auf demselben dedup_key, sobald ein Key nicht mehr feuert — aber nur, wenn der Durchlauf, der ihn erneut erkannt hätte, tatsächlich erfolgreich war, damit ein Auth-Fehler kein echtes Backlog ins Schweigen auflöst. Resolves sind außerdem nach Quelle abgegrenzt, weil der 15-Minuten-Durchlauf und der 08:00-Fairness-Job dasselbe State-Objekt teilen und der Durchlauf sonst jede Fairness-Warnung eine Viertelstunde nach ihrem Auslösen schließen würde.
Der Watchdog frisst das API-Budget, von dem er abhängt. Guard: API Budget Gate liest DailyApiRequests aus /limits bei jedem Durchlauf und zieht sich oberhalb von SFDC_API_BUDGET_PCT (85) zurück. Der Eigenverbrauch ist nicht das Risiko — zwei Aufrufe pro Durchlauf sind 192 pro Tag gegen eine Enterprise-Zuteilung, die bei 100.000 Requests pro rollenden 24 Stunden startet, plus 1.000 je Nutzerlizenz. Das Risiko ist, der Aufruf zu sein, der eine ohnehin angespannte Org kippt.
Was das ersetzt
Der Status quo ist ein Report, den jemand einmal gebaut hat und den niemand öffnet. Er zeigt die geparkte Queue exakt an und sagt nichts in dem Moment, in dem die Queue zu wachsen beginnt — dem einzigen Moment, der zählt.
Die Audit Logs von LeanData selbst sind der nähere Vergleich und in dem, was sie tun, besser als dieser Workflow. Das Q2-2026-Release hat sie mit einem eingebetteten Assistenten neu gebaut, der Routing-Fragen in natürlicher Sprache beantwortet und dabei Node-Pfad und ausgewertete Bedingungen zitiert. Für einen Admin, der einen einzelnen Lead debuggt, ist das in dem enthalten, was Sie ohnehin zahlen, und schlägt alles hier. Was es nicht tut: von selbst aufwachen. Es beantwortet Fragen, und der Fehlerfall, den dieser Workflow adressiert, ist, dass niemand weiß, dass es eine Frage zu stellen gibt. Fahren Sie beides: Der Watchdog sagt Ihnen, dass etwas gebrochen ist, das Audit Log sagt Ihnen, warum.
Dasselbe als geplantes SQL über ein Warehouse zu bauen ist die legitime Alternative und gewinnt klar, sobald Salesforce-Daten dort ohnehin per Sync landen, weil Sie Historie, Backfill und billigere Aggregate bekommen. Die n8n-Variante gewinnt, wenn sie das nicht tun, denn sie liest das CRM direkt und braucht keine vorher aufgesetzte Ingestion-Pipeline — und Alarmierung, Deduplizierung und Auto-Resolve sind der Teil, den ein SELECT zu keinem Preis liefert.
# Routing Failure Watchdog — n8n bundle
Watches lead routing from the outside and pages a human before the reps notice. Three detectors run every 15 minutes over one Salesforce query; a fourth runs once a weekday morning over an aggregate query. Findings are deduplicated, auto-resolved when the condition clears, and split between PagerDuty (things the on-call can fix now) and Slack (things a manager reads at 09:00).
Files:
- `routing-failure-watchdog-n8n.json` — the complete workflow export, 20 nodes.
- `_README.md` — this file.
---
## 1. Import
1. n8n → **Workflows → Import from File** → `routing-failure-watchdog-n8n.json`.
2. Open **Settings** on the imported workflow. The export ships `executionOrder: v1` and `timezone: America/New_York`. **Change the timezone to your org's operating timezone** — both cron expressions read it, and the 08:00 fairness job's window boundary depends on it.
3. Do not activate yet. Section 5 verifies each branch first.
The two triggers are independent:
| Trigger | Cron | Timezone source |
|---|---|---|
| `Schedule — Routing Sweep` | `*/15 * * * *` | workflow settings |
| `Schedule — Fairness + Digest 08:00` | `0 8 * * 1-5` | workflow settings |
`BUSINESS_HOURS_TZ` is a *separate* setting from the workflow timezone. The workflow timezone decides when the flow wakes up; `BUSINESS_HOURS_TZ` decides which minutes count against an SLA. They are usually the same value and do not have to be — a US-East n8n instance watching a London sales team sets `America/New_York` on the workflow and `Europe/London` on the business clock.
---
## 2. Credentials
### 2a. Salesforce — `PLACEHOLDER_SALESFORCE_OAUTH2_CRED_ID`
Type: **OAuth2 API** (generic), used by four HTTP Request nodes.
Create a Connected App in Salesforce (**Setup → App Manager → New Connected App**) with OAuth enabled and the `api` and `refresh_token` scopes. The watchdog is a job, not a person, so use the **client credentials** flow and set a Run As user on the Connected App policy — an integration user whose profile can read the routed object, `User`, `Task`, and (if you enable it) `LeanData__Log__c`.
In n8n:
| Field | Value |
|---|---|
| Grant Type | Client Credentials |
| Access Token URL | `https://<your-domain>.my.salesforce.com/services/oauth2/token` |
| Client ID / Secret | from the Connected App |
| Authentication | Send as Body |
Give the Run As user **read-only** access. The workflow issues no writes anywhere — every Salesforce call in the export is a `GET` against `/query/` or `/limits`. If your integration user has write permissions, that is your org's choice and not something this flow needs.
### 2b. Slack — `PLACEHOLDER_SLACK_CRED_ID`
Type: **Slack API**. A bot token with `chat:write`, invited to both channels. Nothing else is required — the flow posts Block Kit and never reads a channel.
### 2c. PagerDuty — no n8n credential
PagerDuty's Events API v2 authenticates with the `routing_key` inside the request body, not a header, so there is no credential object. Create an **Events API v2** integration on the service that owns your RevOps on-call rotation and put its integration key in `PAGERDUTY_ROUTING_KEY`.
Treat that key as a secret: anything holding it can open incidents on your rotation.
---
## 3. Environment variables
Set these on the n8n instance (self-hosted: the container environment; n8n Cloud: **Settings → Variables**, referenced identically as `$env.NAME`).
### Required
| Variable | Example | What it does |
|---|---|---|
| `SFDC_INSTANCE_URL` | `https://acme.my.salesforce.com` | Base for every REST call. No trailing slash needed; the flow strips one. |
| `PARKING_OWNER_IDS` | `00G5f000004ABCD,0055f00000XYZAB` | **The unrouted detector does not work without this.** See section 4. |
| `SLACK_CHANNEL_ID` | `C08ABCDEF12` | Channel for posts and resolves. |
| `PAGERDUTY_ROUTING_KEY` | `R0ABCDEF...` | Events API v2 integration key. |
### Thresholds — set these deliberately
| Variable | Default | What it does |
|---|---|---|
| `UNROUTED_GRACE_MINUTES` | `10` | Business minutes a record may sit in a parking queue before it counts. Below your routing platform's own processing latency this generates pure noise. |
| `UNROUTED_BACKLOG_WARN` | `25` | Parked-record count that posts to Slack. |
| `UNROUTED_BACKLOG_PAGE` | `100` | Parked-record count that pages the on-call. |
| `ROUTING_SLA_MINUTES` | `2` | Business minutes from create to owner. Breaches **page** — this is the router failing. |
| `RESPONSE_SLA_MINUTES` | `5` | Business minutes from owner to first logged touch. Breaches **post** — this is a rep, and never pages. |
| `RENOTIFY_MINUTES` | `120` | How long a fired condition stays suppressed before re-alerting. |
| `MAX_ITEMS_PER_ALERT` | `25` | Sample size carried in an alert payload; the count is always exact. |
### Optional
| Variable | Default | What it does |
|---|---|---|
| `SFDC_API_VERSION` | `v67.0` | Summer '26. Older orgs can drop this; `TYPEOF` needs 46.0 or later. |
| `SFDC_API_BUDGET_PCT` | `85` | Org-wide API usage at which the watchdog stands down and says so. |
| `ROUTING_OBJECT` | `Lead` | `Case` and custom objects work if they carry `OwnerId`, `IsConverted`, and child `Tasks`. Drop `IsConverted` from `Build Sweep Query` for objects without it. |
| `LOOKBACK_HOURS` | `48` | Sweep window. Keep it under your LeanData log retention if you enable that branch. |
| `SWEEP_ROW_CAP` | `2000` | Row cap. Hitting it produces an explicit `truncated_sweep` finding rather than silently short counts. |
| `ROUTING_TS_FIELD` | *(unset)* | API name of a routed-at field on the object, if you stamp one. The most reliable routing clock available. |
| `LEANDATA_LOG_ENABLED` | `false` | Turn on to derive the routing clock from `LeanData__Log__c`. |
| `LEANDATA_RECORD_ID_FIELD` | `LeanData__Lead__c` | The Log field pointing back at the routed record. **Verify this against your own org** — see section 4. |
| `BUSINESS_HOURS_TZ` | `America/New_York` | IANA zone for the SLA clock. |
| `BUSINESS_HOURS_START_MIN` | `480` | Minutes past midnight, so 08:00. |
| `BUSINESS_HOURS_END_MIN` | `1080` | 18:00. |
| `BUSINESS_DAYS` | `1,2,3,4,5` | 0 = Sunday. |
| `BUSINESS_HOLIDAYS` | *(unset)* | `2026-11-26,2026-12-25`. Excluded from the SLA clock. |
| `STAMPEDE_MIN_BATCH` | `250` | Parked-record count above which a single-source cluster is read as a bulk import, not a failure. |
| `STAMPEDE_SUPPRESS_MINUTES` | `30` | Reported in the suppression message. |
| `RR_POOL_OWNER_IDS` | *(unset)* | Round-robin pool members. Fewer than two disables the fairness check. |
| `RR_WINDOW_DAYS` | `7` | Fairness window. |
| `RR_MIN_ASSIGNMENTS` | `40` | Pool-total floor below which share is not computed at all. |
| `RR_MIN_SHARE_RATIO` | `0.5` | Fraction of equal share below which a member is "starved". |
---
## 4. The two settings that decide whether this works
### `PARKING_OWNER_IDS`
`Lead.OwnerId` is never null. When no assignment rule matches, Salesforce assigns the record to the **Default Lead Owner** in **Setup → Lead Settings**. There is no field that says "this lead was not routed" — the record looks owned, by design.
So the unrouted detector is a membership test against a set you configure, not a null test. Populate it with:
1. The Default Lead Owner from Lead Settings (user or queue).
2. Every unsorted / holding / catch-all queue your assignment rules or routing graph can drop into.
3. Any queue a routing platform uses as its own error or fallback destination.
Get the Ids from **Setup → Queues** (the URL carries the `00G...` Id) or **Setup → Users** for a user owner. Both 15- and 18-character forms work; the flow compares on the 15-character prefix, because pasting a 15-character Id from the URL bar and comparing it to the 18-character Id the API returns is the most common way this detector ends up matching nothing and reporting a permanent all-clear.
Leaving this unset does not fail silently. The flow emits a `parking_unconfigured` finding on every sweep until you set it.
### `LEANDATA_RECORD_ID_FIELD`
Enable the LeanData branch only if you have no routed-at field of your own. LeanData writes one `LeanData__Log__c` row per record per trip through a deployed routing graph, and that row's `CreatedDate` is a usable routing timestamp. Two things to know:
- **Retention defaults to 90 days**, configurable under LeanData Dashboard → Admin → Settings → Reporting. Keep `LOOKBACK_HOURS` well inside it. A missing log row means "not routed" *or* "aged out", and the flow will not guess: it marks `routedAtSource: 'none'` and leaves the routing clock dark for that record rather than defaulting to `CreatedDate` and reporting a fake latency of zero.
- **The link field's API name varies** with what you route and how the package is configured. `LeanData__Lead__c` is the default here; confirm yours in **Setup → Object Manager → LeanData Log → Fields & Relationships**. A wrong value produces an `INVALID_FIELD` error, which the flow surfaces as a `leandata_log_query_failed` warning rather than swallowing.
`Query LeanData Log` executes on every sweep even when `LEANDATA_LOG_ENABLED=false` — the merge node ignores its output, but the API call is still spent. If you are not using this branch, **disable the node on the canvas** and save one call per sweep.
---
## 5. First-run verification
Run these in order. Steps 1–4 use the manual **Execute Workflow** button; step 5 requires activation.
**1 — Prove the credential and the budget gate.** Execute. `HTTP — Salesforce Limits` should return `statusCode: 200` and a body containing `DailyApiRequests`. `API Budget Gate` should emit `proceed: true` with a `usedPct` under your threshold. Now break it on purpose: change the Connected App secret in the n8n credential to garbage and execute again. The gate must emit `proceed: false, reason: 'auth_401'` and route to `Alert Gate + Resolve` — **not** an empty success. Restore the secret.
This is the step worth doing carefully. A watchdog that reports "nothing wrong" when it cannot see the system it watches is worse than no watchdog, because the silence is indistinguishable from health.
**2 — Prove the denominator.** Execute normally. `Parse Routing State` must emit one `sweep_summary` item with `sampled` greater than zero during business hours. Then temporarily set `LOOKBACK_HOURS=0` and execute again: `Run Detectors` must emit the `watchdog::no_denominator` finding at severity `error`, not an all-clear. Restore `LOOKBACK_HOURS`.
**3 — Prove the unrouted detector.** In a sandbox, create a lead that your assignment rules will not match, wait past `UNROUTED_GRACE_MINUTES`, and execute. You should get an `unrouted_backlog` finding at `warning` with that record in `sample`. If you get nothing, the Id-form problem in section 4 is the first thing to check: compare `PARKING_OWNER_IDS` against the `ownerId` value the flow reports on that record.
**4 — Prove the two clocks are separate.** Pick a routed record with no activity, older than `RESPONSE_SLA_MINUTES`. `Run Detectors` should report it under `response_latency` with `channel: 'post'` — and `Page or Post?` should route it to the Slack branch. Confirm no PagerDuty event fires. Rep slowness must never reach the on-call; if it does here, it will at 02:00.
**5 — Prove dedup and resolve.** Activate the workflow and leave the condition from step 3 in place. Over the next hour you should see exactly one Slack post, not four — `Alert Gate + Resolve` keys on `dedupKey` and suppresses for `RENOTIFY_MINUTES`. Then fix the record's owner. Within 15 minutes you should see a `Resolved: watchdog::unrouted_backlog` message, and any PagerDuty incident opened by that key should close itself.
Do this step activated. `$getWorkflowStaticData` persists on **production executions only**; manual runs will show no deduplication whatsoever, which is documented n8n behaviour and not a bug in this flow.
---
## 6. What this costs to run
**Salesforce API.** Two calls per sweep with the LeanData node disabled (limits + query), three with it enabled. At `*/15` that is 192 or 288 calls/day, plus 2 for the fairness job — call it 200–300 against an Enterprise allocation that starts at 100,000 requests per rolling 24 hours and rises by 1,000 per user licence. Under 0.3%. The budget gate exists for the case where something *else* in the org is at 95%, not for this flow's own consumption.
**n8n executions.** One execution per trigger firing: 96/day for the sweep plus 1 for the fairness job, roughly **2,950 per month**. n8n Cloud's Starter plan includes 2,500 executions/month, so a `*/15` cadence overruns Starter on this workflow alone. Either move to Pro (10,000 executions), self-host, or drop the sweep to `*/30` — which costs you up to 15 extra minutes of detection latency on the unrouted backlog and is a reasonable trade below a few hundred inbound leads a day.
**PagerDuty.** Events API v2 has no per-event charge; the cost is the on-call rotation you already pay for. The routing rules in this flow exist so that stays true — if everything paged, you would be paying in attention instead.
---
## 7. Known limits
1. **Polling, not events.** Detection latency is bounded by the sweep interval. Salesforce Platform Events or Change Data Capture would cut it to seconds and would also mean maintaining a subscriber; that is a different artifact.
2. **`LastActivityDate` is a date, not a datetime.** When no `Task` exists, the first-touch fallback resolves to midnight UTC of that day and the response clock is coarse. Records with a real logged `Task` are exact. If first-touch precision matters, require Tasks.
3. **The fairness check reads assignment counts, not routing intent.** A pool member at zero may have been deliberately removed. The finding names the possibilities rather than asserting a cause.
4. **Territory mis-assignment is not detected.** Checking that a record went to the *right* owner needs your territory map, which is org-specific. The inactive-owner check is the subset that is deterministic from CRM data alone.
5. **Not runtime-tested against a live org.** The SOQL, the endpoints, and the payload shapes are from current vendor documentation; the node logic has not been executed against production data. Section 5 exists to be run before you trust it.