WF-D1 · Eligibility, Segmentation and Batch Assignment
Trigger: Run manually per wave, or on a schedule once the first wave has proven itself.
Exclude before you include. Build the exclusion list first and apply it as the entry condition. A record is excluded, with reactivation_excluded_reason written, if any of these is true:
- consent_basis = None, or consent_captured_at is empty
- tag sms-stop, do-not-contact, or reactivation-excluded
- number_type ≠ Mobile (for the SMS track)
- a hard email bounce, or two or more soft bounces (for the email track)
- an open dispute, complaint, refund, or unresolved service issue
- recontact_after is in the future
- any activity in the last 90 days. They are not dormant, and treating a live contact as dormant is the most embarrassing failure mode of this build
- an open opportunity anywhere in any pipeline
- duplicate of a record already in this wave
Actions, in order:
- Apply the segment definition: one segment per wave, never combined.
- Write reactivation_wave, add the wave tag and reactivation-eligible.
- Create an opportunity in Reactivation → Queued.
- Assign reactivation_batch by dividing the wave into daily batches at or below the send cap: start at 50 per day. Sort the batches so the warmest records go first: a bad first batch tells you the copy is wrong while it is still cheap to fix.
- Hold the whole wave for a human review gate. Export the first batch and have someone read 20 records and the exact message they will receive. Launch nothing until that is signed off. This step prevents every expensive mistake in this build.
Pilot rule: the first batch is 25 records, not 50, and nothing else sends until you have looked at delivery, replies and opt-outs from those 25.
WF-D2 · Wave Send
Trigger: reactivation_batch equals today’s batch number and the record is in Queued.
Conditions: inside the send window (10:00–17:00 local, weekdays); no exclusion tag acquired since assignment; daily cap not yet reached.
Actions:
- Re-run the exclusion check. Days may have passed since assignment and someone may have opted out, complained, or become an active customer in between. Never trust a stale queue.
- Branch on previous_service. If populated, send the specific variant. If empty, send the general variant, and never fabricate a history.
- Send SMS, specific variant: “Hi {{first_name}}: it’s {{sender_first_name}} at {{business_name}}. You came to us for {{previous_service}} a while back and I was going through our books. Are you still looking after that, or would it help to get back in?”
- General variant: “Hi {{first_name}}, {{sender_first_name}} here from {{business_name}}. We spoke a while ago about {{original_source_topic}} and I never heard how you got on. Did you get it sorted in the end?”
- Stamp reactivation_sent_at; set reactivation_touch_count = 1; add reactivation-sent; move to Message Sent.
Why the copy looks like this: it is one short message, from a named person, with exactly one question, and the question is about them rather than about buying. It contains no offer, no link, no emoji, no capitals and no price. Links in a first cold-ish touch depress deliverability and raise complaint rates; earn the link with a reply. The question “did you get it sorted?” is the highest-performing shape in this build because it is answerable in two words and genuinely curious.
Follow-up touches, same workflow:
- Wait 3 days. Condition: no reply, no exclusion. Send: “No worries if the timing’s off, {{first_name}}, just didn’t want to leave it hanging. Want me to send over a couple of times, or shall I leave it with you?” Increment reactivation_touch_count to 2.
- Wait 4 days. Condition: still no reply. Send: “Last one from me: I’ll leave you be. If anything changes, this number reaches me directly. All the best, {{sender_first_name}}.” Increment to 3.
- Wait 2 days. Add reactivation-no-response; move to No Response; set recontact_after = today + 12 months.
Hard cap: three touches. Ever. No exceptions, no fourth “just checking” message. The third message deliberately releases them, which is both decent and the reason the opt-out rate on this build stays low.
Exit conditions: any inbound reply on any channel; any opt-out; appointment booked; an opportunity created elsewhere; exclusion tag applied.
WF-D3 · Reply Router
Trigger: Inbound SMS or email from a contact tagged with any reactivation wave tag.
- Cancel every pending touch in WF-D2 immediately.
- First branch: opt-out language. STOP, UNSUBSCRIBE, REMOVE, CANCEL, END, QUIT, OPT OUT, “take me off”, “don’t contact me”, “how did you get my number” → route to WF-D7 and stop. Treat “how did you get my number” as an opt-out, not a question; answer it once, politely, then suppress.
- Second branch: escalation language. Legal, attorney, lawyer, complaint, report, regulator, ombudsman, ICO, scam, fraud, harassment → no automated reply at all. Notify the owner directly by push and SMS. Suppress the contact from all waves. A human handles it personally.
- Third branch: classification by BO into exactly one of: Interested, Not Now, Negative, Wrong Person, Unclear. Write reply_sentiment and reply_classified_at. Bias toward Unclear. A misread “no” treated as a “yes” is the worst outcome available here.
- Route on classification:
- Interested → tag reactivation-interested, stage Replied: Interested, continue to WF-D4. Notify the team channel.
- Not Now → tag reactivation-not-now, stage Replied, Not Now, continue to WF-D5.
- Negative → tag reactivation-negative, stage Replied: Negative, send one line (“Understood, thanks for letting me know) I won’t chase you again.”, then suppress from all future waves. No rebuttal. No offer. Nothing.
- Wrong Person → correct the record, clear the phone or email, set consent_basis = None, stage Excluded.
- Unclear → no automated reply. Create a task for a human, due in 1 hour, with the message text. Notify the on-duty owner.
- Dead-thread guard: if any inbound message sits unanswered for 15 minutes, escalate to a human regardless of classification. A dormant contact who finally replied and then got silence is worse off than one you never messaged.
WF-D4 · Interested → Booking
Trigger: Tag reactivation-interested added.
- BO replies within seconds. One short message: acknowledge, confirm what they need in one question, offer two specific times. “Great: good to hear from you. Is it {{previous_service}} again, or something different this time?”
- On their answer, send the booking link with two named slots: “I’ve got Thursday 2pm or Friday 10am, either work? Or pick whatever suits here: {{booking_link}}”
- Wait 2 hours. Condition: no booking, no reply. One nudge only: “No rush. Here’s the link whenever you’re ready: {{booking_link}}”
- Wait 1 day. Condition: still no booking. Create a call task for the on-duty owner. An interested reply that did not book is a phone call, not another text.
- Wait 2 days. Condition: still no booking. Final text: “Still happy to find you a time if you want one. Just say the word.” Then stop and leave the record in Replied: Interested for human follow-up. Never return an interested reply to the automated ladder.
- On booking: move to Appointment Booked, tag reactivation-booked, remove all reactivation workflows, hand to the standard confirmation and reminder sequence.
- Escalate to a human immediately on any pricing question, any reference to a past bad experience, or any message BO cannot answer from previous_service and the service list.
WF-D5 · Not Now → Dated Nurture
Trigger: Tag reactivation-not-now added.
- BO asks exactly one question: “Totally fine: roughly when would be better? I’ll make a note and get out of your way.”
- Parse the answer into recontact_after. A month name becomes the 1st of that month; “a few months” becomes today + 90 days; “next year” becomes 1 January; no answer becomes today + 120 days.
- Confirm and close: “Noted: I’ll check back around then. Nothing from me in the meantime.” Then nothing is sent until that date. Honouring this is what makes the later check-in work.
- Wait until recontact_after. Re-run the full exclusion check, then send one message: “Hi {{first_name}}, {{sender_first_name}} at {{business_name}}. You mentioned {{recontact_month}} might suit better for {{previous_service}}. Still useful, or shall I close it off?”
- Route the reply back through WF-D3. A record may go round this loop at most twice; on the third “not now”, move to Excluded and stop asking.
WF-D6 · Parallel Email Track
Trigger: Record is in the wave with a valid email, and either no mobile or no SMS consent.
- Warm the domain first. Day 1: 50 emails. Day 2: 100. Day 3: 200. Only step up while bounce rate stays under 2% and complaint rate under 0.1%. A reactivation blast is the fastest way to destroy a sending domain, and the damage reaches your transactional mail too.
- Email 1, day 0. Plain text, from a named person’s address, no images, no template, no footer graphics. Subject: “Did you get this sorted?” Body: three sentences and one question. A single unsubscribe link, clearly visible.
- Wait 4 days. Condition: no reply, no click. Email 2, subject “One quick thing”. One short paragraph and the landing page link.
- Wait 6 days. Condition: still nothing. Email 3, subject “Closing your file”. Two lines, releases them, link to the re-opt-in page if they want to stay.
- Any open or click → tag as engaged. Any reply → route through WF-D3.
- No engagement after three emails → move to No Response and remove the address from all future campaign sends. Continuing to mail a non-responder is how a list gets worse, not better.
WF-D7 · Opt-Out and Suppression
Trigger: Opt-out language on any channel, a carrier opt-out, an email unsubscribe, or a complaint.
- Set messaging do-not-disturb; set consent_basis = None; add sms-stop and do-not-contact and reactivation-excluded.
- Remove from every active workflow across the whole account, not just this campaign.
- Send one confirmation and nothing further: “Done. You won’t hear from us again. Sorry for the interruption.”
- Move the opportunity to Excluded, reason “opted out”.
- Log it permanently. No import, bulk edit, or future wave may ever clear this. Only an explicit new opt-in from the contact reverses it, and that writes a fresh consent_captured_at and consent_source.
WF-D8 · Wave Monitoring and Circuit Breaker
Trigger: Runs hourly during an active wave.
- Compute, for the current wave: delivery rate, reply rate, opt-out rate, negative-classification share, and complaint count.
- Circuit breaker: pause the entire wave automatically if any of these trips: opt-out rate above 2% of messages delivered; delivery rate below 90%; more than two complaints; negative replies exceeding 25% of all replies. Notify the owner with the numbers.
- A paused wave requires a human to review the copy, the segment, and the consent basis before it can resume. Do not simply restart it.
- Produce a daily wave summary: sent, delivered, replied, classified by sentiment, booked, opted out, and the same figures cumulatively for the wave.
This workflow is the reason this build is safe to run. Without it, a bad segment or a misjudged message keeps sending for three days before anyone notices, and by then the number and the domain are both damaged.