In early 2024 an operations director at a manufacturing customer told me something I repeated to my own team for months afterward. She said it politely, which somehow made it worse.
"Our approval chain is slow."
So we measured it. The process itself — validation, routing, the countersign steps — averaged under four minutes of system time. End to end, the average was thirty-one hours. Twenty-seven of those hours were spent waiting for a person who did not know they were waiting.
The workflow was never slow. The notification was invisible. And nobody on my team could tell her which of the two was true, because nothing in the platform could answer the question was this person notified, and did they see it?
That was the day I stopped thinking of notifications as a feature.
In most platforms, a notification is a side effect
Look at how a low-code platform usually introduces notifications. There is an automation node with a checkbox labeled something like send notification on record created. You tick it, you pick a template, you move on.
Notice what is missing. There is no notification in the data model — no table, no record, no owner column, no delivery status, no receipt timestamp. The message is not an entity. It is a byproduct, written to a channel and then forgotten, somewhere between a log entry and a print statement.
That is why the conversation above is so common. A byproduct cannot be queried, reconciled, retried, or audited. It cannot even be counted. So when she asked how many approvers had been told about the pending item, the honest answer was that we did not know — and that we had no design that could ever know.
Everything worth doing here starts with one decision: give the notification an identity.
The moment a message has an ID, it becomes governable
When we rewrote our notification layer, the first change was small and structural. Sending a notification returns an identifier.
A notification with an ID can be listed, filtered by recipient — so each person has an inbox rather than a stream — marked as sent, read, or deliberately superseded, archived under a retention rule, and counted per record. It is the same work as any other table in the system. But once a message becomes a row, three things become possible that were previously unthinkable: reconciliation, deduplication, and accountability.
Deduplication is where notification systems quietly die. A single business event — an order approved — can produce eleven rows for six people, and skim a real outbox on a busy Tuesday and you will find the same fact repeated to the same person four times, because four separate automations each decided it was their duty to inform. A platform that cannot see its own outbox cannot see its own noise.
An actionable notification is a destination, and destinations rot
There is a second decision that looks trivial and is not. A notification can be informational, or it can carry a destination — open this URL, open this record, open this pending task in this process instance.
We use all three, and I would defend the choice. A notification that lands someone on the thing they must act upon is worth ten that merely describe it. One small change during that investigation — opening the exact process task instead of the application home page — removed minutes from every step, and minutes multiply across a company.
But destinations rot, and they rot silently.
Here is a story I am not proud of. We renamed a module during a reorganization. Six weeks of historical notifications still pointed at the old identifier. Users did not file bugs about broken links. They filed bugs about the bell being broken, and then they stopped clicking the bell. Rebuilding trust in that channel took longer than the rename itself.
The lesson is not "avoid deep links." It is that a notification is a pointer, and pointers age. If your notification is a row, you can validate its destination at send time and repair it at read time. If it is a byproduct, you learn about it from a support ticket.
The suppression problem nobody plans for
Now put the notification system under load, and it fails in a way that is genuinely funny until it happens to you.
A customer migrates data. A file arrives with forty thousand rows describing closed purchase orders, archived contracts, departed employees. The import runs. Every on create rule fires exactly as designed — that is what rules do — and the platform cheerfully generates a notification per row per concerned party. Six targets on this table. The arithmetic is not subtle.
Nobody reads two hundred and forty thousand messages. Human beings do something much worse. They turn notifications off. Permanently. And then the channel that was supposed to carry next Thursday's urgent escalation is dead, and nobody notices it died, because silence is the normal state of a channel everyone has muted.
Our answer was to let the caller declare intent, not just the change. A batch context can say I am a migration, suppress per-record notifications, and the platform honors it at the thread level rather than the rule level. It is a few lines of API that encode a larger idea: two identical writes are not the same event. One is a person typing a decision; one is a machine copying history. The row cannot tell the difference. The caller can — and the platform should ask the caller.
Cross-channel is not a feature. It is a delivery contract.
Enterprise work happens across the company's collaboration stack — WeCom, DingTalk, Feishu — plus email, plus the mobile app. It is tempting to treat this as a rendering problem: same message, several skins.
It is not. Each channel carries a different, and importantly a weaker or stronger, guarantee.
An in-app inbox guarantees that a row exists and nothing about attention. Email guarantees delivery to a mailbox, and no reliable signal that a human opened it. A chat platform is worse in an interesting way: it delivers into someone's evening, so a message that helps at 10:00 a.m. is hostile at 10:00 p.m. And a mobile push is the most intrusive channel you own, which is why it is the fastest to be disabled.
Which brings me to the decision I am most ambivalent about. Customers often want delivery through their own gateway — a corporate SMS provider, their own WeCom bot. So the platform produces the row, hands it over, and the customer's script delivers it and marks it done.
That is honest in one direction and dangerous in the other. Honest, because we refuse to claim a guarantee we do not control. Dangerous, because a self-reported sent is not proof of anything: a script can be rate limited, a gateway can drop a message mid-failover. When that happens, the notification row says sent, the recipient's inbox is empty, and both statements sit in the same database disagreeing with each other. We stopped calling that field delivered for exactly this reason. Precision in naming is not pedantry when the name is the last thing between a customer and a false sense of safety.
The difference between a notification and a task
Here is the question I now ask about every notification request: if the recipient ignores this, what happens?
For most notifications the answer is nothing — the message ages in a list, which means most notifications are not tasks at all, just ambient noise with a friendly icon.
For the ones that matter — a contract awaiting approval, a compliance exception pending review — the answer must not be nothing. Somebody must eventually be told again, or told louder, or told to someone else. That is escalation, and escalation is the boundary. A notification informs; a task obligates. Without escalation you have not built a notification system — you have built a broadcast system and asked human memory to be the retry mechanism.
The uncomfortable conclusion
The uncomfortable conclusion is that we shipped notification as a one-line capability, called it done, and spent the next two years discovering we had shipped an undelivered promise with no owner, no receipt, and no way to distinguish nothing happened from nobody looked.
We treated a message as a log line. A log line is something you write and forget. A notification is something a person is expected to act on — and every decision that makes a notification cheap to send also makes it expensive to be heard.
So ask the question we could not answer: who is supposed to read this, and what happens to the business if nobody does?
If you cannot answer both halves, you do not have notifications. You have a bell everyone has learned to stop hearing.
Top comments (0)