One Event, Many Doors
One event can leave the building through eight doors at once. What decides whether a chat integration is an asset or a channel everyone has muted is not the formatting — it is the four questions asked before each door, and the fact that a jammed door cannot block the others.
A notification system that sends everything to everywhere gets muted in a fortnight, and after that it may as well not exist. The interesting engineering is therefore not in the sending. It is in the refusing: how many independent chances there are to decide this particular event should not go to this particular place, and what happens when one of those decisions goes wrong.
The record comes first
Something happens in the workspace and a notification is recorded. That record is the thing — it is what appears in the bell menu, what can be marked read, and what still exists tomorrow.
Only after it exists does anything think about outside channels. Each connected destination is offered the notification in turn, and each one decides for itself whether to take it.
That ordering is the load-bearing decision in the whole design. The chat message is a copy of a record, not the record. Every channel can be down, misconfigured or disconnected, and the notification still exists, still appears in the product, and still means what it meant.
If the alert is the only place an event was recorded, then a muted channel is a lost event. It should never be the only place.
Four questions before each door
For every destination, in order, and any one of them ends it quietly.
-
Is this connector usable?
Connected, enabled, and holding whatever it needs to send. An unconfigured destination is not an error here — it is simply not a destination.
-
Does this workspace want this module here?
Each connection carries its own module toggles. Procurement to the operations space and nothing else is an ordinary configuration, not a special case.
-
Does the preference allow this type on this channel?
A finer-grained layer, per module and per notification type, per channel. This is where "approvals yes, every status change no" is actually decided.
-
Is it still usable now the job runs?
The connection is checked again at execution rather than only at dispatch, so a destination disconnected in the intervening seconds is not sent to.
The third question is the one that decides whether people keep the channel unmuted, and it is worth setting deliberately rather than leaving at whatever the defaults are. Volume is the only thing that kills a notification channel, and it is the only thing entirely within your control.
The fourth exists because dispatch and execution are separated by a queue. It is a small amount of duplicated checking that closes a small window, and the alternative is a message arriving in a space somebody disconnected a minute ago.
A jammed door does not block the corridor
Here is the part that matters most and shows least.
Each destination is offered the notification inside its own guard. If deciding whether to send to one of them throws an error — a malformed configuration, an unexpected value, a database hiccup — that failure is caught, recorded, and the next destination is offered the same notification as though nothing had happened.
The alternative is not hypothetical, it is the default. One loop over eight destinations, one guard around the loop, and a single bad configuration on the first channel means the other seven never hear about the event. Worse, because this all hangs off the notification being created, an unguarded failure can propagate back into the operation that raised it — so a broken chat setting becomes a failed approval.
| If one destination misbehaves | One guard around everything | A guard per destination |
|---|---|---|
| The other destinations | Never offered the event | Unaffected |
| The notification record | At risk | Already written |
| The operation that raised it | Can fail | Untouched |
| What you see | Something unrelated broke | One logged line naming the channel |
The cost of the right column is a few extra lines per destination, repeated eight times, which looks like something a tidy-minded person would want to refactor into a loop. It is the repetition that is doing the work, and that is worth a comment beside it so nobody helpfully removes it.
Where the message text comes from
Worth being straight about, since these posts are written from the code.
The chat message is derived from the notification rather than authored separately per channel. The title is built from the module and the category — so it reads as the area of the product followed by what kind of event it was. The body is the notification's own plain text, and the link is the record it points at.
That is a deliberate trade. Bespoke copy per channel would read better and would mean a new notification type needing six new pieces of writing before it could be forwarded anywhere — which is how channels end up supporting a subset of events for reasons nobody can reconstruct.
Derived text means every notification type is forwardable the day it exists, and the six connectors stay in step with the product without anybody maintaining a table.
A cache that is small on purpose
One efficiency detail worth a paragraph because it explains a shape you would otherwise find odd.
Deciding whether to forward requires reading the connector's configuration, and a single operation can raise several notifications — approving a batch, closing an RFQ that notifies four people. Eight destinations times several notifications is a lot of identical lookups.
So each forwarder keeps a small cache of the configuration for the duration of the request. Not a shared cache, not a persisted one — a per-request one, discarded when the request ends.
That scope is the point. A longer-lived cache would mean a setting changed on the settings screen taking effect at some unpredictable later moment, which is exactly the kind of behaviour that makes people stop trusting a settings screen.
What is in place, layer by layer
From event to channel
The record is written first
The in-app notification exists before any outside channel is considered, so a chat message is a copy rather than the only trace of an event.
Eight independent destinations
Six chat platforms plus text messages and mobile push, each offered the same notification and each deciding for itself.
A guard around each one
A failure deciding one destination is caught and logged, and the next is offered the event as though nothing happened — including the record itself, which is already written.
Per-connection module toggles
Each connected destination carries its own list of modules it accepts, so procurement to one space and nothing else is ordinary configuration.
Per-type preferences per channel
A finer layer decides which notification types travel on which channel, which is where volume is actually controlled.
Re-checked at execution
The connection is verified again when the job runs, not only when it was queued, so a destination disconnected in between is not sent to.
Derived message text
Title from the module and category, body from the notification, link to the record — so a new notification type is forwardable the day it exists.
A per-request configuration cache
Repeated lookups within one operation are avoided, and the cache is discarded with the request so a saved setting takes effect immediately.
Queued, never inline
Forwarding happens as background work, so no chat provider's slow afternoon is felt by the person who approved something.
The eight destinations know nothing about each other. Connecting a second one does not change what the first receives, and removing one is invisible to the rest.
Three positions held on purpose
- A chat message is never the only record of an event. Everything appears in the product first, which is what makes a muted channel an inconvenience rather than a gap in the audit trail.
- Each destination is guarded separately, and the repetition is deliberate. Collapsing eight guards into one loop is tidier to read and means a single misconfigured connector can cost the other seven their notification — and can reach back into the operation that raised it.
- Message text is derived rather than written per channel. Bespoke copy reads better and guarantees that some notification types never reach some channels, for reasons that become archaeology within a year.
Five questions about notification routing
What happens if a chat provider is down?
A good answer sounds like
Nothing important.
What ours actually is
The record is written first and forwarding is queued, so the operation and the audit trail are untouched.
Can one bad connector affect the others?
A good answer sounds like
No.
What ours actually is
Each destination is offered the event inside its own guard, and a failure is logged rather than propagated.
How do I stop a channel becoming noise?
A good answer sounds like
Per-type control.
What ours actually is
Module toggles per connection, and preferences per notification type per channel.
When does a settings change take effect?
A good answer sounds like
Immediately.
What ours actually is
Configuration is cached only for the length of a request, so a saved setting applies to the next one.
Does a new notification type reach chat automatically?
A good answer sounds like
Yes.
What ours actually is
Message text is derived from the notification, so nothing has to be written per channel first.
Our take
Every integration doc you will read is about how to make something send. Almost none of them are about how to make it not send, which is the half that decides whether anybody is still reading the channel in March. Two things do most of the work: enough granularity that a team can turn off the events they do not care about without turning off the ones they do, and enough independence that one broken destination is a logged line rather than a silence across all of them. Neither is difficult. Both are invisible when they are working, which is precisely why they are the parts that get left for later.
Send less, and keep the channel
The difference between a notification channel people watch and one they muted in February is almost entirely which events you chose not to send.
Talk through notification routingFrequently asked questions
Can we send different modules to different chat spaces?
Yes. Each connected destination carries its own module toggles, so procurement to one space and inventory to another is an ordinary configuration. Where a platform binds a webhook to a single space, a second space means a second connection.
If we connect several destinations, does one get preference?
No. Each is offered the same notification independently and decides for itself, and none of them knows the others exist. Connecting a second one does not change what the first receives.
What happens to a notification if every channel is disconnected?
It is recorded in the workspace exactly as it always is, and appears in the bell menu. Outside channels are copies — nothing about an event depends on one being connected.
Does a failed chat message affect the thing that triggered it?
No. The record is written first, forwarding is queued, and each destination is guarded on its own. A chat provider having a bad afternoon is not felt by the person who approved a purchase order.
Why does the chat message title read as a module and a category?
Because it is derived from the notification rather than written separately for each channel. That is what lets a new notification type reach every connected destination the day it exists, instead of waiting for six pieces of copy to be written for it.