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.

rateLimit on targetPhoneNumber

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

findCampaignTargetById

target_not_found

DNC

findDncTarget(targetPhoneNumber)

state = skipped, reason dnc

Prior terminal disposition

a previous call to this number had a disposition in SKIP_CALL_DISPOSITIONS (Do_Not_Call, Not_a_Seller, Amazon_Seller)

state = skipped, attempts = 9999, reason prior_disposition

Contact lifecycle

the linked contact’s stage is in SKIP_CALL_LIFECYCLE_STAGES (customer, marketingqualifiedlead)

state = skipped, attempts = 9999, reason lifecycle_stage

E.164

/^\+[1-9]\d{1,14}$/

reason invalid_e164

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.

  1. Select a source number via PhoneNumberSelector — state first, then area code, then any available number. See Phone numbers.

  2. Pick a caller agent — a random active caller-type agent for the workspace, falling back to RETELL_AGENT_LIST.

  3. Build metadata — targetPayload values are stringified and null-coalesced to "", because a Retell agent reads a missing variable’s name aloud. campaignId, campaignName, campaignTargetId, agentId and agentVersionId are added, and the whole object is sent as both metadata and retell_llm_dynamic_variables.

  4. Create the Retell call. On error: set the target to failed, increment attempts, record lastError, and return { skipped: true, skipReason: retell_error } — a provider outage should not produce unhandled exceptions.

  5. Persist — upsertCallAndSetTargetDialing writes the Call row (status dialing, providerCallId, phoneNumberId, agentId, aiAgentId, aiAgentName, agentVersionId, subscriptionId) and sets the target to dialing in one transaction. A failure here throws DatabaseError and 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.