Prisma schema reference
The PostgreSQL schema is defined by the Payability web application’s Prisma schema. That file is the authority: it owns the tables, the enum types, the constraints, and the migrations.
This repository contains no migrations. morpheus_data_layer mirrors a subset of the
schema so Python can read and write it. Every model docstring says the same thing —
"Read/write only — schema modifications are not permitted via this layer."
A copy is checked in at docs/modules/data/examples/schema.prisma and reproduced in full at
the bottom of this page.
|
The checked-in copy is a snapshot for reference. The live schema lives in the web application repository and can move without a commit here. When something disagrees, the web application’s schema wins — and this copy needs refreshing. |
Adding a column, end to end
-
Add the field to the web application’s Prisma schema and migrate it there.
-
Add the matching
mapped_columnto the model inmorpheus_data_layer/models/, using the exact camelCase column name. -
If it is an enum column, add the
StrEnumtomorpheus_data_layer/enums.pyand build the PostgreSQL type withcreate_type=False— see Enumerations. -
Add it to
update_columnsanywhere it should participate in an upsert. -
Refresh the copy under
examples/and update ORM models.
Doing step 2 before step 1 produces a runtime error on the first query touching the column, not a startup failure — SQLAlchemy does not validate the mapping against the database.
Coverage
Prisma declares 28 models. Morpheus maps 13.
| Prisma model | Table | Mapped in morpheus_data_layer |
|---|---|---|
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes — as |
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes |
|
|
Yes — as |
|
|
No |
|
— |
No |
|
— |
No |
|
— |
No |
|
|
No |
|
|
No |
|
|
No |
|
— |
No |
|
— |
No |
|
— |
No |
|
|
No |
|
|
No |
|
|
No |
|
|
No |
|
|
No |
|
|
No |
|
|
No |
The unmapped half is the web application’s own domain — campaign definitions, sequence templates, AI agent configuration, user profiles, domain warm-up. Morpheus consumes the results of those systems through HubSpot and BigQuery rather than reading their tables.
Note the naming inconsistency the ORM inherits: some models declare @@map to a snake_case
plural table, others do not and keep a PascalCase singular name. Activity, Call, and
CampaignTarget are quoted identifiers in SQL for that reason.
Relations
| Relation | Meaning |
|---|---|
|
The tenant boundary. Every domain table carries |
|
A business and its people. |
|
The hub of the model. Almost everything hangs off a person. |
|
One phone number in a campaign, and every attempt against it. |
|
Who dialled, and the registry entry for the number dialled. |
|
A commitment and its owner. |
|
The idempotency key for the engagement sync. |
|
|
Known drift
Real differences between Prisma and the SQLAlchemy models. None currently break at runtime, but each is a trap.
| Area | Prisma | SQLAlchemy |
|---|---|---|
|
6 values: |
11 values, a longer ladder including |
|
|
|
|
Composite: |
Global nullable-unique on |
|
Has |
No |
|
Required foreign key to |
Nullable |
|
Has |
Neither mapped |
|
Has |
Neither field mapped |
|
Has |
Not mapped |
|
Has |
Neither mapped |
Two of these deserve attention:
- The lifecycle enum mismatch is the significant one
-
If the Prisma set is what the database type actually holds, then writing
raw_leadorbdr_handofffrom Python would be rejected by PostgreSQL. Either the checked-in Prisma copy is stale or those stages are unreachable in practice. Verify against the live type before relying on any stage beyondenriched— see Enumerations. - Contact uniqueness is composite upstream, global here
-
Prisma scopes uniqueness to the subscription; the ORM does not. On a single tenant the two are equivalent, which is why nothing has broken. Onboarding a second subscription would make the ORM’s global constraint reject a legitimate row — two tenants cannot hold the same email address.
upsert_for_seller()already works around collisions by stripping conflicting values (see Repositories); that workaround becomes load-bearing the moment a second tenant exists.
Everything else in the table is a field Morpheus simply does not read. Unmapped columns are harmless — SQLAlchemy ignores columns it does not declare — but they will not appear on a model instance, and an upsert that omits them leaves whatever the web application wrote intact.
Enums declared only in Prisma
CampaignStatus, AiAgentType, CampaignType, AiAgentLlmModelType, SubscriptionUserRole,
SequenceType, SequenceStatus, SequenceStepChannel, DomainWarmupStatus.
All belong to unmapped tables. There is no reason to mirror them until a handler reads one.
Full schema
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "generated"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
/**
* =========================
* Enums
* =========================
*/
enum SellerPlatform {
amazon
walmart
shopify
}
enum SellerDatasource {
smartscout
manual
}
enum ContactHeatStatus {
hot // active follow-up in progress
warm // rep acted but deal not closed
warming // task failed, automated email track running
idle // 30 days of silence, deprioritized
}
enum ContactSource {
campaign
hubspot
import
manual
}
enum LineTypes {
mobile
landline
fixedVoip
nonFixedVoip
tollFree
sharedCost
premium
voicemail
pager
voip
uan
unknown
other
}
enum RiskLevel {
low
medium
high
critical
}
// Mirrors HubSpot lifecyclestage values
enum ContactLifecycleStage {
subscriber
lead
marketingqualifiedlead
salesqualifiedlead
opportunity
customer
evangelist
other
}
// Mirrors HubSpot hs_lead_status values
enum ContactLeadStatus {
new
open
in_progress
open_deal
unqualified
attempted_to_contact
connected
bad_timing
}
enum ContactHubspotSyncStatus {
synced
pending
failed
skipped
}
enum CampaignStatus {
draft
running
paused
stopped
completed
}
enum TargetState {
pending
in_progress
dialing
completed
failed
canceled
skipped // Prior terminal disposition (Do_Not_Call, Not_a_Seller, Amazon_Seller) – do not call again
// --- NOVOS STATUS ADICIONADOS ---
DNC // Do Not Call
WN // Wrong Number
MAX // Max Attempts Reached
DT_PENDING // Double Tap Pending (Aguardando retry rápido)
VOICEMAIL // Voicemail Detected
}
enum CallStatus {
created
dialing
answered
ended
}
enum AiAgentType {
analysis
caller
sms
other
}
enum SellerLifeCycleStage {
new
enriched
qualified
opportunity
closed_won
closed_lost
}
enum ActivityType {
sms
email
call
task
meeting
note
reminder
}
enum FollowUpTaskStatus {
pending
in_progress
completed
skipped
failed
}
enum FollowUpTaskType {
call
email
sequence_enrollment
}
enum CampaignType {
call
email
}
enum EmailTargetStatus {
pending
requested
sent
opened
bounced
unsubscribed
blocked
failed
}
/**
* LLM-aligned reply / engagement category for an email campaign recipient.
*/
enum EmailTargetCategory {
interested
not_interested
question
out_of_office
unsubscribe
wrong_person
other
}
enum EmailVariationStatus {
pending_generation
active
archived
}
enum AiAgentLlmModelType {
gpt
claude
}
/**
* =========================
* Enums (Subscription)
* =========================
*/
enum SubscriptionUserRole {
owner
admin
member
}
enum SequenceType {
onboarding
re_engagement
not_connected_outreach
}
enum SequenceStatus {
pending // created, waiting for trigger (e.g. waiting for rep task window)
active // currently running
paused // manually paused
completed // contact onboarded
abandoned // ran out of steps, no response
exited // pivoted to different sequence
}
enum SequenceStepChannel {
email
call_task
sms
wait
data_gate // BigQuery condition check that branches the sequence
crm_task // Create a HubSpot task via the CRM SQS queue
crm_event // Publish a user-defined event to the CRM SQS queue (event_type from a registry, body from key/value pairs)
}
/**
* =========================
* Models
* =========================
*/
model Subscription {
id String @id @default(uuid()) @db.Uuid
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
users SubscriptionUser[]
campaigns Campaign[]
phoneNumbers PhoneNumbers[]
calls Call[]
campaignTargets CampaignTarget[]
aiAgents AiAgent[]
aiAgentVersions AiAgentVersion[]
callAiAgentAnalysisResults CallAiAgentAnalysisResult[]
configs Configs[]
agentTestCases AgentTestCase[]
activities Activity[]
sellers Seller[]
contacts Contact[]
emailCampaignTargets EmailCampaignTarget[]
emailVariations EmailVariation[]
emailSendingDomains EmailSendingDomain[]
domains Domain[]
targetPhoneNumbers TargetPhoneNumbers[]
targetEmails TargetEmails[]
emails Emails[]
salesReps SalesRep[]
followUpTasks FollowUpTask[]
contactSequences ContactSequence[]
sequenceTemplates SequenceTemplate[]
blockedPhoneNumbers BlockedPhoneNumbers[]
}
model SubscriptionUser {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
profileId String @db.Uuid
role SubscriptionUserRole @default(member)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade)
@@unique([subscriptionId, profileId])
@@index([profileId])
}
model Campaign {
id String @id @default(uuid()) @db.Uuid
name String
type CampaignType @default(call)
preferedPhone String?
status CampaignStatus @default(draft)
maxConcurrentCalls Int @default(10)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
windowStart DateTime? @db.Time(6)
windowEnd DateTime? @db.Time(6)
allowWeekends Boolean @default(false)
maxAttempts Int?
waitHoursToRecall Int? @default(24)
preferredAiAgentId String? @db.Uuid
subscriptionId String @db.Uuid
// Shared optional fields (available on all campaign types)
description String?
goal String?
resendTopicId String?
schedulerIntervalSeconds Int? @default(30)
// Email campaign config (nullable; only used when type = email)
emailsPerHour Int?
maxEmailsPerDay Int?
emailsPerVariationPerHour Int? @map("emailsPerVariation")
emailWindowStart DateTime? @db.Time(6)
emailWindowEnd DateTime? @db.Time(6)
emailAllowWeekends Boolean @default(false)
// Relations
targets CampaignTarget[]
calls Call[]
preferredAiAgent AiAgent? @relation("CampaignPreferredAnalysisAgent", fields: [preferredAiAgentId], references: [id])
callAiAgentAnalysisResults CallAiAgentAnalysisResult[]
subscription Subscription @relation(fields: [subscriptionId], references: [id])
emailTargets EmailCampaignTarget[]
emailVariations EmailVariation[]
emails Emails[]
contactSequences ContactSequence[]
@@index([status])
@@index([subscriptionId])
@@index([type])
}
model Profile {
id String @id @default(uuid()) @db.Uuid
supabaseUserId String @unique @db.Uuid
name String
justcallUserId String? @unique
isAvailable Boolean @default(false)
maxConcurrentCalls Int @default(1)
activeCalls Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
phoneNumber String? @unique
email String? @unique
// Relations: Profile can be in many subscriptions via SubscriptionUser (bridge)
calls Call[]
subscriptionUsers SubscriptionUser[]
salesReps SalesRep[]
@@index([isAvailable, activeCalls])
}
model AiAgent {
id String @id @default(uuid()) @db.Uuid
name String
type AiAgentType
llmModelType AiAgentLlmModelType
subscriptionId String? @db.Uuid
/**
* Vercel AI Gateway model slug (e.g. openai/gpt-4o). When set, used instead of llmModelType default.
*/
modelId String?
/**
* Retell AI agent_id. Only set for type=caller agents that are linked to Retell.
*/
retellAgentId String? @unique
/**
* Points to the currently active AiAgentVersion (most recently published config).
* Set to null only when no version exists yet.
*/
latestVersionId String? @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
versions AiAgentVersion[] @relation("AgentVersions")
latestVersion AiAgentVersion? @relation("LatestVersion", fields: [latestVersionId], references: [id])
campaignsPreferredForAnalysis Campaign[] @relation("CampaignPreferredAnalysisAgent")
callAiAgentAnalysisResults CallAiAgentAnalysisResult[]
calls Call[]
@@index([subscriptionId])
@@index([retellAgentId])
@@map("ai_agents")
}
model AiAgentVersion {
id String @id @default(uuid()) @db.Uuid
aiAgentId String @db.Uuid
subscriptionId String? @db.Uuid
version Int // 1-based
mainPrompt String @default("")
/**
* For type=caller agents: stores the full Retell IAgentData config as JSON.
* For type=analysis agents: stores supplementary config (model overrides, etc.).
*/
configJson Json @default("{}")
createdAt DateTime @default(now())
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
aiAgent AiAgent @relation("AgentVersions", fields: [aiAgentId], references: [id], onDelete: Cascade)
agentAsLatest AiAgent[] @relation("LatestVersion")
analysisResults CallAiAgentAnalysisResult[]
calls Call[]
@@unique([aiAgentId, version])
@@index([aiAgentId])
@@index([subscriptionId])
@@map("ai_agent_versions")
}
model CampaignTarget {
id String @id @default(uuid()) @db.Uuid
campaignId String @db.Uuid
subscriptionId String? @db.Uuid
targetPhoneNumbersId String? @db.Uuid
contactId String? @db.Uuid
phoneNumber String
payloadJson Json @default(dbgenerated("'{}'::jsonb"))
// State Machine
state TargetState @default(pending)
attempts Int @default(0)
lastError String?
tz String?
amazonSeller String?
// Business Logic Cache
isAmazonSeller Boolean?
meetsMonthlyThreshold Boolean?
interestedInDailyPayments Boolean?
stopCalling Boolean?
disposition String?
lastCallRecordingUrl String?
nextAction String?
hubspotEngagementId String?
hubspotSyncStatus String?
validConversation Boolean?
isTestTarget Boolean? @default(false)
// Integration Cache
hubspotContactId String?
// Scheduling
lastAttemptAt DateTime? @db.Timestamptz(6)
nextEarliestAttemptAt DateTime? @db.Timestamptz(6)
leaseExpiresAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
campaign Campaign @relation(fields: [campaignId], references: [id])
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
targetPhoneNumber TargetPhoneNumbers? @relation(fields: [targetPhoneNumbersId], references: [id])
calls Call[]
followUpTasks FollowUpTask[]
contactSequences ContactSequence[]
@@index([campaignId, state])
@@index([phoneNumber, state], map: "idx_campaign_targets_phone_state")
@@index([subscriptionId])
@@index([state])
@@index([contactId])
@@index([nextEarliestAttemptAt], map: "idx_campaign_targets_next_attempt")
@@index([leaseExpiresAt], map: "idx_campaign_targets_lease")
}
model Call {
id String @id @default(uuid()) @db.Uuid
/// Nullable for external-only rows (e.g. Nooks `call.logged`) when no campaign match exists.
campaignId String? @db.Uuid
targetId String? @db.Uuid
agentId String? @db.Uuid
agentVersionId String? @db.Uuid
subscriptionId String? @db.Uuid
// Identifiers
targetPhoneNumber String?
aiAgentId String?
aiAgentName String?
// ⚠️ CHANGE: Added @unique constraint for Upsert logic
providerCallId String? @unique
// Core Status
status CallStatus @default(created)
direction String @default("outbound")
channelType String? // 'phone_call', 'web_call', 'nooks_call'
// Timestamps (Time)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
startTimestamp DateTime?
endTimestamp DateTime?
// --- METRICS & KPIs (Auditability) ---
// 1. Duration & Cost
durationSec Int? // Duration (Rounded)
durationMs Int? // Duration (Exact)
callCost Float? // Cost
// 2. Technical / Connection
latencyMs Int? // End to End Latency
disconnectionReason String? // End Reason (Technical)
retellDisposition String? // Session Outcome (Raw Retell status)
// 3. AI Analysis - Disposition & Sentiment
userSentiment String? // User Sentiment
disposition String? // Call Disposition (Our Interpretation)
callSuccessful String? // Call Outcome (Legacy/Custom)
score Int? // 1-10 agent score for disposition (user-set)
// 4. AI Analysis - Intelligence Flags
inVoicemail Boolean @default(false)
callScreeningDetected Boolean @default(false) // call_screening_detected
aiSuspicionDetected Boolean @default(false) // ai_suspicion_detected
detectedLanguage String? // detected_language
// 5. AI Analysis - Business Logic Extraction
monthlyRevenue String? // monthly_revenue
isAmazonSeller Boolean?
meetsMonthlyThreshold Boolean?
interestedInDailyPayments Boolean?
isDecisionMaker Boolean @default(false) // is_decision_maker
// 6. Actionable Data (Next Steps)
nextAction String? // next_action (e.g. Do_Not_Call, Sales_Handoff)
terminationReason String? // termination_reason (AI Logic reason)
// 7. Callback & Contact Updates
callbackRequested Boolean @default(false) // callback_requested
proposedCallbackTime String? // proposed_callback_time (Raw string from AI)
contactName String? // contact_name (Extracted)
correctContactInfo String? // correct_contact_info
correctNumber String? // correct_number (If user gave new number)
// Content
summary String?
transcript String?
recordingUrl String?
notes String?
// Integrations
hubspotEngagementId String?
hubspotSyncStatus String? // synced, failed, skipped
// Phone Number Relation (our outbound dial-from numbers)
phoneNumberId String? @db.Uuid
/// Callee row in `target_phone_numbers` (same FK idea as `CampaignTarget.targetPhoneNumbersId`; Nooks, etc.)
targetPhoneNumbersId String? @db.Uuid
// Relations
campaign Campaign? @relation(fields: [campaignId], references: [id])
target CampaignTarget? @relation(fields: [targetId], references: [id])
agent AiAgent? @relation(fields: [agentId], references: [id])
agentVersion AiAgentVersion? @relation(fields: [agentVersionId], references: [id])
subscription Subscription? @relation(fields: [subscriptionId], references: [id])
phoneNumber PhoneNumbers? @relation(fields: [phoneNumberId], references: [id])
registryTargetPhone TargetPhoneNumbers? @relation("CallRegistryTargetPhone", fields: [targetPhoneNumbersId], references: [id])
WebhookEvent WebhookEvent[]
aiAgentAnalysisResults CallAiAgentAnalysisResult[]
salesRep SalesRep? @relation(fields: [salesRepId], references: [id])
salesRepId String? @db.Uuid
profile Profile? @relation(fields: [profileId], references: [id])
profileId String? @db.Uuid
testCases AgentTestCase[] @relation("CallTestCases")
contactSequenceSteps ContactSequenceStep[]
@@index([campaignId, status])
@@index([targetPhoneNumber, disposition], map: "idx_calls_phone_disposition")
@@index([targetPhoneNumber, campaignId, createdAt], map: "idx_calls_phone_campaign_created")
@@index([subscriptionId])
@@index([providerCallId])
@@index([agentId])
@@index([targetId])
@@index([channelType])
@@index([phoneNumberId])
@@index([targetPhoneNumbersId])
@@index([salesRepId])
}
model CallAiAgentAnalysisResult {
id String @id @default(uuid()) @db.Uuid
callId String @db.Uuid
campaignId String? @db.Uuid
subscriptionId String @db.Uuid
aiAgentId String @db.Uuid
aiAgentVersionId String @db.Uuid
correctDisposition Boolean
suggestedDisposition String?
agentScore Int // 1-10
summary String
issues Json @default("[]") // CallAnalysisIssue[]
createdAt DateTime @default(now())
call Call @relation(fields: [callId], references: [id], onDelete: Cascade)
campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
aiAgent AiAgent @relation(fields: [aiAgentId], references: [id], onDelete: Cascade)
aiAgentVersion AiAgentVersion @relation(fields: [aiAgentVersionId], references: [id], onDelete: Cascade)
@@unique([callId, aiAgentId, aiAgentVersionId])
@@index([callId])
@@index([campaignId])
@@index([subscriptionId])
@@map("call_ai_agent_analysis_results")
}
model WebhookEvent {
id String @id @default(uuid()) @db.Uuid
source String
eventId String
eventType String
rawPayload Json
providerCallId String?
callId String? @db.Uuid
receivedAt DateTime @default(now())
processedAt DateTime?
call Call? @relation(fields: [callId], references: [id])
@@unique([source, eventId])
@@index([providerCallId])
@@index([callId])
@@index([source, eventId])
}
model PhoneNumbers {
id String @id @default(uuid()) @db.Uuid
phoneNumber String @unique
isDefault Boolean @default(false)
isActive Boolean @default(true)
isDeleted Boolean @default(false)
subscriptionId String @db.Uuid
maxAllowedCalls Int?
costPerSecond Float?
maxAllowedCallsPerDay Int?
allowedOutboundShortCodes Json? @default("[]")
// Phone Number Management
state String? // US state code (e.g., "CA", "NY")
phoneNumberSid String? // Microservice phone number SID
retellPhoneNumberId String? // Retell phone number ID after registration
monthlyCost Float? // Monthly cost from microservice
country String @default("US") // Country code
provider String @default("external") // Purchase provider: "external" | "retell_twilio" | "retell_telnyx"
smsEnabled Boolean @default(false) // Whether this number is designated for SMS outreach
isReleased Boolean @default(false) // Soft delete
releasedAt DateTime?
lastUsedAt DateTime? // For optimization
deletedAt DateTime?
dailyCallCount Int @default(0) // Reset daily
totalCallCount Int @default(0) // Lifetime usage
purchasedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
calls Call[]
subscription Subscription @relation(fields: [subscriptionId], references: [id])
@@index([phoneNumber])
@@index([state, isActive, isDeleted])
@@index([subscriptionId])
}
model Configs {
id String @id @default(uuid()) @db.Uuid
key String @unique
subscriptionId String @db.Uuid
value String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
@@index([key])
@@index([subscriptionId])
@@map("configs")
}
model AgentTestCase {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
name String?
notes String?
// Source call — null for manually-entered test cases
sourceCallId String? @db.Uuid
// Call inputs
transcript String?
recordingUrl String?
// Ground-truth values (the correct outcome this test case must produce)
disposition String?
isAmazonSeller Boolean?
aiSuspicionDetected Boolean?
callScreeningDetected Boolean?
isDecisionMaker Boolean?
userSentiment String?
nextAction String?
callSuccessful String?
inVoicemail Boolean?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
sourceCall Call? @relation("CallTestCases", fields: [sourceCallId], references: [id])
@@index([subscriptionId])
@@index([sourceCallId])
@@map("agent_test_cases")
}
model Activity {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
type ActivityType
// FK to Contact — nullable for backward compat with existing rows
contactId String? @db.Uuid
contactPhone String?
contactEmail String?
campaignId String? @db.Uuid
targetId String? @db.Uuid
taskId String? @db.Uuid
subject String? // email subject line
body String? // message preview / body
status String @default("sent") // sent | failed | delivered | received
direction String @default("outbound")
notes String?
retellChatId String?
hubspotContactId String?
hubspotEngagementId String?
externalId String?
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
task FollowUpTask? @relation(fields: [taskId], references: [id])
contactSequenceSteps ContactSequenceStep[]
@@unique([subscriptionId, externalId])
@@index([subscriptionId])
@@index([type])
@@index([hubspotEngagementId])
@@index([createdAt])
@@index([contactPhone])
@@index([contactId])
@@index([taskId])
}
// Represents a company that sells products on Amazon, Walmart, or Shopify
model Seller {
id String @id @default(uuid())
subscriptionId String @db.Uuid
platform SellerPlatform
datasource SellerDatasource
lifeCycleStage SellerLifeCycleStage @default(new)
name String?
singleBrand Boolean @default(false)
brandNameMatch Boolean @default(false)
email String? @unique
phone String? @unique
primaryCategory String?
primarySubCategory String?
businessName String?
sellerId String @unique
estimateSales Int?
avgPrice Int?
percentFba Float?
numberReviewsLifetime Int?
numberReviews30Days Int?
numberWinningBrands Int?
numberAsins Int?
numberTopAsins Int?
street String?
city String?
state String?
country String?
zipCode String?
numBrands Int?
moMGrowth Float?
threeMonthGrowth Float?
sixMonthGrowth Float?
yearGrowth Float?
moMGrowthCount Int?
sixMonthGrowthCount Int?
isSuspended Boolean @default(false)
lastSuspendedDate DateTime?
startedSellingDate DateTime?
// ── HubSpot Company sync ──────────────────────────────────────
// Sellers map to HubSpot Companies (not Contacts)
hubspotCompanyId String? @unique
hubspotOwnerId String?
hubspotSyncStatus ContactHubspotSyncStatus?
hubspotLastSyncedAt DateTime?
hubspotEngagementId String?
// ── HubSpot Company enrichment fields ────────────────────────
website String?
description String?
linkedinUrl String?
industry String?
annualRevenue Int?
numberOfEmployees Int?
payoutPainScore Int?
note String?
errors Json? @default("[]")
enrichedAt DateTime?
scrapedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
contacts Contact[]
@@index([subscriptionId])
@@index([platform])
@@index([sellerId])
@@index([hubspotCompanyId])
@@index([country])
@@index([isSuspended])
@@index([primaryCategory])
@@map("sellers")
}
/**
* Single source of truth for contacts — shared between CampaignTargets and HubSpot.
* Phone and email are both nullable-unique: non-null values must be unique, multiple NULLs allowed.
*/
model Contact {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
sellerId String?
// ── Identity ──────────────────────────────────────────────────
firstName String?
lastName String?
email String?
phone String?
mobilePhone String?
companyName String?
jobTitle String?
website String?
// ── Address ───────────────────────────────────────────────────
street String?
city String?
state String?
country String?
zipCode String?
// ── Source & Lifecycle ────────────────────────────────────────
source String @default("manual")
lifecycleStage ContactLifecycleStage?
leadStatus ContactLeadStatus?
heatStatus ContactHeatStatus @default(idle) // hot | warm | warming | idle
totalRevenue Float?
leadScore Int?
// ── Calling / Campaign context ────────────────────────────────
disposition String?
nextAction String?
stopCalling Boolean @default(false)
validConversation Boolean?
lastCallRecordingUrl String?
// ── HubSpot sync ─────────────────────────────────────────────
hubspotContactId String?
hubspotOwnerId String?
hubspotSyncStatus ContactHubspotSyncStatus?
hubspotLastSyncedAt DateTime?
hubspotEngagementId String?
hubspotSyncPriority Int? @default(0)
hubspotDealId String?
hubspotDealStatus String?
hubspotDealStage String?
// ── Supplier sync ─────────────────────────────────────────────
supplierKey String?
supplierStatus String?
supplierCancellationStatus String?
supplierMarketplace String?
// ── Notes & metadata ──────────────────────────────────────────
notes String?
customFields Json? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
seller Seller? @relation(fields: [sellerId], references: [id])
emailCampaignTargets EmailCampaignTarget[]
activities Activity[]
targetPhoneNumbers TargetPhoneNumbers[]
targetEmails TargetEmails[]
emails Emails[]
campaignTargets CampaignTarget[]
followUpTasks FollowUpTask[]
contactSequences ContactSequence[]
@@unique([subscriptionId, email])
@@unique([subscriptionId, phone])
@@unique([subscriptionId, hubspotContactId])
@@unique([subscriptionId, supplierKey])
@@index([subscriptionId])
@@index([sellerId])
@@index([email])
@@index([phone])
@@index([disposition])
@@index([hubspotContactId])
@@index([supplierKey])
@@index([source])
@@index([lifecycleStage])
@@index([leadStatus])
@@index([stopCalling])
@@map("contacts")
}
/**
* Mediation table linking a Contact to an email Campaign.
* One record per (campaign, contact) pair. contactId is always set —
* CSV imports upsert the Contact first before creating this record.
*/
model EmailCampaignTarget {
id String @id @default(uuid()) @db.Uuid
campaignId String @db.Uuid
contactId String @db.Uuid
subscriptionId String @db.Uuid
emailId String @db.Uuid
// Cached contact fields for quick display without join
email String
firstName String?
lastName String?
// State
status EmailTargetStatus @default(pending)
attempts Int @default(0)
lastError String?
// Tracking
sentAt DateTime?
openedAt DateTime?
clickedAt DateTime?
unsubscribedAt DateTime?
// Resend correlation + variation used + sending domain
resendEmailId String?
variationId String? @db.Uuid
sendingDomainId String? @db.Uuid
// Set when the target is enqueued to Inngest; used to prevent duplicate sends
// during a lock window of 3× the campaign's schedulerIntervalSeconds.
queuedAt DateTime?
/**
* True after outbound send CRM activity was successfully queued to HubSpot (SQS).
*/
hubspotSynced Boolean @default(false)
/// Inbound / LLM-derived engagement category (e.g. reply classification).
category EmailTargetCategory?
categorisedAt DateTime?
/// Audit: model id or label used when `category` was set.
categoryModel String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
campaign Campaign @relation(fields: [campaignId], references: [id])
contact Contact @relation(fields: [contactId], references: [id])
subscription Subscription @relation(fields: [subscriptionId], references: [id])
variation EmailVariation? @relation(fields: [variationId], references: [id])
targetEmail TargetEmails @relation(fields: [emailId], references: [id])
emails Emails[]
contactSequences ContactSequence[]
@@unique([campaignId, contactId])
@@unique([campaignId, email])
@@index([campaignId, status])
@@index([campaignId, status, queuedAt])
@@index([campaignId, sentAt])
@@index([emailId])
@@index([subscriptionId])
@@index([contactId])
@@index([email])
@@index([resendEmailId])
@@map("email_campaign_targets")
}
model BlockedPhoneNumbers {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
phoneNumber String @unique
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
@@index([subscriptionId])
@@index([phoneNumber])
@@map("blocked_phone_numbers")
}
/**
* Content variation for an email campaign.
* Multiple active variations ensure recipients see different content.
*/
model EmailVariation {
id String @id @default(uuid()) @db.Uuid
campaignId String @db.Uuid
subscriptionId String @db.Uuid
name String
subject String
htmlBody String @db.Text
// Optional Resend template reference
resendTemplateId String?
templateType String @default("custom") // "custom" | "resend_template"
// Resend template variables – array of { key, type, fallbackValue }
templateVariables Json @default("[]")
status EmailVariationStatus @default(active)
spamScore Int?
markedAsSpam Boolean @default(false)
usageCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
campaign Campaign @relation(fields: [campaignId], references: [id])
subscription Subscription @relation(fields: [subscriptionId], references: [id])
targets EmailCampaignTarget[]
@@index([campaignId, status])
@@index([subscriptionId])
@@map("email_variations")
}
enum DomainWarmupStatus {
warming_up
active
}
/**
* Parent sending domain per subscription — one row per Resend domain (e.g. payability.com
* or a subdomain sales.payability.com). Holds the Resend domain id, a daily cap shared
* across all its email accounts, and a warm-up status label.
*/
model Domain {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
name String // e.g. payability.com or sales.payability.com
resendDomainId String?
resendStatus String? // mirror of Resend status at link time
dailyCap Int? // null = unlimited
warmupStatus DomainWarmupStatus @default(active)
isActive Boolean @default(true)
totalSentCount Int @default(0) // lifetime sends across all accounts; never reset by the daily cron
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
sendingDomains EmailSendingDomain[]
@@unique([subscriptionId, name])
@@index([subscriptionId])
@@map("domains")
}
/**
* Sending domain registry per subscription (an "email account" in the UI).
* Supports multiple from-addresses that belong to a parent Domain for campaign rotation.
*/
model EmailSendingDomain {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
domainId String? @db.Uuid
domain String
fromName String
fromEmail String
resendDomainId String?
isVerified Boolean @default(false)
isActive Boolean @default(true)
dailyCap Int? // per-account daily cap; null = unlimited
dailySentCount Int @default(0) // reset daily by cron; distinct from lifetime sentCount
totalSentCount Int @default(0) // lifetime handoff count, bumped with dailySentCount; never reset
sentCount Int @default(0)
receivedCount Int @default(0)
openedCount Int @default(0)
clickedCount Int @default(0)
bouncedCount Int @default(0)
unsubscribedCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
parentDomain Domain? @relation(fields: [domainId], references: [id])
emails Emails[]
@@unique([subscriptionId, fromEmail])
@@index([subscriptionId])
@@index([domainId])
@@map("email_sending_domains")
}
model TargetPhoneNumbers {
id String @id @default(uuid()) @db.Uuid
phoneNumber String @unique
subscriptionId String @db.Uuid
contactId String? @db.Uuid
countryCode String?
carrierName String?
mobileCountryCode String?
mobileNetworkCode String?
syncPriority Int @default(0)
isActive Boolean @default(true)
isDeleted Boolean @default(false)
isBlocked Boolean @default(false)
syncTwilio Boolean @default(false)
syncHubspot Boolean @default(false)
lineType LineTypes?
riskLevel RiskLevel?
hubspotSyncedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
campaignTargets CampaignTarget[]
calls Call[] @relation("CallRegistryTargetPhone")
@@index([contactId])
@@map("target_phone_numbers")
}
model TargetEmails {
id String @id @default(uuid()) @db.Uuid
email String
subscriptionId String @db.Uuid
contactId String? @db.Uuid
isActive Boolean @default(true)
isDeleted Boolean @default(false)
isBlocked Boolean @default(false)
newsletterOptOut Boolean @default(false)
newsletterOptOutAt DateTime?
oneToOneOptOut Boolean @default(false)
oneToOneOptOutAt DateTime?
marketingOptOut Boolean @default(false)
marketingOptOutAt DateTime?
newsletterGapDays Int? @default(7)
newsletterLastSentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
emailCampaignTargets EmailCampaignTarget[]
@@unique([subscriptionId, email])
@@index([subscriptionId])
@@index([subscriptionId, isDeleted, isBlocked, newsletterOptOut])
@@index([contactId])
@@map("target_emails")
}
enum EmailDirection {
sent
received
}
/**
* Inbox list/detail taxonomy (filters: Campaign, Reply, Transactional, Other).
* Distinct from EmailTargetCategory (LLM reply classification on EmailCampaignTarget).
*/
enum EmailInboxCategory {
campaign
reply
transactional
other
}
model Emails {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
sendingDomainId String? @db.Uuid
contactId String? @db.Uuid
campaignId String? @db.Uuid
/// Email campaign target row when this message belongs to a campaign send/reply chain.
emailCampaignTargetId String? @db.Uuid
/// Shared by all messages in a conversation; new UUID for each new thread.
threadId String @db.Uuid
direction EmailDirection
inboxCategory EmailInboxCategory?
/// LLM-derived category when synced from EmailCampaignTarget (optional).
category EmailTargetCategory?
/// Resend email_id (outbound or inbound).
externalId String
/// Parent Message-Id / Resend id for reply correlation (optional).
inReplyToExternalId String?
toEmail String
fromEmail String
subject String
body String @db.Text
bodyHtml String? @db.Text
/// Pixel-open tracking (Resend email.opened).
opened Boolean @default(false)
/// In-app read state (separate from opened).
readAt DateTime?
archivedAt DateTime?
sentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
sendingDomain EmailSendingDomain? @relation(fields: [sendingDomainId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
campaign Campaign? @relation(fields: [campaignId], references: [id])
emailCampaignTarget EmailCampaignTarget? @relation(fields: [emailCampaignTargetId], references: [id])
contactSequenceSteps ContactSequenceStep[]
@@unique([subscriptionId, externalId])
@@index([subscriptionId, createdAt(sort: Desc)])
@@index([subscriptionId, threadId, createdAt])
@@index([subscriptionId, archivedAt])
@@index([toEmail])
@@index([fromEmail])
@@index([contactId])
@@index([emailCampaignTargetId])
@@map("emails")
}
/**
* Represents a sales rep who is assigned to follow up on leads.
* Subscription-scoped so the same Supabase user can have different CRM IDs
* across workspaces. profileId is optional — a rep can be added manually
* before they have a Supabase account.
*/
model SalesRep {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
// Optional link to a Supabase-authenticated user
profileId String? @db.Uuid
name String
email String
// CRM identifiers
hubspotOwnerId String? // HubSpot owner ID (used to assign tasks/contacts in HubSpot)
nooksUserId String? // Nooks user ID (used to assign tasks and sequences in Nooks)
activeListId String? // Active list ID (used the sales rep sequences in Hubspot)
hubspotCallSegmentId String? // HubSpot call segment ID (used to assign calls to the sales rep)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
profile Profile? @relation(fields: [profileId], references: [id])
followUpTasks FollowUpTask[]
calls Call[]
@@unique([subscriptionId, email])
@@index([subscriptionId])
@@index([profileId])
@@index([hubspotOwnerId])
@@index([nooksUserId])
@@map("sales_reps")
}
/**
* Tracks follow-up tasks created for sales reps against leads.
* A task can be mirrored in HubSpot (hubspotTaskId) and/or Nooks (nooksTaskId)
* and optionally backed by a Nooks sequence enrollment (nooksSequenceStateId).
* Both external IDs are nullable — a task may only be pushed to one platform
* depending on which integrations are active for the workspace.
*/
model FollowUpTask {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
// Who the task is assigned to
salesRepId String @db.Uuid
// What lead the task is about
contactId String @db.Uuid
campaignTargetId String? @db.Uuid
contactSequenceId String? @db.Uuid
type FollowUpTaskType @default(call)
status FollowUpTaskStatus @default(pending)
// External system IDs
hubspotTaskId String? // HubSpot CRM Task object ID
nooksTaskId String? // Nooks task ID
nooksSequenceStateId String? // Nooks sequence enrollment ID (when type = sequence_enrollment)
// Sync status per platform
hubspotSyncStatus String? // pending | synced | failed
nooksSyncStatus String? // pending | synced | failed
notes String?
escalationStage Int @default(0) // 0→1→2→3=failed
inngestRunId String? // store run ID so you can cancel it programmatically
lastEscalatedAt DateTime?
// Scheduling
dueAt DateTime? @db.Timestamptz(6)
lastHubspotSyncAt DateTime? @db.Timestamptz(6)
lastNooksSyncAt DateTime? @db.Timestamptz(6)
completedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
salesRep SalesRep @relation(fields: [salesRepId], references: [id])
contact Contact @relation(fields: [contactId], references: [id])
campaignTarget CampaignTarget? @relation(fields: [campaignTargetId], references: [id])
contactSequence ContactSequence? @relation(fields: [contactSequenceId], references: [id])
activities Activity[]
contactSequenceSteps ContactSequenceStep[]
@@index([subscriptionId])
@@index([salesRepId])
@@index([contactId])
@@index([campaignTargetId])
@@index([status])
@@index([dueAt])
@@index([hubspotTaskId])
@@index([nooksTaskId])
@@map("follow_up_tasks")
}
model SequenceTemplate {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
name String
goal String? // sequence goal description for the ai to understand
description String?
callCampaignDefault Boolean @default(false)
emailCampaignDefault Boolean @default(false)
smsCampaignDefault Boolean @default(false)
type SequenceType
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
subscription Subscription @relation(fields: [subscriptionId], references: [id])
steps SequenceTemplateStep[]
contactSequences ContactSequence[]
@@index([subscriptionId])
@@map("sequence_templates")
}
model SequenceTemplateStep {
id String @id @default(uuid()) @db.Uuid
templateId String @db.Uuid
stepIndex Int
delayHours Int // wait steps: the delay amount (interpreted per delayUnit)
delayUnit String @default("hours") // "hours" | "minutes" — only meaningful for wait steps
timeoutHours Int? // for call_task: how long rep has
name String? // step name for the ai to understand
description String? // step description for the ai to understand
instructions String? // step instructions for the ai to understand
expectedOutcome String? // step expected outcome for the ai to understand
emailTone String? // warm | direct | social_proof | urgency | breakup
emailSubjectHint String? // optional hint to AI writer
smsTemplate String? // for SMS steps
isAiGenerated Boolean @default(true) // use AI writer or fixed template
fixedEmailId String? @db.Uuid // if not AI, point to a fixed EmailVariation
channel SequenceStepChannel // email | call_task | sms | wait | data_gate | crm_task
// Data gate / branching (only used when channel = data_gate).
// The gate runs a BigQuery check against `gateBigqueryView` and routes the
// sequence to a different step based on the boolean result.
gateBigqueryView String? // BQ view name (e.g. "outreach_app.contacts"); must expose a hubspotcontactid column
// Multiple conditions evaluated against a single row for this contact.
// Shape: [{ field: string, operator: string, value: string | null }]
// operator: is_not_null | is_null | equals | not_equals | gt | lt | is_in | is_not_in
gateConditions Json? // list of field checks; combined per gateMatchMode
gateMatchMode String @default("all") // "all" (AND) | "any" (OR)
// Legacy single-condition fields (kept for backward compatibility; new writes use gateConditions).
gateField String? // deprecated: column to evaluate
gateOperator String? // deprecated: operator
gateValue String? // deprecated: comparison value
trueBranchStepIndex Int? // step to jump to when the condition is TRUE
falseBranchStepIndex Int? // step to jump to when the condition is FALSE
gateRetryDelayHours Int? // when set, FALSE waits this long and re-checks instead of jumping
gateMaxRetries Int? // cap on re-checks before the sequence is abandoned (null = unbounded)
// CRM task config (only used when channel = crm_task).
crmTaskType String? // call | email | follow_up
crmTaskDescription String? // task body / notes
crmTaskDueHours Int? // due-date offset in hours from execution time
extraConfigs Json?
template SequenceTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
executedSteps ContactSequenceStep[]
@@unique([templateId, stepIndex])
@@index([templateId])
@@map("sequence_template_steps")
}
model ContactSequence {
id String @id @default(uuid()) @db.Uuid
subscriptionId String @db.Uuid
contactId String @db.Uuid
templateId String @db.Uuid
currentStepIndex Int @default(0)
nextStepAt DateTime? @db.Timestamptz(6) // when to execute next step
// Inngest run id of the single execution that drives this sequence end-to-end.
// All ContactSequenceStep logs for this run share the same executionId.
executionId String?
// Link back to what triggered this
campaignTargetId String? @db.Uuid
emailCampaignTargetId String? @db.Uuid
campaignId String? @db.Uuid
startedAt DateTime? @db.Timestamptz(6)
completedAt DateTime? @db.Timestamptz(6)
abandonedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
status SequenceStatus @default(pending)
subscription Subscription @relation(fields: [subscriptionId], references: [id])
template SequenceTemplate @relation(fields: [templateId], references: [id])
contact Contact? @relation(fields: [contactId], references: [id])
campaign Campaign? @relation(fields: [campaignId], references: [id])
campaignTarget CampaignTarget? @relation(fields: [campaignTargetId], references: [id])
emailCampaignTarget EmailCampaignTarget? @relation(fields: [emailCampaignTargetId], references: [id])
steps ContactSequenceStep[]
followUpTasks FollowUpTask[]
// "One active sequence per contact" is enforced in application code
// (SequenceExecutionService.enrollContact), not by a DB unique constraint —
// a (contactId, status) unique wrongly blocks multiple terminal/paused rows.
@@index([contactId]) // lookups by contact (was covered by the removed unique)
@@index([status, nextStepAt]) // scheduler query
@@index([executionId])
@@index([subscriptionId])
@@index([campaignId])
@@index([campaignTargetId])
@@index([emailCampaignTargetId])
@@map("contact_sequences")
}
model ContactSequenceStep {
id String @id @default(uuid()) @db.Uuid
sequenceId String @db.Uuid
// Nullable + SetNull: a template step can be removed while editing a template
// without destroying this historical log — the link is severed, the log kept.
templateStepId String? @db.Uuid
// Inngest run id of the execution that produced this log (mirrors ContactSequence.executionId).
executionId String?
status String @default("pending") // pending | executed | skipped | failed
// Number of times a data_gate step has re-checked its condition (retry loop guard).
gateRetryCount Int @default(0)
// What was sent/created
callId String? @db.Uuid // FK to Call
emailId String? @db.Uuid // FK to Emails
followUpTaskId String? @db.Uuid // FK to FollowUpTask (for call_task steps)
activityId String? @db.Uuid // FK to Activity (for SMS)
executedAt DateTime? @db.Timestamptz(6)
outcome String? // replied | opened | connected | no_answer | no_reply
// Full response payload from this execution (provider ids, CRM event body,
// gate evaluation details, etc.) — one row per execution, never overwritten.
response Json?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
sequence ContactSequence @relation(fields: [sequenceId], references: [id])
call Call? @relation(fields: [callId], references: [id])
email Emails? @relation(fields: [emailId], references: [id])
followUpTask FollowUpTask? @relation(fields: [followUpTaskId], references: [id])
activity Activity? @relation(fields: [activityId], references: [id])
templateStep SequenceTemplateStep? @relation(fields: [templateStepId], references: [id], onDelete: SetNull)
@@index([sequenceId])
@@index([executionId])
@@index([templateStepId])
@@index([callId])
@@index([emailId])
@@index([followUpTaskId])
@@index([activityId])
@@map("contact_sequence_steps")
}