Calling models

Campaign

Table Campaign. Shared by voice and email campaigns; type discriminates.

Field Type Notes

name

String

type

CampaignType

call (default) or email

status

CampaignStatus

default draft

subscriptionId

String @db.Uuid

Owning workspace

preferedPhone

String?

Preferred outbound number (note the spelling)

maxConcurrentCalls

Int

default 10

windowStart, windowEnd

DateTime? @db.Time(6)

Time-of-day call window

allowWeekends

Boolean

default false

maxAttempts

Int?

Retry ceiling per target

waitHoursToRecall

Int?

default 24

preferredAiAgentId

String? @db.Uuid

Preferred analysis agent

description, goal

String?

Shared; also used as AI context

resendTopicId

String?

Resend topic

schedulerIntervalSeconds

Int?

default 30

emailsPerHour

Int?

Email only

maxEmailsPerDay

Int?

Email only

emailsPerVariationPerHour

Int?

Email only; mapped to column emailsPerVariation

emailWindowStart, emailWindowEnd

DateTime? @db.Time(6)

Email only

emailAllowWeekends

Boolean

Email only, default false

templateId

String? @db.Uuid

Source template (a campaign-less EmailVariation), onDelete: SetNull

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

campaignId

String @db.Uuid

subscriptionId

String? @db.Uuid

Nullable for legacy rows

targetPhoneNumbersId

String? @db.Uuid

Registry link

contactId

String? @db.Uuid

Contact link

phoneNumber

String

E.164

payloadJson

Json

CSV columns and metadata; passed to Retell as dynamic variables. Default '{}'::jsonb

state

TargetState

default pending

attempts

Int

default 0; sentinel values 9999 / +999999 disqualify permanently

lastError

String?

tz

String?

Derived from the area code; defaults to America/New_York at evaluation time

amazonSeller

String?

Raw AI answer, e.g. "Undetermined"

isAmazonSeller, meetsMonthlyThreshold, interestedInDailyPayments

Boolean?

Cached call outcomes

stopCalling

Boolean?

disposition

String?

Last call’s disposition

lastCallRecordingUrl

String?

nextAction

String?

validConversation

Boolean?

durationSec >= 10

isTestTarget

Boolean?

default false; excluded from CRM sync

hubspotContactId, hubspotEngagementId, hubspotSyncStatus

String?

lastAttemptAt, nextEarliestAttemptAt, leaseExpiresAt

DateTime? @db.Timestamptz(6)

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

campaignId?, targetId?, agentId?, agentVersionId?, subscriptionId?, phoneNumberId?, targetPhoneNumbersId?, salesRepId?, profileId?

All nullable: an external call may have no campaign, target or workspace match

Identifiers

providerCallId (@unique), targetPhoneNumber, aiAgentId, aiAgentName

providerCallId uniqueness is what makes webhook upserts idempotent

Status

status (CallStatus, default created), direction (default outbound), channelType

channelType is phone_call, web_call, nooks_call or hubspot_call

Timing

createdAt, updatedAt, startTimestamp?, endTimestamp?

Duration and cost

durationSec?, durationMs?, callCost?

Connection

latencyMs?, disconnectionReason?, retellDisposition?

Interpretation

userSentiment?, disposition?, callSuccessful?, score?

score is an operator-set 1–10 rating; disposition is the platform’s reading of retellDisposition

Flags

inVoicemail, callScreeningDetected, aiSuspicionDetected (all default false), detectedLanguage?

Business extraction

monthlyRevenue?, isAmazonSeller?, meetsMonthlyThreshold?, interestedInDailyPayments?, isDecisionMaker (default false)

Next steps

nextAction?, terminationReason?, callbackRequested (default false), proposedCallbackTime?

proposedCallbackTime is the raw AI string, not a parsed date

Contact corrections

contactName?, correctContactInfo?, correctNumber?

What the callee said the right details are

Content

summary?, transcript?, recordingUrl?, notes?

notes is operator-entered

CRM

hubspotEngagementId?, hubspotSyncStatus?

synced, failed, skipped

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.

Call.agentId is an AiAgent UUID. Call.aiAgentId is the Retell agent identifier, stored as a plain string alongside aiAgentName for display. They are different things.

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 updated in a successful non-empty batch. Seed the Configs key before the first run.

  • Does not write Activity rows, 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

callId

String @db.Uuid

onDelete: Cascade

campaignId

String? @db.Uuid

onDelete: SetNull; null for Nooks calls

subscriptionId

String @db.Uuid

onDelete: Cascade

aiAgentId, aiAgentVersionId

String @db.Uuid

Both onDelete: Cascade

correctDisposition

Boolean

Did the logged disposition match the conversation

suggestedDisposition

String?

Present when correctDisposition is false

agentScore

Int

1–10

summary

String

issues

Json

CallAnalysisIssue[], default []

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

phoneNumber

String @unique

Globally unique

subscriptionId

String @db.Uuid

isDefault, isActive, isDeleted, isReleased

Boolean

isActive defaults true; the others false

smsEnabled

Boolean

default false

provider

String

default external; also retell_twilio, retell_telnyx

state

String?

US state code

country

String

default US

maxAllowedCalls, maxAllowedCallsPerDay

Int?

null = unlimited

costPerSecond, monthlyCost

Float?

allowedOutboundShortCodes

Json?

default []

phoneNumberSid

String?

MessagingApp identifier

retellPhoneNumberId

String?

Retell identifier

dailyCallCount

Int

Reset nightly

totalCallCount

Int

Lifetime

lastUsedAt, purchasedAt, releasedAt, deletedAt

DateTime?

purchasedAt defaults to now()

Indexes: phoneNumber, (state, isActive, isDeleted) for the selection query, subscriptionId.

BlockedPhoneNumbers

Table blocked_phone_numbers. A workspace deny list.

Field Type Notes

subscriptionId

String @db.Uuid

phoneNumber

String @unique

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

source

String

Retell, Resend, Nooks, JustCall

eventId

String

Provider event id, or a generated UUID when the provider sends none

eventType

String

rawPayload

Json

Complete payload

providerCallId

String?

Correlation key

callId

String? @db.Uuid

Linked Call, when matched

receivedAt

DateTime

default now()

processedAt

DateTime?

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.

POST /api/webhooks/five9/route.ts writes with source = "Nooks" and eventType = "five_9", generating a random eventId and providerCallId. Because the id is random, its rows are not idempotent on replay, and they share the Nooks source with genuine Nooks events. Filter on eventType to separate them.