AI Notifications¶
The assistant can write first. A platform event — a margin call, a verification step, a closed trade — becomes a short text addressed to the client or to the manager.
This is the only place where the module produces a message nobody asked for, so the path is deliberately narrow.
The path of one event¶
flowchart LR
e["Platform event"] --> b["Bindings<br/>event + brand"]
b --> p["Rules:<br/>filters, dedupe, cooldown, daily cap, quiet hours"]
p --> v["Re-read the state<br/>(margin events)"]
v --> g["Generate the text"]
g --> q{"Recipient"}
q -->|client| a["Approval queue"]
q -->|manager| d["Delivery"]
a -->|manager approves| d
Two decisions from that picture matter to the interface:
Client messages wait for a person. A generated text in a brokerage product turns into
investment advice too easily, so by default it is parked in awaiting_approval. A manager
reads it, edits it if needed, and only then it is delivered. Manager-addressed messages go
out directly.
A stale event is not sent. Before a margin message reaches a client, the module re-reads the account. If the level recovered, the notification is suppressed rather than delivered — the event queue on the platform side can lose transitions, so the payload alone is not trusted.
The approval screen¶
One screen is enough: the awaiting_approval queue with the text, the recipient and the
reason. Everything else is a journal.
MngGetAINotificationsByFilter returns rows plus the current state of the delivery channels:
{
"rows": [
{
"id": "ntf_7c1…",
"eventName": "account.margin_state",
"recipientType": "account",
"recipientId": 2000067,
"state": "awaiting_approval",
"content": "The margin level on your account is low…",
"suppressReason": null,
"variables": { "login": 2000067, "margin_level": 95.4 },
"delivery": null,
"approvedBy": null,
"approvedAt": null,
"sentAt": null,
"createdAt": "2026-09-17 10:12:04"
}
],
"count": 1,
"channels": [
{ "channel": "notify", "implemented": true, "requires": "MngAddNotify", "available": true },
{ "channel": "email", "implemented": false, "requires": null, "available": false },
{ "channel": "sms", "implemented": false, "requires": null, "available": false }
]
}
| State | Meaning |
|---|---|
pending |
generated, not yet delivered — usually a channel is unavailable |
awaiting_approval |
waiting for a manager |
approved |
a manager approved it; delivery has not succeeded yet |
sent |
delivered to at least one channel |
suppressed |
a rule stopped it, or the state changed; suppressReason says which |
failed |
generation failed |
rejected |
a manager rejected it |
Editing the text is the main scenario, not an extra.
MngApproveAINotification takes an optional content
that replaces the generated text. The assistant proposes wording; the person who approves
it owns it.
Rejections keep the text. It shows what the assistant wanted to write and why it was
refused — the same material that improves prompts. Make reason a required field in the
interface even though the method allows it to be empty.
Why a notification never appeared¶
Suppressed events are not written to the journal — only counted. A high-volume event
like trade.add would otherwise flood the table with "not sent, filter" rows and bury the
real ones.
To explain a specific case, use the dry run MngTestAIWorkflowBinding: it takes a sample payload and shows, for every binding, whether it would fire, the reason if not, the recipient, whether approval is required, and the prompt that would go to the model. It sends nothing and writes nothing.
Reasons a binding stays silent:
reason |
Meaning |
|---|---|
filter |
a binding filter did not match the payload |
duplicate |
the same event within the deduplication window |
cooldown |
this recipient was messaged by this binding recently |
recipient_limit |
the daily cap for this recipient is reached, across all bindings |
quiet_hours |
the configured quiet hours (service time, UTC) |
no_recipient |
the payload has no valid recipient id |
Delivery channels¶
The module writes the text; existing services deliver it:
| Channel | Delivered by | Note |
|---|---|---|
notify |
platform MngAddNotify |
in-terminal and backoffice bell |
email |
Mailer module | not wired yet |
sms |
SMS module | not wired yet |
manager |
a task for the responsible manager | instead of messaging the client |
A channel whose command is not published on the bus does not break the scenario: the
notification stays in the journal marked command_unavailable, and
MngRetryAINotification delivers it once the command
appears. Nothing is regenerated.
When the model is unavailable¶
A binding can carry settings.fallbackText — a ready sentence with the same event
placeholders. If the provider is down, the notification is sent with that text instead of
being lost. The journal marks it, so the wording is not mistaken for the model's.
Without a fallback text the notification is recorded as failed: there is nothing to send,
but the reason is visible.