Reminders that stop when they should
The first irreversible step. Four state checks before a message exists — and the silent failure that made this build send to nobody.
Everything you have built so far is reversible. A wrong note can be edited, a wrong stage moved back, a wrong task closed. This lesson is the first thing in this course that cannot be undone. A sent message is sent.
Which changes what a control has to be. Up to now the answer has been "put a gate in front of it". Here the gate has to ask a different question — not may I? but is this still true?
The sentence that gives the game away
This is from a real payment-reminder template in the seeded org. Read the last line:
Your enrolment is one step from done — we have not received payment yet. … "If you have already paid, ignore this — the receipt takes a few minutes to reach us."
That sentence is an apology written in advance for a check the system does not do. It is in thousands of production templates, and every one of them is a team admitting they send reminders without knowing whether the thing being reminded about has already happened.
The one idea. Idempotency, not persuasion. Nobody ever improved collections by writing a better chase. They improved it by not sending the chase to people who had already paid.
Four questions before a single message exists
| Ask | If you skip it | Where it lives |
|---|---|---|
| Have they already paid? | You chase a paying customer for money. The worst one. | A NOT EXISTS on completed payments. |
| Did we already chase them today? | Two identical emails, an hour apart. | A NOT EXISTS on recent activity. |
| Have they unsubscribed? | You have a compliance problem, not a marketing one. | The contact's subscription status. |
| Does the address still work? | You burn your sending reputation on bounces. | The contact's email status. |
Have they already paid?
- If you skip it
- You chase a paying customer for money. The worst one.
- Where it lives
- A NOT EXISTS on completed payments.
Did we already chase them today?
- If you skip it
- Two identical emails, an hour apart.
- Where it lives
- A NOT EXISTS on recent activity.
Have they unsubscribed?
- If you skip it
- You have a compliance problem, not a marketing one.
- Where it lives
- The contact's subscription status.
Does the address still work?
- If you skip it
- You burn your sending reputation on bounces.
- Where it lives
- The contact's email status.
That placement is deliberate. If the checks live in the send step you are filtering a list you have already built; if they live in the query the wrong people were never on the list at all. There is no moment where a bad recipient exists in memory.
The build
Desk — chase the payment, and stop when you should
A daily schedule, one query that carries every pre-send check, a gate on the count, and only then the irreversible step.
Scroll for all 5 steps →
On the real seeded data the checks remove a quarter of the list: 8 people are at Payment Pending, and only 6 should be chased. Two of them have a completed payment sitting against the deal — the stage simply was never moved. They are the ones who would have received that "if you have already paid, ignore this" email.
And note allow_resends: false on the send node. That is the platform's own idempotency control — campaign_send dedups on (campaign_id, contact_id), so even if this workflow fires twice in one morning, nobody is chased twice. Your query is the first line of defence; this is the second.
The run that looked perfect and did nothing
The first version of this workflow ran clean. The scheduler fired, the query returned, the gate evaluated, and the workflow reported success. Here is what the query node actually recorded:
| Field | Value |
|---|---|
| success | true |
| row_count | 0 |
| columns | [] |
| error | — none — |
success
- Value
- true
row_count
- Value
- 0
columns
- Value
- []
error
- Value
- — none —
The SQL had failed. One column name was wrong — a.type where the table has a.activity_type — and the database rejected the whole statement. The node reported success with zero rows, and the workflow took the "nobody to chase today" branch.
A failed query and an empty result are indistinguishable here. Your morning chase silently stops running, every branch behaves reasonably, no alert fires, and you find out when somebody asks why collections dropped. "Nobody to chase today" is a perfectly normal thing to see — which is exactly what makes it such good camouflage.
And underneath it, a second silent failure
Fixing the column revealed that the gate had never worked either. It was written as:
{{chaseable.row_count}} > 0
Which reads perfectly and is wrong. A database_query node publishes exactly two output fields — data and error. There is no row_count. The reference resolved to nothing, the comparison was false every single time, and the send would never have fired even with six people waiting.
So two independent bugs sat on top of each other, and both produced the same innocent outcome: a clean run that chased nobody. Neither would have been found by watching for errors, because there were none. The working version tests the data itself:
{{chaseable.data[0].deal_id}} is not empty
Check the node's real output fields before you reference one. row_count exists in the execution log's metadata, which is exactly why it is such a convincing thing to type.
Three habits fall out of this, and they are all cheap:
- Run every query outside the workflow first. Paste it into a SQL client. A typo becomes an error message instead of a zero.
- Prove the gate can go BOTH ways. A condition that has only ever been observed false is not a condition you have tested — it is one you have watched not fire.
- Treat a zero-row scheduled job as suspicious, not silent. If a daily job that normally finds six people finds none, that is worth a message to somebody — the same absence-detection instinct lesson 13 is entirely about.
A note on this lesson's own build
The send step in the workflow above has never been executed. The demo org has a live email integration and the contacts are seeded addresses, so running it would put real mail on the wire for no teaching benefit — the lesson is the suppression, and the suppression is verifiable from the query. Build it in your own org and point it at yourself before you point it at anyone else.
Next: they have paid. Now the hard part — noticing the ones who paid and then never showed up.
Related lessons
Base rates — what a piece of evidence is actually worth
A face-recognition system that is 99.9% accurate and almost entirely wrong, and a number that sent an innocent woman to prison. Both are the same arithmetic, and it is the arithmetic that decides what any piece of evidence is worth.
ReadConfirmation and survivorship — what you never looked for
Two questions about evidence you did not go looking for. One is a rule you have to discover, and one is a pattern in five famous people — and in both, the thing that would have told you the truth is the thing nobody checks.
ReadLoss aversion, sunk cost and regression — what it costs you
Four questions you answer about yourself rather than about a scenario, and your own answers are the finding. Then the pattern that makes praise look useless and criticism look like it works, whatever you actually do.
Read
