Troubleshooting
No calls are being made
Work down the chain; each step rules out the one above.
| # | Check | If wrong |
|---|---|---|
1 |
|
Anything else and |
2 |
The |
No tick, no dials |
3 |
The broadcast returns |
|
4 |
The response’s |
|
5 |
The Redis counter falls back toward zero between ticks |
If it only rises, |
6 |
Targets are due — |
Everything may be inside its 24 h cooling-off window |
7 |
|
If not, |
8 |
The |
|
9 |
The Inngest rate limit |
2 runs per |
A leaking Redis counter is the classic slow failure: dialing gradually reduces and then stops, with no
error anywhere. Compare getCallTargetRuns(campaignId) against maxConcurrentCalls × 2.
Targets are being skipped
Read CampaignTarget.lastError and the workflow’s skipReason.
skipReason / lastError |
Cause |
|---|---|
|
The number is on the DNC list |
|
A previous call to this number had |
|
The contact is a |
|
The number does not match |
|
Outside the window in the target’s timezone |
|
Retell rejected the call; the target is |
|
The registry row has |
|
Registry |
|
The registry row belongs to another workspace |
Targets with attempts = 9999 were permanently disqualified by pre-validation and will not return to the
pool even if their state is reset.
No emails are being sent
| # | Check | If wrong |
|---|---|---|
1 |
|
The broadcast returns |
2 |
The |
No tick, no sends |
3 |
The broadcast returns |
|
4 |
|
|
5 |
At least one active variation exists |
|
6 |
At least one active sending account exists under an active domain |
|
7 |
Accounts are under both their own and the parent domain’s |
|
8 |
|
Daily cap reached |
9 |
Pending targets exist whose |
|
10 |
|
Check the skip reason on each |
emailSend skip reasons:
| Reason | Meaning |
|---|---|
|
The target has left |
|
|
|
Fewer than |
|
Every active variation is over its hourly rate. The lock is released |
|
Campaign daily cap. The lock is released |
|
Campaign or variation missing |
newsletter_gap is the one that most often looks like a bug: the campaign is healthy, the recipients are
eligible, but the address received a different campaign’s email within the last week.
Pending targets hidden behind leases
Each tick stamps queuedAt on the batch it leases, and the lease query skips rows leased within
EMAIL_TARGET_LOCK_WINDOW_SECONDS (2 days). A tick that halts after leasing gives its leases back.
Before that fix, a run throttled by the daily cap left the batch leased, so a few hours of
throttled ticks could hide thousands of recipients for 2 days.
Count leased pending targets:
SELECT count(*) FILTER (WHERE "queuedAt" >= NOW() - INTERVAL '2 days') AS leased,
count(*) FILTER (WHERE "queuedAt" IS NULL OR "queuedAt" < NOW() - INTERVAL '2 days') AS available
FROM email_campaign_targets
WHERE "campaignId" = '<campaign-id>' AND status = 'pending';
If almost every pending target is leased and the timestamps are hours old, release them. The
1-hour guard leaves leases from a live tick alone, and emailSend skips any target that has
already left pending, so a duplicate event cannot send twice:
UPDATE email_campaign_targets
SET "queuedAt" = NULL
WHERE "campaignId" = '<campaign-id>'
AND status = 'pending'
AND "queuedAt" < NOW() - INTERVAL '1 hour';
No AI analysis is being produced
| Check | Detail |
|---|---|
An analysis agent is configured |
The campaign’s |
The agent’s |
Otherwise |
|
Otherwise every run fails |
The disconnection reason |
Reasons in |
Whether a double tap intervened |
An eligible double tap returns before the analysis step |
The |
Transient errors ( |
Insufficient funds means the AI Gateway account is out of credit — the error is treated as transient and
silently swallowed, so it produces missing analyses rather than alerts.
A sequence is stuck
| Check | Detail |
|---|---|
|
|
|
Null means no run ever claimed it. Send |
The Inngest run |
Look it up by |
|
Each row’s |
Gate steps |
|
Skipped actions |
|
BigQuery |
A contact with no |
already_called is intentional: a call_task step creates at most one task per enrollment, so a template
that loops back to a call step will skip it the second time.
CRM data is not appearing in HubSpot
-
Confirm the contact has a
hubspotContactId. Every CRM publisher skips without one, deliberately. -
Confirm
CRM_QUEUE_URLand AWS credentials are set — publishers returnfalserather than throwing, so a missing queue is silent. -
Check for the publish in the logs.
-
Confirm call eligibility: duration ≥ 10 s, not voicemail, not screened, disposition in
HUBSPOT_WHITE_LISTED_DISPOSITIONS, andisTestTargetnot true. -
Check the downstream consumer — the application only enqueues.
Call.hubspotSyncStatusis set by the consumer, not by this application.
Webhooks are not arriving
-
Check the
WebhookEventtable. A row means it arrived and the problem is in the handler. -
No row → verify the provider’s configured URL, and that the path is under
/api/webhooks/(excluded from middleware). -
401from HubSpot →HUBSPOT_WEBHOOK_CLIENT_SECRETmismatch; that route rejects when unset. -
A row exists but nothing changed → the event type may be unregistered. Only
call_analyzedis registered for Retell;call_startedis stored and ignored. Five9 payloads are stored and never dispatched. -
A Retell row with no effect → check for
campaignIdandcampaignTargetIdincall.metadata. Without both, the handler logs and returns.
A report is empty or stale
-
Views prefixed
vm_are materialised — check the refresh schedule before suspecting the route. -
Date filters are Eastern Time. A
dateFromof today returns nothing before 00:00 ET, which is 05:00 UTC. -
Views are managed in Supabase, not by Prisma migrations. A new environment needs them created by hand.
-
phone_number_pickup_ratejoins"PhoneNumbers"— the quoted PascalCase name. An unquoted reference fails.
Duplicate sends or calls
The guards, in order of what to check:
| Guard | Where |
|---|---|
|
Duplicate deliveries |
|
Duplicate call rows |
|
Repeat dialing |
|
Repeat post-processing |
|
Concurrent broadcast ticks |
|
Concurrent email ticks |
|
Duplicate send events |
|
Duplicate logs |
POST /api/webhooks/five9 is the known exception — it generates a random eventId, so replays create new
rows.
Build and type errors
| Symptom | Fix |
|---|---|
|
|
Prisma model types missing after a schema edit |
|
Stale build after dependency changes |
|
Missing types in a production build |
Use |