Calling models
Campaign
Table Campaign. Shared by voice and email campaigns; type discriminates.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
|
|
|
default |
|
|
Owning workspace |
|
|
Preferred outbound number (note the spelling) |
|
|
default |
|
|
Time-of-day call window |
|
|
default |
|
|
Retry ceiling per target |
|
|
default |
|
|
Preferred analysis agent |
|
|
Shared; also used as AI context |
|
|
Resend topic |
|
|
default |
|
|
Email only |
|
|
Email only |
|
|
Email only; mapped to column |
|
|
Email only |
|
|
Email only, default |
|
|
Source template (a campaign-less |
Indexes: status, subscriptionId, type, templateId.
Relations: targets, calls, emailTargets, emailVariations (relation CampaignVariations),
template (relation CampaignTemplate), emails, contactSequences,
callAiAgentAnalysisResults, preferredAiAgent (relation
CampaignPreferredAnalysisAgent), subscription.
Two distinct relations point at EmailVariation: emailVariations is the campaign’s own set, while
template is the single campaign-less variation it was seeded from.
CampaignTarget
Table CampaignTarget. One phone number in one call campaign.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
Nullable for legacy rows |
|
|
Registry link |
|
|
Contact link |
|
|
E.164 |
|
|
CSV columns and metadata; passed to Retell as dynamic variables. Default |
|
|
default |
|
|
default |
|
|
|
|
|
Derived from the area code; defaults to |
|
|
Raw AI answer, e.g. |
|
|
Cached call outcomes |
|
|
|
|
|
Last call’s disposition |
|
|
|
|
|
|
|
|
|
|
|
default |
|
|
|
|
|
Scheduling |
Indexes: (campaignId, state), (phoneNumber, state) as idx_campaign_targets_phone_state,
subscriptionId, state, contactId, nextEarliestAttemptAt as
idx_campaign_targets_next_attempt, leaseExpiresAt as idx_campaign_targets_lease.
The last two exist for get_and_lease_due_targets, which filters on both columns on every
broadcast tick.
Relations: campaign, subscription, contact, targetPhoneNumber, calls, followUpTasks,
contactSequences.
Business flags are denormalised onto the target so the targets table renders without joining the latest call.
Call
Table Call. One call attempt — from a campaign, or from an external dialer.
| Field group | Fields | Notes |
|---|---|---|
Correlation |
|
All nullable: an external call may have no campaign, target or workspace match |
Identifiers |
|
|
Status |
|
|
Timing |
|
|
Duration and cost |
|
|
Connection |
|
|
Interpretation |
|
|
Flags |
|
|
Business extraction |
|
|
Next steps |
|
|
Contact corrections |
|
What the callee said the right details are |
Content |
|
|
CRM |
|
|
Indexes: (campaignId, status), (targetPhoneNumber, disposition) as
idx_calls_phone_disposition, (targetPhoneNumber, campaignId, createdAt) as
idx_calls_phone_campaign_created, subscriptionId, providerCallId, agentId, targetId,
channelType, phoneNumberId, targetPhoneNumbersId, salesRepId, hubspotEngagementId.
idx_calls_phone_campaign_created serves both the "call history for this number across all
campaigns" view and the double tap’s countCallsTodayForTargetPhone guard.
Relations: campaign, target, agent (AiAgent), agentVersion, subscription,
phoneNumber, registryTargetPhone (relation CallRegistryTargetPhone), salesRep, profile,
WebhookEvent, aiAgentAnalysisResults, testCases (relation CallTestCases),
contactSequenceSteps.
|
|
Inbound HubSpot import
calls/hubspot-import (twice daily ET at 08:00 and 20:00, also calls/hubspot-import.requested)
queries hubspot4.v_calls_latest for engagements with updated after Configs
hubspot_call_sync_cursor (ISO datetime on the HUBSPOT_CONTACT_SYNC_SUBSCRIPTION_ID workspace),
then upserts them into Call.
-
providerCallId=hubspot:{engagementId},channelType=hubspot_call,status=ended. -
Existing Retell/Nooks rows that already have the HubSpot engagement id are skipped.
-
HubSpot-imported rows are updated when recording, disposition, duration, notes, or timestamps change.
-
Contact, sales-rep, and phone-registry links are resolve-only; the call is still created when they are missing.
-
The cursor advances to the max
updatedin a successful non-empty batch. Seed the Configs key before the first run. -
Does not write
Activityrows, publish CRM events, or write back to HubSpot.
Recommended index (human applies; agents do not run migrations):
CREATE INDEX CONCURRENTLY IF NOT EXISTS "Call_hubspotEngagementId_idx"
ON "Call" ("hubspotEngagementId");
CallAiAgentAnalysisResult
Table call_ai_agent_analysis_results.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
Both |
|
|
Did the logged disposition match the conversation |
|
|
Present when |
|
|
1–10 |
|
|
|
|
|
|
Unique: (callId, aiAgentId, aiAgentVersionId) — one verdict per call per agent version, so
re-running with a new prompt version adds a row while re-running the same version overwrites it.
Indexes: callId, campaignId, subscriptionId.
PhoneNumbers
Table PhoneNumbers. The pool of outbound (dial-from) numbers.
| Field | Type | Notes |
|---|---|---|
|
|
Globally unique |
|
|
|
|
|
|
|
|
default |
|
|
default |
|
|
US state code |
|
|
default |
|
|
|
|
|
|
|
|
default |
|
|
MessagingApp identifier |
|
|
Retell identifier |
|
|
Reset nightly |
|
|
Lifetime |
|
|
|
Indexes: phoneNumber, (state, isActive, isDeleted) for the selection query, subscriptionId.
BlockedPhoneNumbers
Table blocked_phone_numbers. A workspace deny list.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
Globally unique |
Distinct from TargetPhoneNumbers.isBlocked: this table blocks a raw number that may never have been
a target, while the registry flag blocks a known contact endpoint.
WebhookEvent
Table WebhookEvent. The raw audit trail of every inbound webhook, written before dispatch.
| Field | Type | Notes |
|---|---|---|
|
|
|
|
|
Provider event id, or a generated UUID when the provider sends none |
|
|
|
|
|
Complete payload |
|
|
Correlation key |
|
|
Linked |
|
|
default |
|
|
Unique: (source, eventId) — the idempotency key. A duplicate delivery upserts to a no-op.
Indexes: providerCallId, callId, (source, eventId), receivedAt.
The table is not workspace-scoped: the payload is captured before a tenant has been resolved.
webhookEventPurgeCron deletes rows whose receivedAt is strictly older than seven days, including rows that were never marked processed. Replay from this table is only possible for events received in the last seven days.
|
|