Overview
Campaign Management enables users to create scheduled WhatsApp message campaigns for groups or individuals. Each campaign consists of multiple scheduled messages with automatic progress tracking and delivery monitoring.
Campaign Lifecycle
Create Campaign
Define campaign parameters including target audience, date range, message template, and scheduling options.
Schedule Messages
Messages are automatically generated based on the date range and recurrence pattern.
Track Progress
Monitor campaign status, message delivery, and completion percentage in real-time.
Complete or Update
Campaigns can be updated (before messages are sent) or marked complete when all messages are delivered.
Campaign Status
Campaigns progress through different states:
enum CampaignStatus {
DRAFT // Not yet scheduled
SCHEDULED // Ready to send messages
IN_PROGRESS // Some messages sent
COMPLETED // All messages delivered
CANCELLED // Campaign stopped by user
FAILED // Delivery errors occurred
}
Creating a Campaign
API Endpoint
src/server/api/routers/messageCampaign.ts:9
createCampaign : protectedProcedure
. input (
z . object ({
groupId: z . string (),
groupName: z . string (),
sessionId: z . string (),
title: z . string (). optional (),
targetAmount: z . string (). optional (),
startDate: z . string (),
endDate: z . string (),
messageTime: z . string (). regex ( / ^ \d {1,2} : \d {2} $ / ),
timeZone: z . string (). default ( 'America/Chicago' ),
messageTemplate: z . string (),
isFreeForm: z . boolean (). default ( false ),
isRecurring: z . boolean (),
recurrence: z . enum ([ 'DAILY' , 'WEEKLY' , 'SEMI_MONTHLY' , 'MONTHLY' , 'SEMI_ANNUALLY' , 'ANNUALLY' ]). default ( 'DAILY' ),
audienceType: z . enum ([ 'groups' , 'individuals' ]). default ( 'groups' ),
})
)
. mutation ( async ({ ctx , input }) => {
// Campaign creation logic
})
WhatsApp group ID or contact ID for the campaign target
Display name for the group or contact
WhatsApp session ID to use for sending messages
Optional campaign title (displayed in messages unless isFreeForm is true)
Optional contribution target amount for fundraising campaigns
ISO 8601 date string for campaign start (e.g., “2026-03-10”)
ISO 8601 date string for campaign end (must be after startDate)
Time to send messages daily in HH:MM format (e.g., “09:30”)
timeZone
string
default: "America/Chicago"
IANA timezone identifier for scheduling (e.g., “America/New_York”)
Message content. Use asterisks (*) to separate messages for recurring campaigns
If true, only sends the message template without campaign metadata
Whether to send messages on a recurring schedule
Recurrence pattern: DAILY, WEEKLY, SEMI_MONTHLY, MONTHLY, SEMI_ANNUALLY, ANNUALLY
Target audience type: ‘groups’ or ‘individuals’
Message Template Variables
Templates support dynamic variables:
{days_left} - Replaced with remaining days in campaign
Standard Format
Free Form (isFreeForm: true)
Campaign Title: Spring Fundraiser
Campaign Start Date: 2026-03-10
Campaign End Date: 2026-03-20
Contribution Target Amount: $5000
Days Remaining: 7
Please consider contributing today! Only {days_left} days left.
Multiple Message Sequences
For recurring campaigns, separate messages with asterisks to create unique content for each occurrence:
Welcome to our campaign! *
Day 2 - Thank you for your support! *
Day 3 - We're halfway there! *
Final day - Last chance to contribute!
The number of messages must match the calculated occurrences. For example, a 4-day DAILY campaign requires exactly 4 messages separated by asterisks, or one message without asterisks to repeat.
Recurrence Patterns
Recurrence intervals are mapped to day counts:
src/server/api/routers/messageCampaign.ts:31
const recurrenceDaysMap = {
DAILY: 1 ,
WEEKLY: 7 ,
SEMI_MONTHLY: 15 ,
MONTHLY: 30 ,
SEMI_ANNUALLY: 182 ,
ANNUALLY: 365 ,
}
Messages sent every day at the specified time {
isRecurring : true ,
recurrence : 'DAILY'
}
Messages sent every 7 days {
isRecurring : true ,
recurrence : 'WEEKLY'
}
Messages sent every 30 days {
isRecurring : true ,
recurrence : 'MONTHLY'
}
Viewing Campaigns
Active Campaigns
Get campaigns with upcoming scheduled messages:
src/server/api/routers/messageCampaign.ts:251
getCampaigns : protectedProcedure
. query ( async ({ ctx }) => {
return await ctx . db . messageCampaign . findMany ({
where: {
isDeleted: false ,
messages: {
some: {
scheduledAt: { gt: new Date () },
}
},
session: {
userId: ctx . session . user . id ,
}
},
include: {
group: true ,
messages: true ,
},
orderBy: {
createdAt: 'desc'
},
});
})
Completed Campaigns
Get campaigns where all messages have been sent or scheduled time has passed:
src/server/api/routers/messageCampaign.ts:185
getCompletedCampaigns : protectedProcedure
. query ( async ({ ctx }) => {
return await ctx . db . messageCampaign . findMany ({
where: {
isDeleted: false ,
messages: {
every: {
OR: [
{ isSent: true },
{ scheduledAt: { lt: new Date () } },
]
}
},
session: {
userId: ctx . session . user . id ,
}
},
orderBy: {
endDate: 'desc'
},
});
})
Updating Campaigns
Campaigns can only be updated if no messages have been sent yet. Once messages start sending, the campaign becomes read-only.
src/server/api/routers/messageCampaign.ts:403
// Check if any messages have already been sent
const hasSentMessages = existingCampaign . messages ?. some (( m : any ) => m . isSent ) ?? false ;
if ( hasSentMessages ) {
throw new Error ( "Cannot edit campaign with messages that have already been sent" );
}
The update process:
Validate Access
Verify the campaign belongs to the current user and exists
Check Message Status
Ensure no messages have been sent yet
Mark Old Messages as Deleted
Soft delete all unsent messages from the original campaign
Regenerate Messages
Create new messages based on updated parameters
Deleting Campaigns
Campaigns are soft-deleted to preserve historical data:
src/server/api/routers/messageCampaign.ts:319
deleteCampaign : protectedProcedure
. input ( z . object ({
campaignId: z . string (),
}))
. mutation ( async ({ ctx , input }) => {
await ctx . db . messageCampaign . update ({
where: {
id: input . campaignId ,
session: { userId: ctx . session . user . id }
},
data: {
isDeleted: true ,
messages: {
updateMany: {
where: {
MessageCampaignId: input . campaignId ,
},
data: {
isDeleted: true ,
},
}
},
},
});
return { success: true };
})
Campaign Data Model
model MessageCampaign {
id String @ id @ default ( cuid ())
sessionId String
groupId String
title String ?
targetAmount String ?
startDate DateTime
endDate DateTime
sendTimeUtc DateTime
timeZone String @ default ( "America/Chicago" )
template String
messages Message []
createdAt DateTime @ default ( now ())
updatedAt DateTime @ updatedAt
isDeleted Boolean @ default ( false )
isEdited Boolean @ default ( false )
isCompleted Boolean @ default ( false )
status CampaignStatus @ default ( SCHEDULED )
recurrence Recurrence ?
isRecurring Boolean @ default ( false )
nextSendAt DateTime ?
session WhatsAppSession @ relation ( fields : [ sessionId ], references : [ id ], onDelete : Cascade )
group WhatsAppGroup @ relation ( fields : [ groupId ], references : [ id ], onDelete : Cascade )
}
Progress Tracking
Track campaign progress through message status:
Total Messages Count of all non-deleted messages in the campaign
Sent Messages Messages where isSent = true
Failed Messages Messages where isFailed = true
Calculate completion percentage:
const totalMessages = campaign . messages . filter ( m => ! m . isDeleted ). length ;
const sentMessages = campaign . messages . filter ( m => m . isSent ). length ;
const completionPercentage = ( sentMessages / totalMessages ) * 100 ;
Best Practices
Always specify the timezone for accurate scheduling
Messages are stored in UTC but scheduled in the specified timezone
Use IANA timezone identifiers (e.g., “America/New_York”)
Keep messages concise for better engagement
Use the variable for urgency
Test templates before creating large campaigns
Verify date ranges before creating campaigns
For recurring campaigns with sequences, calculate required message count
Use free-form mode for simple announcements
Next Steps
Message Scheduling Learn about timezone handling and recurrence patterns
Admin Dashboard Monitor all campaigns across users