callTarget
- File
-
lib/workflows/functions/call-target.ts - Event
-
call-target/call.requested - Config
-
retries: 0,concurrency: { limit: 5 },rateLimit: { key: "event.data.targetPhoneNumber", limit: 2, period: "24h" }
Event payload
interface CallTargetEventData {
campaignId: string
campaignName: string
targetId: string
targetPhoneNumber: string
targetPayload?: Record<string, unknown>
subscriptionId: string
}
Emitted by PhoneNumberCallingService.processBroadcast and re-emitted by callEndedAnalysis on a
double tap.
Why these settings
retries: 0-
A dial is a discrete business action. An automatic retry would call the prospect a second time, so failures are returned as data rather than thrown.
rateLimitontargetPhoneNumber-
An absolute ceiling of two dials per number per 24 hours, independent of campaign configuration, broadcaster behaviour or double taps. Excess events are dropped, not queued. This is the last line of defence against harassing a prospect.
concurrency: { limit: 5 }-
Caps simultaneous Retell call creation regardless of how many events are enqueued.
Steps
check-call-window
PhoneNumberCallingService.checkCallWindow(campaignId, targetId).
Reads the campaign’s windowStart, windowEnd and allowWeekends, and the target’s tz (derived from
the area code, defaulting to America/New_York), converts the current UTC instant into that zone with
Intl, and returns false when the local time is outside [windowStart, windowEnd) or it is a
disallowed weekend.
Not in window → return { skipped: true, reason, skipReason: outside_call_window }. The lease is not
cleared, so the target is re-picked naturally once it expires.
pre-validate
preValidateTargetOnly(campaignId, targetId, targetPhoneNumber). First match wins:
| Guard | Check | Result |
|---|---|---|
Target exists |
|
|
DNC |
|
|
Prior terminal disposition |
a previous call to this number had a disposition in |
|
Contact lifecycle |
the linked contact’s stage is in |
|
E.164 |
|
reason |
Setting attempts = 9999 means that even a manual state reset cannot bring the target back into the
dial pool.
DNC and prior-disposition checks are keyed on the phone number, not the target id, so a Do Not Call request in one campaign suppresses the number in all of them.
initiate-call
initiateCall(campaignId, campaignName, targetId, targetPhoneNumber, targetPayload, false) — the final
false skips re-validation, which the previous step already did.
-
Select a source number via
PhoneNumberSelector— state first, then area code, then any available number. See Phone numbers. -
Pick a caller agent — a random active
caller-type agent for the workspace, falling back toRETELL_AGENT_LIST. -
Build metadata —
targetPayloadvalues are stringified and null-coalesced to"", because a Retell agent reads a missing variable’s name aloud.campaignId,campaignName,campaignTargetId,agentIdandagentVersionIdare added, and the whole object is sent as bothmetadataandretell_llm_dynamic_variables. -
Create the Retell call. On error: set the target to
failed, incrementattempts, recordlastError, and return{ skipped: true, skipReason: retell_error }— a provider outage should not produce unhandled exceptions. -
Persist —
upsertCallAndSetTargetDialingwrites theCallrow (statusdialing,providerCallId,phoneNumberId,agentId,aiAgentId,aiAgentName,agentVersionId,subscriptionId) and sets the target todialingin one transaction. A failure here throwsDatabaseErrorand is not rolled back at Retell — the call is already dialing and will be tracked by its webhook.
Success returns { callId, providerCallId, phoneNumberId, isFallback }.
Counter hygiene
Every exit path goes through a finish() helper that fire-and-forgets
decrementCallTargetRuns(campaignId), logging a failure without changing the result. A skipped or
failed dial must release its slot, or the campaign’s in-flight ceiling would ratchet down until it
stopped dialing entirely.
The double-tap path in callEndedAnalysis is the deliberate exception: it must decrement before
step.sleep, so it calls decrementCallTargetRuns directly rather than through this helper.
What happens next
The function stops after initiation. Post-call work is triggered by the call_analyzed webhook, which
emits call-target/call.ended — see callEndedAnalysis.