Without a state machine model, sequence position exists only as an implicit calculation derived from send timestamps — and that calculation breaks the moment history is incomplete. A cron loop that re-derives step index on every run will duplicate sends when emails are delayed, skip steps when records are missing, and produce divergent behavior across contacts that should be in identical states. CWE-372 (Incomplete Internal State) captures the class of defect: the system behaves correctly in the happy path but enters an undefined state under any pressure — process restart, partial failure, manual intervention. The business impact is concrete: contacts receive redundant outreach, or fall silent mid-sequence, with no audit trail to diagnose either.
Critical because missing per-contact state causes duplicate sends and irreversible data loss on any process restart or partial failure.
Add a contact_sequences enrollment table with an explicit status enum and advance contacts through transitions — never re-derive step position from history. The status column must carry at least active, paused, completed, replied, and opted_out as distinct values.
// contacts_in_sequences table
// columns: id, contact_id, sequence_id, current_step_index,
// status ('active'|'paused'|'completed'|'replied'|'opted_out'), updated_at
async function advanceContact(enrollmentId: string) {
const enrollment = await db.contactSequence.findUnique({ where: { id: enrollmentId } })
if (enrollment.status !== 'active') return // Guard: invalid transition
const nextStep = sequence.steps[enrollment.currentStepIndex + 1]
if (!nextStep) {
await db.contactSequence.update({ where: { id: enrollmentId }, data: { status: 'completed' } })
return
}
await db.contactSequence.update({
where: { id: enrollmentId },
data: { currentStepIndex: enrollment.currentStepIndex + 1 }
})
await scheduleStep(enrollmentId, nextStep)
}
ID: campaign-orchestration-sequencing.sequence-architecture.state-machine-model
Severity: critical
What to look for: Examine how sequences are structured in code. A proper state machine approach means each step/state is explicitly defined, transitions between steps are governed by conditions (not just time delays), and a contact's current position in the sequence is always recoverable from persisted state. Look for: explicit state enums or constants (e.g., PENDING, SENT, WAITING_REPLY, COMPLETED, OPTED_OUT), transition functions that validate the current state before moving to the next, and per-contact state records in the database. Red flags: sequences implemented as a single loop or cron job that iterates all contacts linearly with no per-contact state, step logic that re-calculates which step a contact is on from scratch each time, or hard-coded sleep/delay calls in a sequential function.
Pass criteria: Sequence steps and contact positions are modeled as explicit states. Transitions are governed by defined conditions. A contact's state is queryable from the database without reprocessing history. Enumerate all state values used in the codebase and count the distinct states — at least 3 must exist (e.g., active, completed, paused). Report the count even on pass.
Fail criteria: Sequences run as linear scripts or loops with no per-contact state tracking. Steps are inferred from elapsed time rather than persisted state. No concept of "current state" for a contact. A single boolean completed field does not count as pass — it must be a multi-state model.
Skip (N/A) when: The project has no sequence or drip campaign functionality.
Cross-reference: The Campaign Analytics & Attribution Audit covers tracking event accuracy, which depends on reliable sequence state to attribute engagement to the correct step.
Detail on fail: Describe the pattern found. Example: "Sequences implemented as a cron loop that queries all contacts and re-evaluates step index based on send timestamps — no persisted state per contact" or "Single async function with setTimeout calls between steps — state lost on process restart"
Remediation: Model sequences as state machines with a per-contact enrollment record:
// contacts_in_sequences table
// id, contact_id, sequence_id, current_step_index, status ('active'|'paused'|'completed'|'replied'), updated_at
type SequenceStatus = 'active' | 'paused' | 'completed' | 'replied' | 'opted_out'
async function advanceContact(enrollmentId: string) {
const enrollment = await db.contactSequence.findUnique({ where: { id: enrollmentId } })
// Only advance if in a valid state for advancement
if (enrollment.status !== 'active') return
const nextStep = sequence.steps[enrollment.currentStepIndex + 1]
if (!nextStep) {
await db.contactSequence.update({ where: { id: enrollmentId }, data: { status: 'completed' } })
return
}
await db.contactSequence.update({
where: { id: enrollmentId },
data: { currentStepIndex: enrollment.currentStepIndex + 1 }
})
await scheduleStep(enrollmentId, nextStep)
}