Skip to main content

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

1

Create Campaign

Define campaign parameters including target audience, date range, message template, and scheduling options.
2

Schedule Messages

Messages are automatically generated based on the date range and recurrence pattern.
3

Track Progress

Monitor campaign status, message delivery, and completion percentage in real-time.
4

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:
prisma/schema.prisma:19
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
  })

Input Parameters

groupId
string
required
WhatsApp group ID or contact ID for the campaign target
groupName
string
required
Display name for the group or contact
sessionId
string
required
WhatsApp session ID to use for sending messages
title
string
Optional campaign title (displayed in messages unless isFreeForm is true)
targetAmount
string
Optional contribution target amount for fundraising campaigns
startDate
string
required
ISO 8601 date string for campaign start (e.g., “2026-03-10”)
endDate
string
required
ISO 8601 date string for campaign end (must be after startDate)
messageTime
string
required
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”)
messageTemplate
string
required
Message content. Use asterisks (*) to separate messages for recurring campaigns
isFreeForm
boolean
default:false
If true, only sends the message template without campaign metadata
isRecurring
boolean
required
Whether to send messages on a recurring schedule
recurrence
enum
default:"DAILY"
Recurrence pattern: DAILY, WEEKLY, SEMI_MONTHLY, MONTHLY, SEMI_ANNUALLY, ANNUALLY
audienceType
enum
default:"groups"
Target audience type: ‘groups’ or ‘individuals’

Message Template Variables

Templates support dynamic variables:
  • {days_left} - Replaced with remaining days in campaign
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'
}

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:
1

Validate Access

Verify the campaign belongs to the current user and exists
2

Check Message Status

Ensure no messages have been sent yet
3

Mark Old Messages as Deleted

Soft delete all unsent messages from the original campaign
4

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

prisma/schema.prisma:77
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

Build docs developers (and LLMs) love