Skip to content
Faidhi Fahmi.Contact
All selected works
TechnicalProduct

A campaign as a state machine, not a send list

Organisation
tmrwX · Family Hub campaign engine
Role
Product architecture, workflow design and delivery
Period
2026 · Malaysia

Rebuilding a flat sequential campaign builder into an event-driven engine that pauses the moment a real person replies, and hands the relationship to an adviser with a timer on it.

At a glance

Situation
The campaign engine was a flat sequential builder. An operator picked an audience and queued the sends, but the engine kept sending after real customers replied mid-sequence.
Stakes
Duplicate and tone-deaf messages under live customers cost trust directly, and warm leads went cold because no adviser knew the moment a lead became theirs.
My role
Product architecture, workflow design and delivery of the migration to an event-driven state machine.
Constraints
Live campaigns running through the migration, historical reporting that could not be lost, and a shared WhatsApp business sender.
What changed
  • Duplicate dispatches stopped after the migration
  • Any customer journey is reconstructable from the append-only event ledger
  • Advisers receive real-time handoffs with an SLA timer instead of discovering leads late
Read this if
Your marketing automation misbehaves the moment customers act like humans instead of a send list.
dispatched after the migration
No duplicatesdispatched after the migration
reconstructable from the event ledger
Full journeyreconstructable from the event ledger
handoffs on the adviser dashboard
SLA-timedhandoffs on the adviser dashboard

How the system actually worked

A campaign as a state machine, not a send list, figure 1
Campaign Studio, workflow view. Operators configure timing and toggle optional steps. The transitions are fixed, which stops a campaign being reshaped mid-flight under live customers.
Before: a flat sequential send listThree scheduled sends in a row. A customer reply arrives but has nowhere to go: the sequence continues regardless, and tomorrow's nudge still goes out.Before: a flat send listSend 1scheduledSend 2scheduledSend 3scheduledCustomer replies“actually, I'm interested”The reply changes nothing. Tomorrow's nudge goes out anyway.

Scroll the figure sideways to read it, or turn your phone.

Nine steps, from enrolment to a confirmed visit.
After: an event-driven state machine across three lanesThree lanes. Customer: receives a message, then replies on WhatsApp. Automation engine: eligible, enrolled, in sequence, sending. The reply crosses into the engine and hits a worker-aware lock, which is a gate. Passing the gate appends replied and automation_paused to an append-only ledger and stops automation. Ownership then transfers to the adviser lane with a two-hour SLA timer attached, ending at visit confirmed. Opt-out is a terminal state reachable at any point. An append-only event ledger underlies every lane.CustomerAutomation engineAdviserEligibleEnrolledIn sequenceWhatsApp, SMS,on a schedulesendReceivesmessageReplies onWhatsAppa real human signalhuman signalWorker-awarelock1replied → paused2Automationpaused3T+2h SLA4Adviser ownsthe leadVisitconfirmedOpted outterminalat any pointAppend-only event ledgerenrolled · sent · opened · clicked · replied · booked · attended · opted out

Scroll the figure sideways to read it, or turn your phone.

  1. Every transition runs through one deterministic handler with worker-aware locking. Two workers evaluating the same enrolment cannot both dispatch.
  2. The ledger is append-only. Enrolled, sent, opened, clicked, replied, booked, attended, opted out. The whole journey is reconstructable from it.
  3. A human signal pauses automation instantly. Someone who replies does not receive tomorrow's scheduled nudge.
  4. Handoff starts a clock. The adviser dashboard shows the timer, so a warm lead going cold is visible rather than discovered late.
The same engine as a state diagram, including the lock and ledger write that every human signal passes through, and the point where automation stops and an adviser owns the lead.

A campaign as a state machine, not a send list, figure 3
The adviser workspace, lead identifiers redacted. One priority queue worked top to bottom, grouped by campaign state and the action that moves the journey forward, rather than a list of everything.
A campaign as a state machine, not a send list, figure 4
Follow-up tracker: handoff tasks, visit reminders and post-visit sequences, with overdue and due-today counts so nothing sits unworked.
A campaign as a state machine, not a send list, figure 5
The WhatsApp inbox on a shared business sender. A reply to a campaign message is precisely what triggers the handoff into this view.
A campaign as a state machine, not a send list, figure 6
Approved outreach templates with their variables, scoped to the moment each is for. An adviser picks a template, not a blank message box.
A campaign as a state machine, not a send list, figure 7
Advisor management: onboarding state, live load per adviser and remaining capacity. Assignment needs this to be a screen rather than a spreadsheet, and it is where team capacity becomes a routing input.
A campaign as a state machine, not a send list, figure 8
Activity and slot scheduling. What is toggled on here is exactly what a lead can choose in the public form: one source of truth, so a customer cannot book a slot that does not exist.
A campaign as a state machine, not a send list, figure 9
The public form the campaign links to, served on a signed per-lead URL so a submission ties back to a known contact instead of arriving anonymous.

Stack & practices

  • Express
  • TypeScript
  • PostgreSQL
  • Socket.IO
  • Infobip
  • Twilio
  • React

Got a problem shaped like this one?

A short conversation is usually enough to see if there's a fit.