Saphan StudioDocs
Notifications

What goes out

Every notice type this page knows of, what a notice carries, the rule that the body of the work never travels, and how routes choose what goes where.

The fleet announces particular events, and one rule governs all of them: a notice tells you that something happened and names the thing it happened to. It never carries the work itself.

"Names", not "links". The notices printed at the foot of this page carry no link, and this section does not promise one: what a notice usually gives you is a stream name you take to the console or the command line yourself. ⚠ Not every type gives you even thatdelivery.failed says so in the message itself, and seat.hard_logout names a seat and a machine instead of a stream.

The notice types

These names are the vocabulary. They are what you write into notice.route.<n>.type.

The table is the census. This page states no number beside it, deliberately — a typed number is exactly what went stale here: this section once carried a shorter table and called it the complete set, for as long as it took anyone to notice that the product had grown others.

TypeWhat it announces
owner.act_owedAn act is owed by a human. Something in the fleet is waiting and will keep waiting until a person does it.
gate.signedA gate was signed — a human decision has been recorded.
merge.landedA merge landed.
delivery.failedA notice could not be delivered. ⛔ Route it to a different binding from the one it would be reporting on — a failure report sent through the path that just failed is a report you will not read. The copy block sets a binding of its own for it.
seat.hard_logoutA seat is out of the fleet — the vendor ruled its credential dead, and the seat stays out until someone logs it back in. ⚠ Partly described — see below.
silence.refusal_unrepairedA leg stopped at a refusal, nothing repaired it, and the stream has been silent since. ⚠ Partly described — see below.
silence.terminal_no_successorA leg ended cleanly, nothing started after it, and a gate it owes was never asked for. ⚠ Partly described — see below.
silence.verdict_missingA review ended without an accept and without a reject, so the work it reviewed cannot move. ⚠ Partly described — see below.

The rows marked ⚠ are described in less depth than the others, and that is a gap in this page rather than a property of the product. They are newer than the rest of this section. What this page can tell you about them is what one carries — the disclosure table under "The body never travels" below covers every row of this table, not only the older ones. What it cannot tell you is the exact moment in the fleet's work that sends one, or what severity it carries, which is why the second table below covers only the older rows. It will not guess at either, because a guess about a message a customer will actually receive is worse than a blank.

The gap is not only in the rows marked ⚠. The older rows are described in more depth, not completely: this page prints no per-type field list for any of them, and the "It carries" column below is written for routing rather than as an inventory. Do not read a described row as a fully described one.

What you can act on today is that every row exists, that a type nothing routes reaches nobody, and that what each one discloses is listed below. If you copied an arrangement that covers only the older rows, the rows marked ⚠ are being produced and delivered nowhere. ⚠ For when one is sent and how severe it is, the honest place to look is a notice your own installation delivered.

Among the older rows, only owner.act_owed demands anything of the reader — the rest are the fleet reporting, and a reader who does nothing has missed nothing. ⚠ That is a statement about those rows and not about the table. The rows marked ⚠ each end on a sentence that reads like a demand — a login, a repair, a verdict — but whether the product treats any of them as an act owed by you is not stated here, and this page will not turn a closing sentence into a claim about the type.

Which event sends which message

The table above says what each name means. This one says when it is sent — the moment in the fleet's own work that produces the message, and what the message then carries. ⚠ It covers only the older rows, because when a message is sent is a fact about the fleet's work that this page has not read for the rows marked ⚠ — knowing what one of those carries, which the disclosure table below does give you, is not the same as knowing what triggers it. ⚠ Its "It carries" column is a routing summary, not the disclosure list; that is the table below.

Sent whenMessageIt carries
A human records a gate decision. The message is produced by the same act that writes the decision down — not by a later scan that noticed it.gate.signedthe gate, the decision, the stream, who decided, when
A merge is observed to have actually happened in git. Not when it was authorised — authorising a merge and a merge happening are two different facts, and this message is tied to the second.merge.landedthe stream, branch → base, the merge commit, the gate that authorised it, who, when
A stream starts owing a person an act — a piece of work reaches a point where it can go no further without a human.owner.act_owedthe stream, which act is owed, when. ⛔ No actor — nobody has acted yet, and that absence is the message
The sender runs out of attempts for a notice it could not deliver.delivery.failedwhich notice, and how it ended. The fleet does not go quiet on a delivery it failed to make

Why you would want each of them

  • gate.signed — the record that a human decided. For a team that wants to see decisions in the conversation rather than ask about them.
  • merge.landedthis is the audit one. It ties a change that shipped to the signed act that permitted it, in one message. If you keep an audit trail, this is the message that belongs in it.
  • owner.act_owed — the only one that asks. Route it to the person who can act, not to a channel where it becomes everyone's and therefore nobody's. ⛔ You do that with a binding of its own — a route on this type naming that person's conversation — and not with notice.route.<n>.actor, because this type carries no actor for that key to match. The copy block sets that binding; the section below says what the missing actor means and what it does not.
  • delivery.failed — the one that tells you the others stopped arriving. ⛔ It is worth nothing if it travels the path that just failed; see the warning in the table above.

What is not here

The table above is the vocabulary this page knows of. It is not a promise that nothing else exists, and this section used to make exactly that promise about a shorter table.

What this page can still tell you is the shape of what the product does not do: there is no notice for work starting, none for a run finishing, none for an error inside the fleet, and none on a schedule or a digest. Nothing is queued waiting to tell you later — a notice is delivered when its event happens, or not at all.

What this page will not tell you is that a message you did not receive was never sent. The stronger sentence stood here for as long as it took the product to grow types this page did not know about, and a reader who planned around it planned wrong. If you need to know whether some event announced itself, the answer is in what your own installation delivered, not here.

The body never travels

The content of the work is not in the message. Not the text of an order, not a review's words, not a diff, not a log, not a message body, not the reasoning behind a decision.

Two consequences, and both of them are the point:

  1. Your chat workspace never becomes a second copy of the record. Whatever your retention, export or search settings are over there, they cannot reach what the fleet holds, because what the fleet holds was never sent.
  2. The boundary is between a name and its content — not between an event's existence and its detail. A mis-addressed binding therefore discloses considerably more than "an event happened". ⛔ Size that disclosure by the table below and not by the "It carries" column above — that column is written for routing, it covers only the rows the section above describes in depth, and reading it as the disclosure list is how this page previously understated what a wrong destination receives.

What a mis-addressed binding would disclose

TypeWhat it puts in the conversation
owner.act_owedthe stream's title and its slug, which act is owed, the track name where the stream has one, and three time expressions. ⛔ No actor — nobody has acted yet, and that absence is the message
gate.signedthe stream's title and slug, the gate and the decision recorded on it, the name of the person who decided, the track name where there is one, and three time expressions
merge.landedthe stream's title and slug, the branch and the base it went into, the merge commit id, the id of the authorising gate, the name of the person who decided, the track name where there is one, and three time expressions
delivery.failedthe name of the binding you configured, the type of the notice that did not arrive, how it ended and how many attempts were made. ⛔ No stream, no track and no time — deliberately, because the channel that failed is the one that would have carried them
seat.hard_logoutthe most disclosing row in this table. A seat identity, a machine's hostname, which AI backend that seat runs, a quoted free-text diagnostic of the authentication failure, three time expressions, and a ready-to-run command carrying the machine name and the identity
silence.refusal_unrepairedthe stream's title and slug, the code of the refusal it stopped on, how long it has been silent, the track name where there is one, and three time expressions
silence.terminal_no_successorthe stream's title and slug, the gate it still owes, how long it has been silent, the track name where there is one, and three time expressions
silence.verdict_missingtwo streams — the reviewed one and the reviewer's own, each as a title and a slug — how long the review ran, the track name where there is one, and three time expressions

Every row in this table also ends with a plain-English sentence saying what is now expected of the reader; those closing sentences carry no identifiers of their own.

Read the seat.hard_logout row twice before you route it. It is the one row where a wrong destination publishes your infrastructure rather than your work — a hostname, a credential's name, your choice of AI vendor and a command that names two of them — and it is the row a reader planning around "notices are just event names" will not have priced.

This table is derived from the message catalogue's rendering on 2026-09-07, in the shape the examples at the foot of this page print. Two limits on it, both of which enlarge the disclosure rather than shrink it: it lists what the message carries, and a binding appends your own note line beneath that, so whatever you wrote there is disclosed too; and where a row says "the track name where there is one", that field is present only for streams that belong to a track.

The trade is deliberate: a notice is a name, not a copy. It tells you which stream to open, and you open it where the record lives — under the fleet's access control rather than your chat vendor's.

What a notice carries

Two things travel on every notice, because without them it could not be routed at all:

  • its type — one of the names in the table above, the value notice.route.<n>.type matches;
  • its severity — the value notice.route.<n>.severity_min compares against. ⚠ The severity scale, and which severity each type carries, is not stated on this site. So severity_min is a floor whose effect you cannot work out from these pages; what it accepts you learn by setting it and reading the refusal.

Everything else depends on the type. The "It carries" column above lists it for the rows that column describes; the disclosure table under "The body never travels" above lists it for every row of the type table, and that is the one to read if what you are deciding is where a binding may safely point. Three things to get right, because the opposite of each is the easy assumption and a reader who plans around it plans wrong:

  • An actor is not universal. owner.act_owed carries none — nobody has acted yet, and that absence is the message. So notice.route.<n>.actor has nothing to match on the one type that asks anything of you: narrow that route with its binding, not with actor. ⚠ Whether the product refuses such a route when you set it, or accepts it and never fires it, is not stated here. Do not write one and find out in production.
  • A time is not universal either. Every type in the table at the top of this page carries the moment the event happened except delivery.failed, which reports which notice and how it ended and carries no clock at all. Where a time is carried it arrives as three expressions, not two — how long ago, in words, and then the UTC and local pair that Languages and time describes.
  • Your own line is a property of the binding, not of the notice. It appears on what that binding delivers because you set it there — see Languages and time.

This page does not print the exact field list per type, and the disclosure table is not one. That table says what a wrong destination would learn; it does not name the fields, give their order, or say which are optional. If you need to know the precise fields a gate.signed carries that a merge.landed does not, read one that your own installation delivered.

Choosing what goes where

A route is a numbered rule, and each one answers four questions.

KeyThe question it answers
notice.route.<n>.typewhich type from the table at the top of this page this route carries
notice.route.<n>.severity_minhow severe an event has to be before this route fires
notice.route.<n>.bindingwhich binding — that is, which conversation, language and timezone — it delivers through
notice.route.<n>.actorwhether it is narrowed to a single actor — on a type that carries one, which owner.act_owed does not

Routes are numbered independently of each other, so the arrangement people usually want is just four routes: the acts owed to a person kept apart from the rest — a route on owner.act_owed naming that person's binding, routes on gate.signed and merge.landed naming a team binding, and a route on delivery.failed naming a destination that is neither of them. That is three bindings and four routes, and it is exactly what the copy block sets. ⛔ Everything to one place is the arrangement to avoid, for the delivery.failed reason alone.

Four routes is not a route for every type. The table at the top of this page is longer than that block, and a type no route names is delivered nowhere — so if you want one of the types the block does not cover, you add a route for it yourself.

This page does not state which of the four route keys are required and which may be omitted. Set the ones you mean, then read the configuration back — saphan config show, or the file itself at ~/.saphan/config — and confirm the route you intended is the route that is there. ⚠ What either read-back prints for a notice.* key is not stated on this site; the file is the one that cannot be wrong about its own contents. In the file a route is a section of its own — notice.route.1.type is type under [notice.route.1] — so match it against the key reference rather than against the dotted line you typed.

There is no digest and no quiet window — that absence is named on the section page with the others. A route that matches a busy event type delivers when the events happen, so narrow it with severity_min — and, on the types that carry an actor, with actor — rather than expecting the product to be quiet on your behalf. ⚠ Narrowing by severity is the remedy this section can point at and not one it can size for you, for the reason given above: the scale is not stated here, so you cannot tell in advance what a floor will filter out. The narrowing that is predictable is the one the copy block uses — a separate binding per audience.

Examples

Derived, not photographed, and that is deliberate. This page carried a screenshot of two notices in a Slack conversation until 2026-09-08. It was a real capture, and it went stale anyway: the wording of a message moves and a picture of it does not, so the page ended up walking a reader field by field through names the product had stopped using. The blocks below are the text the message catalogue renders, taken from the catalogue's own rendering on 2026-09-07 — the same source the field lists on this page are derived from. A block of text can be re-derived; a photograph can only be re-shot.

What these blocks are not. Three things, and each of them is a conclusion a reader would otherwise be entitled to draw:

  1. They are the message, not the whole of what lands in your conversation. A binding appends up to three further lines beneath it — your own note, a reference line, and the no-reply line if you turned it on. Those come from the binding, not from the type; see Languages and time.
  2. They are not evidence that a notice of either type has arrived in a working fleet. For whether a given type is reaching you, the only honest answer is the one your own installation gives.
  3. The values are the catalogue's placeholders and do not describe one coherent event. The stream named on the first line of the merge block and the branch named on its second are not from the same merge. The stream names are invented. The person's name is not — it is this project's own, carried through from the catalogue's rendering, and it is here as the plainest illustration of the row above: this is what a merge.landed puts in whatever conversation you point it at.

owner.act_owed — the one that asks:

*DECISION NEEDED* · STOP-2
which seats may take this wagon is a query not three refusals — round 2 `which-seats-may-take-this-wagon-is-a-query-not-three-refusals-r2`
asked 11 min ago · 2026-09-07T03:29:04.412103Z · 10:29 Asia/Bangkok · track `chat-notifications`
Nothing on this stream moves until this gate is signed.

merge.landed — the audit one, and it repays a close look:

🟢 *LANDED* · which seats may take this wagon is a query not three refusals — round 2 `which-seats-may-take-this-wagon-is-a-query-not-three-refusals-r2`
`fix/the-write-doors-must-refuse-a-schema-ahead-store-r3` → `develop` · `b9aab238`
authorised by fleet_gate#4711 · Marcin Marzec · 11 min ago · 2026-09-07T03:29:04.412103Z · 10:29 Asia/Bangkok · track `chat-notifications`
This stream's history is closed; nothing on it is owed to you.

Reading the merge line by line:

  • the first line — a severity marker, a headline word, and the stream: its title, then its slug in code type, so the slug can be copied without ambiguity;
  • the second linefix/… → develop and the merge commit, which is what merged into what and the commit it produced;
  • authorised by fleet_gate#4711the signed human decision this merge was permitted by. The change and the permission are named in one message, which is the whole reason this is the type an audit trail wants;
  • Marcin Marzec — who;
  • three time expressions, not two11 min ago for the person reading it now, 2026-09-07T03:29:04.412103Z for the record, and 10:29 Asia/Bangkok for the reader's own clock. The last two are the pair Languages and time describes and the binding's timezone governs; the relative one stands in front of them, and no key on these pages governs it;
  • the track field — the name of the track a stream belongs to. It is conditional: most streams belong to no track, and then the field is simply absent, which makes it the kind of field you will not discover by receiving one message;
  • the last line — a plain-English sentence saying what is now expected of you. Every type in the table at the top of this page ends with one.

What this section still does not give you is a per-type field list beyond the disclosure table above, and it does not give you the second language at all: a binding set to pl renders these same messages in Polish, and this page prints only the English. For the exact fields a type carries in the language you configured, read one that your own installation delivered.

On this page