Um flow do n8n que monitora o routing de leads do Salesforce de fora e aciona alguém antes de os reps perceberem. Três detectores rodam a cada 15 minutos sobre uma única consulta SOQL — registros parados numa fila de espera, registros com dono desativado e registros que estouraram o SLA de primeiro contato — mais uma checagem de desvio do round-robin toda manhã de dia útil. Os achados são deduplicados, resolvidos sozinhos quando a condição some e divididos entre PagerDuty e Slack por uma regra que o flow declara em voz alta. O bundle em apps/web/public/artifacts/routing-failure-watchdog-n8n/ traz o export completo de 20 nós mais um _README.md cobrindo import, as duas credenciais, a tabela completa de variáveis de ambiente, uma verificação em cinco passos e quanto custa rodar isso.
Os dois relógios
Quase todo dashboard de speed-to-lead reporta um número só: o tempo entre a criação do lead e o primeiro contato. Esse número é a soma de duas falhas independentes, e somá-las é o motivo pelo qual o alerta resultante acorda a pessoa errada.
Latência de routing é criado → atribuído. Quebra quando uma regra de atribuição para de bater, um nó do grafo de routing estoura, uma fila enche ou uma credencial atrás de um passo de enriquecimento vence no meio do grafo. É um incidente de ops, a pessoa de plantão resolve às 02:00 e neste flow ele aciona plantão.
Latência de resposta é atribuído → primeiro contato registrado. Quebra quando um rep está em reunião, de férias ou ignorando a fila. É uma conversa de gestão, ninguém resolve isso às 02:00 e neste flow ele posta no Slack e nunca aciona ninguém.
Parse Routing State calcula os dois separadamente e se recusa a inventar o primeiro. Se um registro não tem timestamp de routing — nem campo de atribuição, nem linha de log do LeanData — ele fica marcado como routedAtSource: 'none' em vez de cair no default CreatedDate, o que reportaria latência de routing zero para todo registro da org e deixaria o detector mudo para sempre.
A evidência publicada sobre por que isso importa é mais velha e mais rala que o folclore em volta dela. O achado rastreável é o estudo Lead Response Management de 2007 (Oldroyd, com a InsideSales.com), que analisou cerca de 15.000 leads e 100.000 tentativas de ligação e reportou que a chance de contatar um lead cai cerca de 100x e a de qualificá-lo cerca de 21x quando a ligação sai aos 30 minutos em vez de aos 5. São dados de quase vinte anos atrás, de seis empresas, e o número muito repetido de que “78% compram de quem responde primeiro” não tem nenhuma metodologia publicada por trás. Defina RESPONSE_SLA_MINUTES a partir do seu próprio funil se você conseguir medir; o default de 5 minutos é convenção, não lei.
Quando usar
Use quando o routing é automatizado e a falha é silenciosa. Essa combinação é a condição inteira. Um esquema de atribuição por regras no Salesforce, um grafo do LeanData ou um pool de round-robin compartilham a propriedade de que, quando quebram, nada dá erro: o registro continua tendo dono, todos os dashboards continuam renderizando e o primeiro sinal é um rep perguntando por que a fila dele secou, ou um prospect respondendo a um concorrente.
Serve para times que já rodam volume suficiente para uma hora quebrada sair cara: de uns 100 registros inbound por dia para cima, onde uma queda de duas horas são 25 leads e ninguém olha a fila de espera no olho.
Combina com a triagem de leads inbound, que decide para onde os registros vão, e com o servidor MCP de routing do LeanData, que deixa um agente responder “por que esse lead caiu aqui?” depois que o watchdog avisou que alguma coisa caiu errado. Este flow é o alarme; aquele é a investigação.
Quando NÃO usar
Pule se uma pessoa atribui os leads na mão. Atribuição manual falha de forma visível: alguém percebe que a lista está grande. A falha que este flow pega é especificamente a automação quebrando calada.
Pule se você não consegue nomear suas filas de espera. O detector de sem-routing é um teste de pertencimento contra PARKING_OWNER_IDS, não um teste de nulo, por um motivo que vem logo abaixo. Se ninguém sabe dizer qual fila guarda os registros que não bateram com nenhuma regra, essa pergunta precisa de resposta antes de qualquer monitoramento valer a pena — e o flow vai continuar te dizendo isso a cada varredura em vez de reportar uma org limpa.
Pule a cadência de 15 minutos se você roda n8n Cloud Starter. Uma execução por disparo dá 96 por dia, algo como 2.950 por mês, contra as 2.500 execuções que o Starter inclui (página de preços do n8n, consultada em 2026-08-12, € 20 por mês na cobrança anual). Ou você vai para o Pro com 10.000 execuções, ou faz self-host, ou roda */30 e aceita até 15 minutos a mais de latência de detecção.
Por que o detector de sem-routing é um teste de pertencimento
Lead.OwnerId nunca é nulo. Quando nenhuma regra de atribuição bate, o Salesforce entrega o registro ao Default Lead Owner configurado em Lead Settings. Não existe campo que signifique “isto não foi roteado”: um lead sem routing e um roteado corretamente são registros estruturalmente idênticos, distinguíveis só por quem é o dono.
Então PARKING_OWNER_IDS carrega o dono padrão mais toda fila de retenção e de catch-all, e o detector pergunta se um registro ficou em alguma delas além de UNROUTED_GRACE_MINUTES de tempo útil. A comparação roda sobre o prefixo de 15 caracteres do Id, porque admins colam Ids de 15 caracteres da barra de endereço do Salesforce e a API REST devolve os de 18 — comparar as duas formas direto é o jeito mais comum de um detector bem pensado não bater com nada, para sempre.
A checagem de dono precisa de mais um pedaço de SOQL. OwnerId é polimórfico e pode apontar para um User ou para um Group, então Owner.IsActive não é um caminho de campo válido sozinho. Build Sweep Query usa TYPEOF Owner WHEN User THEN Id, Name, IsActive WHEN Group THEN Id, Name, Type END (SOQL, API versão 46.0 em diante) para pegar status de atividade dos donos usuário e tipo de fila dos donos fila numa ida só.
Setup
Importeapps/web/public/artifacts/routing-failure-watchdog-n8n/routing-failure-watchdog-n8n.json em Workflows → Import from File. Defina o timezone do workflow — as duas expressões cron leem ele.
Conecte a credencial do Salesforce. Um Connected App com o grant de client credentials e um usuário de integração Run As somente leitura. O watchdog é um job, não uma pessoa, e toda chamada ao Salesforce no export é um GET contra /query/ ou /limits.
Defina PARKING_OWNER_IDS. A seção 4 do _README.md cobre onde achar os Ids. Nada mais do setup importa tanto.
Defina os dois SLAs com intenção.ROUTING_SLA_MINUTES (2 por padrão) aciona plantão; RESPONSE_SLA_MINUTES (5) posta. Confirme essa divisão no passo 4 da verificação antes de ativar — um estouro de resposta que chegue ao PagerDuty numa terça à tarde vai chegar também às 02:00 de um sábado.
Defina o relógio útil.BUSINESS_HOURS_TZ é separado do timezone do workflow: um decide quando o flow acorda, o outro decide quais minutos contam contra um SLA.
Rode a verificação de cinco passos do _README.md antes de ativar. O passo 1 é o que importa: quebre a credencial do Salesforce de propósito e confirme que o flow alerta em vez de reportar uma varredura limpa.
Modos de falha e guardas
Zero linhas se lê como tudo certo. Um typo num filtro, uma mudança de permissão no usuário de integração ou uma credencial vencida produzem todos um resultado vazio, e aí cada detector reporta que não há nada errado. Guarda: Parse Routing State emite um item denominador sweep_summary, e Run Detectors devolve um achado no_denominator com severidade error — substituindo toda a saída dos detectores — quando uma varredura em horário útil não amostrou nada. Os nós HTTP rodam com neverError e fullResponse para que 401 e 403 cheguem como dado, e não como uma execução falha que ninguém lê.
Uma carga em massa se parece exatamente com uma queda de routing. Uma lista de marketing de 40.000 registros estaciona tudo por minutos, legitimamente. Alertar pelo número absoluto transforma cada importação em incidente. Guarda: o discriminador é concentração de fonte — uma falha real de routing se espalha entre valores de LeadSource, uma importação não. Passando STAMPEDE_MIN_BATCH (250) com 90% dos registros estacionados dividindo uma única fonte, o achado cai para info e o acionamento é suprimido, com o motivo escrito na mensagem.
Aritmética de SLA em relógio de parede inunda a segunda de manhã. Um lead que cai às 18:55 de sexta não estourou um SLA de 5 minutos às 09:00 de segunda, mas a conta ingênua diz que estourou por 3.725 minutos. Guarda: o tempo decorrido é calculado em minutos úteis contra BUSINESS_HOURS_TZ, BUSINESS_DAYS e BUSINESS_HOLIDAYS, usando Intl.DateTimeFormat em vez do relógio do worker, para que o timezone do host do n8n não vaze para o resultado.
Uma causa, 900 alertas. Um grafo de routing quebrado estoura o SLA de todo registro que toca. Guarda: os achados são agrupados por causa e carregam contagem exata com amostra limitada a MAX_ITEMS_PER_ALERT (25); o dedup_key do PagerDuty colapsa repetições num incidente só, e RENOTIFY_MINUTES (120) suprime o re-aviso a menos que a severidade escale.
Incidentes que nunca fecham. Uma condição que some sem um resolve explícito deixa incidentes abertos no PagerDuty até alguém reconhecer um acionamento velho, que é como um canal acaba mutado. Guarda: Alert Gate + Resolve manda event_action: 'resolve' no mesmo dedup_key quando uma chave para de disparar — mas só quando a varredura que a teria redetectado rodou com sucesso, para que uma falha de autenticação não resolva um backlog real para dentro do silêncio. Os resolves também são escopados por origem, porque a varredura de 15 minutos e o job de fairness das 08:00 dividem um mesmo objeto de estado e, sem isso, a varredura fecharia toda alerta de fairness quinze minutos depois de ela abrir.
O watchdog come o orçamento de API do qual ele depende. Guarda: API Budget Gate lê DailyApiRequests de /limits a cada varredura e se retira acima de SFDC_API_BUDGET_PCT (85). O consumo do próprio flow não é o risco — duas chamadas por varredura são 192 por dia contra uma alocação Enterprise que começa em 100.000 requests por 24 horas móveis mais 1.000 por licença de usuário. O risco é ser a chamada que derruba uma org que já estava no limite.
O que isso substitui
O status quo é um relatório que alguém construiu uma vez e ninguém abre. Ele mostra a fila estacionada com precisão e não diz nada no momento em que a fila começa a crescer, que é o único momento que importa.
Os próprios Audit Logs do LeanData são a comparação mais próxima e são melhores que este flow naquilo que fazem. A release Q2-2026 reconstruiu eles com um assistente embutido que responde perguntas de routing em linguagem natural e cita o caminho do nó e as condições avaliadas. Para um admin depurando um lead, isso já vem no que você paga e ganha de qualquer coisa daqui. O que ele não faz é acordar sozinho: ele responde perguntas, e a falha que este flow ataca é ninguém saber que existe uma pergunta a fazer. Rode os dois: o watchdog te diz que algo quebrou, o audit log te diz por quê.
Construir isso como SQL agendado sobre um warehouse é a alternativa legítima e ganha de longe assim que os dados do Salesforce já caem lá por um sync, porque você ganha histórico, backfill e agregados mais baratos. A versão em n8n ganha quando não caem, já que lê o CRM direto e não precisa subir um pipeline de ingestão antes — e acionamento, deduplicação e auto-resolve são a parte que um SELECT não te dá por preço nenhum.
# 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.