Skip to content
Faidhi Fahmi.
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.

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 themselves are fixed, which is what 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.

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 scheduleReceivesmessageReplies onWhatsAppa real human signalsendhuman 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 — not 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, not 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.

Context

The campaign engine was a sequential builder: pick an audience, queue a series of sends. It worked right up until real customers started replying in the middle of a sequence.

What was actually needed

Campaigns that react to what the customer does. Someone who replies on WhatsApp should not receive tomorrow’s scheduled nudge. Someone who books should move onto a confirmation path, not a persuasion path. And an adviser needs to know the moment a lead becomes theirs, not the next morning.

What I did technically

I specified the migration to an event-driven state machine. Programs define the objective and content; enrolments carry each customer’s current state and automation status; an append-only ledger records everything that happened — enrolled, sent, opened, clicked, replied, booked, attended, opted out. Every transition runs through one deterministic handler with worker-aware locking. Automation pauses the instant a human signal arrives and ownership transfers to the assigned adviser with an SLA timer attached.

How the migration was handled

The old flat campaign builder was kept as a read-only historical facade rather than deleted, so past reporting stayed intact while new creation paths were blocked. Migrations that throw away history get reversed the first time someone asks what happened last quarter.

Decisions that mattered

Idempotency and locking before features. The failure that actually costs trust is not a missing capability — it is the same customer receiving the same WhatsApp message three times because two workers evaluated the same enrolment at the same moment.

What changed

Duplicate dispatches stopped. Any customer’s journey can be reconstructed from the event ledger. Advisers get real-time handoff notifications with a timer, rather than discovering late that a warm lead went cold.

The insight I kept

Marketing automation is a distributed systems problem that happens to send messages. Once I started treating it as states, transitions, locks and an event log, the operational complaints stopped arriving.

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.