Un workflow n8n qui surveille le routing des leads Salesforce de l’extérieur et alerte un humain avant que les reps ne s’en aperçoivent. Trois détecteurs tournent toutes les 15 minutes sur une seule requête SOQL — enregistrements bloqués dans une file d’attente, enregistrements dont le propriétaire est désactivé, et enregistrements au-delà du SLA de premier contact — plus une vérification du déséquilibre du round-robin chaque matin ouvré. Les résultats sont dédupliqués, se résolvent seuls quand la condition disparaît, et sont répartis entre PagerDuty et Slack selon une règle que le workflow énonce explicitement. Le bundle dans apps/web/public/artifacts/routing-failure-watchdog-n8n/ fournit l’export complet de 20 nœuds plus un _README.md couvrant l’import, les deux credentials, la table complète des variables d’environnement, une vérification en cinq étapes et ce que cela coûte à faire tourner.
Les deux horloges
La plupart des tableaux de bord speed-to-lead rapportent un seul chiffre : le temps entre la création du lead et le premier contact. Ce chiffre est la somme de deux défaillances indépendantes, et c’est précisément cette addition qui fait que l’alerte réveille la mauvaise personne.
La latence de routing va de créé à attribué. Elle casse quand une règle d’attribution cesse de correspondre, qu’un nœud du graphe de routing lève une erreur, qu’une file sature ou qu’un credential derrière une étape d’enrichissement expire en plein graphe. C’est un incident ops, l’astreinte peut le corriger à 02:00, et dans ce workflow il déclenche un appel.
La latence de réponse va d’attribué au premier contact enregistré. Elle casse quand un rep est en réunion, en congés ou ignore sa file. C’est une conversation de management, personne ne corrige cela à 02:00, et dans ce workflow cela part sur Slack et n’appelle jamais l’astreinte.
Parse Routing State calcule les deux séparément et refuse d’inventer la première. Si un enregistrement n’a pas d’horodatage de routing — ni champ d’attribution, ni ligne de log LeanData — il est marqué routedAtSource: 'none' plutôt que rabattu sur CreatedDate, ce qui rapporterait une latence de routing nulle pour tous les enregistrements de l’org et rendrait le détecteur muet à jamais.
Les preuves publiées sur l’importance de tout cela sont plus anciennes et plus minces que le folklore qui les entoure. Le résultat traçable est l’étude Lead Response Management de 2007 (Oldroyd, avec InsideSales.com), qui a analysé environ 15 000 leads et 100 000 tentatives d’appel et rapporté que les chances de joindre un lead chutent d’environ 100x et celles de le qualifier d’environ 21x quand l’appel part à 30 minutes plutôt qu’à 5. Ce sont des données vieilles de près de vingt ans, issues de six entreprises, et le chiffre très repris selon lequel « 78% achètent chez celui qui répond en premier » ne renvoie à aucune méthodologie publiée. Fixez RESPONSE_SLA_MINUTES à partir de votre propre funnel si vous savez le mesurer ; les 5 minutes par défaut sont une convention, pas une loi.
Quand l’utiliser
Utilisez-le quand le routing est automatisé et qu’une panne est silencieuse. Cette combinaison est toute la condition. Un dispositif d’attribution par règles dans Salesforce, un graphe LeanData ou un pool de round-robin partagent la même propriété : quand ils cassent, rien ne lève d’erreur. L’enregistrement a toujours un propriétaire, tous les tableaux de bord s’affichent toujours, et le premier signal est un rep qui demande pourquoi sa file s’est vidée, ou un prospect qui répond à un concurrent.
Cela convient aux équipes qui traitent déjà assez de volume pour qu’une heure cassée coûte cher : à partir d’environ 100 enregistrements inbound par jour, où une panne de deux heures représente 25 leads et où personne ne surveille la file d’attente à l’œil.
Cela se combine avec le triage des leads inbound, qui décide où vont les enregistrements, et avec le serveur MCP de routing LeanData, qui permet à un agent de répondre à « pourquoi ce lead a-t-il atterri ici ? » après que le watchdog vous a signalé que quelque chose a mal atterri. Ce workflow est l’alarme ; l’autre est l’enquête.
Quand NE PAS l’utiliser
Passez si un humain attribue les leads à la main. L’attribution manuelle échoue de façon visible : quelqu’un remarque que la liste est longue. La défaillance que ce workflow attrape est précisément l’automatisation qui casse sans bruit.
Passez si vous ne savez pas nommer vos files de parking. Le détecteur de non-routés est un test d’appartenance à PARKING_OWNER_IDS, pas un test de nullité, pour une raison exposée plus bas. Si personne ne sait dire quelle file contient les enregistrements qui n’ont correspondu à aucune règle, il faut répondre à cette question avant que le moindre monitoring ait un intérêt — et le workflow continuera de vous le dire à chaque balayage plutôt que de rapporter une org propre.
Passez la cadence de 15 minutes si vous êtes sur n8n Cloud Starter. Une exécution par déclenchement, cela fait 96 par jour, environ 2 950 par mois, contre les 2 500 exécutions incluses dans Starter (page de tarifs n8n, vérifiée le 2026-08-12, 20 € par mois en facturation annuelle). Soit Pro avec 10 000 exécutions, soit l’auto-hébergement, soit */30 en acceptant jusqu’à 15 minutes de latence de détection supplémentaire.
Pourquoi le détecteur de non-routés est un test d’appartenance
Lead.OwnerId n’est jamais nul. Quand aucune règle d’attribution ne correspond, Salesforce confie l’enregistrement au Default Lead Owner configuré dans Lead Settings. Il n’existe aucun champ signifiant « ceci n’a pas été routé » : un lead non routé et un lead correctement routé sont des enregistrements structurellement identiques, distinguables uniquement par leur propriétaire.
PARKING_OWNER_IDS porte donc le propriétaire par défaut plus chaque file de rétention et de catch-all, et le détecteur demande si un enregistrement y séjourne au-delà de UNROUTED_GRACE_MINUTES de temps ouvré. La comparaison porte sur le préfixe de 15 caractères de l’Id, parce que les admins collent des Ids de 15 caractères depuis la barre d’adresse Salesforce alors que l’API REST en renvoie de 18 — comparer les deux formes directement est la façon la plus courante dont un détecteur bien raisonné finit par ne rien matcher, indéfiniment.
La vérification du propriétaire réclame un morceau de SOQL supplémentaire. OwnerId est polymorphe et peut pointer vers un User ou un Group, si bien que Owner.IsActive n’est pas un chemin de champ valide à lui seul. Build Sweep Query utilise TYPEOF Owner WHEN User THEN Id, Name, IsActive WHEN Group THEN Id, Name, Type END (SOQL, API version 46.0 et suivantes) pour obtenir le statut d’activité des propriétaires utilisateurs et le type de file des propriétaires files en un seul aller-retour.
Mise en place
Importezapps/web/public/artifacts/routing-failure-watchdog-n8n/routing-failure-watchdog-n8n.json via Workflows → Import from File. Réglez le fuseau du workflow — les deux expressions cron le lisent.
Branchez le credential Salesforce. Une Connected App en grant client credentials avec un utilisateur d’intégration Run As en lecture seule. Le watchdog est un job, pas une personne, et chaque appel Salesforce de l’export est un GET sur /query/ ou /limits.
Renseignez PARKING_OWNER_IDS. La section 4 du _README.md explique où trouver les Ids. Rien d’autre dans la mise en place ne compte autant.
Fixez les deux SLA délibérément.ROUTING_SLA_MINUTES (2 par défaut) appelle l’astreinte ; RESPONSE_SLA_MINUTES (5) publie. Confirmez cette séparation à l’étape 4 de la vérification avant d’activer — une violation de réponse qui atteint PagerDuty un mardi après-midi l’atteindra aussi à 02:00 un samedi.
Réglez l’horloge ouvrée.BUSINESS_HOURS_TZ est distinct du fuseau du workflow : l’un décide quand le workflow se réveille, l’autre quelles minutes comptent face à un SLA.
Exécutez la vérification en cinq étapes du _README.md avant d’activer. L’étape 1 est celle qui compte : cassez le credential Salesforce exprès et confirmez que le workflow alerte au lieu de rapporter un balayage propre.
Modes de défaillance et garde-fous
Zéro ligne se lit comme un tout va bien. Une faute de frappe dans un filtre, un changement de droits sur l’utilisateur d’intégration ou un credential expiré produisent tous un résultat vide, et chaque détecteur rapporte alors que rien ne va mal. Garde-fou : Parse Routing State émet un item dénominateur sweep_summary, et Run Detectors renvoie un résultat no_denominator en sévérité error — remplaçant toute la sortie des détecteurs — quand un balayage en horaires ouvrés n’a rien échantillonné. Les nœuds HTTP tournent avec neverError et fullResponse pour que les 401 et 403 arrivent comme des données et non comme une exécution en échec que personne ne lit.
Un import de masse ressemble exactement à une panne de routing. Une liste marketing de 40 000 enregistrements en gare légitimement tout pendant quelques minutes. Alerter sur le nombre absolu transforme chaque import en incident. Garde-fou : le discriminant est la concentration de source — une vraie panne de routing s’étale sur les valeurs de LeadSource, un import non. Au-delà de STAMPEDE_MIN_BATCH (250) avec 90% des enregistrements garés partageant une seule source, le résultat retombe en info et l’appel est supprimé, le motif étant écrit dans le message.
L’arithmétique de SLA en heure murale inonde le lundi matin. Un lead qui arrive vendredi à 18:55 n’a pas violé un SLA de 5 minutes lundi à 09:00, mais le calcul naïf dit qu’il l’a violé de 3 725 minutes. Garde-fou : le temps écoulé est calculé en minutes ouvrées face à BUSINESS_HOURS_TZ, BUSINESS_DAYS et BUSINESS_HOLIDAYS, en utilisant Intl.DateTimeFormat plutôt que l’horloge du worker, pour que le fuseau de l’hôte n8n ne fuite pas dans le résultat.
Une cause, 900 alertes. Un graphe de routing cassé met en défaut chaque enregistrement qu’il touche. Garde-fou : les résultats sont groupés par cause et portent un compte exact avec un échantillon plafonné à MAX_ITEMS_PER_ALERT (25) ; la dedup_key PagerDuty replie les répétitions en un seul incident, et RENOTIFY_MINUTES (120) supprime la relance sauf escalade de sévérité.
Des incidents qui ne se ferment jamais. Une condition qui disparaît sans resolve explicite laisse des incidents PagerDuty ouverts jusqu’à ce que quelqu’un acquitte un appel périmé, et c’est ainsi qu’un canal finit en sourdine. Garde-fou : Alert Gate + Resolve envoie event_action: 'resolve' sur la même dedup_key dès qu’une clé cesse de se déclencher — mais uniquement si le balayage qui l’aurait redétectée a bien abouti, pour qu’une panne d’authentification ne résolve pas un vrai backlog en silence. Les resolves sont aussi cantonnés par origine, car le balayage de 15 minutes et le job de fairness de 08:00 partagent le même objet d’état et, sans cela, le balayage fermerait chaque alerte de fairness un quart d’heure après son ouverture.
Le watchdog dévore le budget API dont il dépend. Garde-fou : API Budget Gate lit DailyApiRequests depuis /limits à chaque balayage et se retire au-dessus de SFDC_API_BUDGET_PCT (85). Sa propre consommation n’est pas le risque — deux appels par balayage font 192 par jour face à une allocation Enterprise qui démarre à 100 000 requêtes par tranche glissante de 24 heures, plus 1 000 par licence utilisateur. Le risque est d’être l’appel qui fait basculer une org déjà tendue.
Ce que cela remplace
Le statu quo est un rapport que quelqu’un a construit une fois et que personne n’ouvre. Il affiche la file garée avec exactitude et ne dit rien au moment où la file commence à grossir, le seul moment qui compte.
Les Audit Logs de LeanData sont la comparaison la plus proche et font mieux que ce workflow sur leur propre terrain. La version Q2-2026 les a reconstruits avec un assistant intégré qui répond aux questions de routing en langage naturel en citant le chemin du nœud et les conditions évaluées. Pour un admin qui débogue un lead, c’est inclus dans ce que vous payez déjà et cela bat tout ce qui est ici. Ce qu’il ne fait pas, c’est se réveiller tout seul : il répond à des questions, et la défaillance visée par ce workflow est que personne ne sait qu’il y a une question à poser. Faites tourner les deux : le watchdog vous dit que quelque chose a cassé, l’audit log vous dit pourquoi.
Construire la même chose en SQL planifié sur un warehouse est l’alternative légitime et l’emporte nettement dès que les données Salesforce y atterrissent déjà via un sync, parce que vous gagnez l’historique, le backfill et des agrégats moins chers. La version n8n l’emporte quand ce n’est pas le cas, puisqu’elle lit le CRM directement sans monter au préalable un pipeline d’ingestion — et l’appel d’astreinte, la déduplication et l’auto-resolve sont la partie qu’un SELECT ne vous donne à aucun prix.
# 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.