Detecting a reply and not acting on it is worse than not detecting it at all — the system now has evidence that a human responded and chose to continue sending anyway. CWE-841 (Improper Enforcement of Behavioral Workflow) applies: the reply event is received but the workflow does not enforce the expected state transition (active → replied). Pending BullMQ or Sidekiq jobs scheduled before the reply are the specific failure vector: updating the enrollment status in the database does not cancel jobs already enqueued. The contact receives the next step at the scheduled time despite the status update, because the job handler may not re-check enrollment status before sending.
High because detecting a reply without cancelling pending send jobs means the next scheduled step delivers regardless of enrollment status, continuing automated outreach after human engagement.
In your handleReply function, update enrollment status and cancel pending jobs atomically — both actions must happen, not just the status write.
async function handleReply({ contactId, body }: ReplyEvent) {
// Update all active enrollments for this contact
await db.contactSequence.updateMany({
where: { contactId, status: { in: ['active', 'paused'] } },
data: { status: 'replied', repliedAt: new Date() }
})
// Cancel queued jobs — status update alone is not enough
const pendingJobs = await queue.getDelayed()
for (const job of pendingJobs) {
if (job.data.contactId === contactId) await job.remove()
}
// Notify the assigned rep
await notifyRepOfReply(contactId, body)
}
Also add a guard at the top of your send job handler: re-fetch enrollment status and return early if it is not 'active' — this covers the race window between status update and job execution.
ID: campaign-orchestration-sequencing.reply-engagement.reply-pauses-sequence
Severity: high
What to look for: When a reply is detected from a contact, verify that the system immediately pauses or exits that contact from the active sequence. This prevents sending the next scheduled step to a contact who has already engaged by replying. Look for: a handleReply() function or event handler that updates the contact's enrollment status to 'replied' or 'paused', cancellation of pending queue jobs for that contact's enrollment, and a notification mechanism (email or CRM task) to alert a human to the reply.
Pass criteria: When a reply is detected, the contact's sequence enrollment status is immediately updated to paused or exited. Pending send jobs for that contact's enrollment are cancelled or skipped. List all actions taken on reply detection and count them — at least 2 actions must occur (status update + job cancellation).
Fail criteria: Reply detection exists but does not affect sequence execution. Contacts who reply still receive subsequent sequence steps. Or: the enrollment status is updated but pending queue jobs are not cancelled, so the next step sends anyway. Logging the reply without updating enrollment status does not count as pass.
Skip (N/A) when: Reply detection is not implemented (covered by prior check).
Cross-reference: The Compliance & Consent Engine Audit checks that opt-out signals cascade to suppress all active sends, which parallels the reply-pause mechanism.
Detail on fail: "handleReply() sends a Slack notification but does not update enrollment status — contact continues receiving sequence steps" or "Enrollment status updated to 'replied' but pending BullMQ jobs not cancelled — next step sends at scheduled time"
Remediation: Cancel pending jobs when a reply is received:
async function handleReply({ contactId, body }: ReplyEvent) {
// Update all active enrollments for this contact to 'replied'
await db.contactSequence.updateMany({
where: { contactId, status: { in: ['active', 'paused'] } },
data: { status: 'replied', repliedAt: new Date() }
})
// Cancel any pending queue jobs for this contact
const pendingJobs = await queue.getDelayed()
for (const job of pendingJobs) {
if (job.data.contactId === contactId) {
await job.remove()
}
}
// Notify the assigned rep
await notifyRepOfReply(contactId, body)
}