Communication — Common cases
How to read each case: start with the symptom/question, then read the expected behavior, how to verify it in the UI/API/data/code, the source anchor, and the Real-world example. These cases prioritize running behavior; open the linked spec/code when you need the original contract.
"A new notification creates a job but does not send"
This is usually caused by Notification Settings at the Scheduler step. Check in order:
- Has the notification type been seeded into
NotificationOptionfor the BU? (A new type must be seeded before it can be sent.) - Does the BU have a subscriber/recipient? If not, the handler skips gracefully.
- Does Generator have a handler for the new type? A new feature may not have wired the route yet.
Real-world example: A new notification type is enabled for a BU, jobs are created, but customers receive nothing. → The bottleneck is Scheduler/Generator. → The new type is not seeded into
NotificationOptionfor the BU → seed it before sending works.
"UserMessage was created but Inbox did not update immediately"
Realtime Inbox is an invalidation signal only, so check the chain:
- Was
UserMessagepersisted for the correct recipient owner? - Was the persistence outcome a new creation? Compatible re-ensure does not emit a new event.
- Did the Publisher write
inbox.changedto the correct environment's Redis Stream? - Did Tuke receive the event and call
UnreadCountagain? If Inbox is open, the first page must also be revalidated.
If Redis/SSE fails, this is not data loss: REST, focus/visibility refresh and polling remain the convergence path. Do not send the full message through SSE or use the event to grant read access.
Real-world example: The Generator creates a
UserMessagerow successfully but Redis publication fails. → The row is not rolled back and no false event is reported. → The badge converges on the next REST refresh or polling cycle.
"Inbox SSE connection was lost"
- The client reconnects with exponential backoff and sends the latest
Last-Event-ID. - Events within the retention window are replayed and the client deduplicates by
eventId. - An expired or invalid replay returns
inbox.sync-required; the client calls unread and first-page REST again. - Reconnect must not block opening, reading or mutating Inbox through REST.
Real-world example: Tuke loses network for two minutes and reconnects. → Redis replays retained events; if retention has expired, it returns
inbox.sync-required. → Inbox still reaches the correct state through REST, without SignalR.
"Where is a UserMessage stored?"
Check in this order; do not scan all storage:
- Identify the portal identity and permitted owner scope:
Account_{id},Employee_{id}orEnterpriseUser_{id}. - Point-read Azure Table
UserMessagebyPartitionKey+RowKey; only then tryCustomer_{UserId}for compatibility reads of old data. - If content is needed, read
MessageBodyBlobNamefrom the row and then read Blob containermessage-contentafter authorization; the list contains onlyMessagePreview. - If the badge is needed, read
_InboxUnreador_InboxUnread_Account_{id}in the same partition; do not use the Redis Stream to calculate unread.
NextCursor is an opaque cursor issued by the API and contains continuation state for each scope. For the next page, send the exact cursor together with the same filter set; do not decode it or construct a RowKey on the client.
Real-world example: Support has
EmployeeId=42and aRowKeyfrom the Inbox response. → ReadUserMessage/Employee_42/{RowKey}, then load the body throughMessageBodyBlobName. → Do not queryCustomer_{UserId}or infer content from a Redis event.
Add a placeholder to a template
Templates use {{...}}. For example, booking-notification-checkout-instruction adds a checkout-instruction placeholder. Process:
- Add the placeholder to the template.
- Ensure the Generator handler provides a value for that placeholder when rendering.
- Missing placeholder data → renders empty (check fallback).
Real example:
{{INCIDENT_DETAILS_LINK}}points to/attendance/incident-management(there is no deep-link to a specific incident yet — switch to a deep-link when the route exists).
Turn off/reduce a notification type
Some situations need notification suppress:
| Case | How |
|---|---|
| Do not send notification for pending booking | Mute in the pending condition |
| Turn off booking notification by subscription config | Subscription setting |
Mute/suppress is notification-only: it does not change booking/enrollment/invoice state (see the "alert mute is notification-only" principle in Subscription).
Configure default recipients for a site
- Seed the default recipient email for the site.
- The optional "Send copy to site contact email" shows only for site-level options (
NotificationOption.LevelTypeId = Org);BusinessUnit/Enterpriselevels (e.g. "Business Unit Setting updated") → the option is hidden in the editor,SendToSiteContactEmail = trueis never persisted (the request sendstrue, it is stored asfalse; channel flag, template, subscriber, sender stay unchanged). - When generating a site-level notification: setting enabled +
OrgSettings.EmailReplyTonon-empty → the generator adds the site contact email as a recipient (does not replace subscribers); disabled/empty email → not added. Applies to every site-level handler that builds the recipient/subscriber list and to direct send paths (not through the generator). - Scope beats stored flag at send time: non-site-level never receives a copy even if the flag is still
true; no migration needed. - Account-facing handlers (single recipient — booking/quote/subscription,
CancelBooking,Invoice, NCS attendance) use the site contact only as a template variable, outside this copy rule.
Real-world example: An admin enables copy for
IncidentSubmitted(site-level), leaving the setting enabled on the BU option "Business Unit Setting updated". → The generator creates the notification. → TheIncidentSubmittedone adds the site contact email; the BU one has no copy because scope takes precedence.
"Customer receives duplicate email" (send side)
Different from duplicates in Platform (duplicate job creation), this is duplication during send. Prevent it with a send claim:
Created/RetryReady -> Sending (conditional update)If duplication still happens, check (1) OutgoingMessage is truly "one row/recipient/channel" (RecipientKeyHash stable?), (2) send claim is conditional on Status, and (3) AttemptNo is not accidentally included.
Real-world example: A parent receives two identical booking-confirmation emails. → Send once per person/channel. → Send claim moves
Created → Sendingconditionally on Status → the second worker cannot claim it again.
Campaign to many recipients: large payload
Use MessagingJob + Blob. Notes:
- Large content stays in Blob (
messaging-jobcontainer); SQL only keeps orchestration + blob name. - Retry reuses compatible blob payload; mismatched payload → stop + report conflict (do not overwrite).
Safe report-subscription cutover
Report subscription is a thin path with:
- Shadow mode + allowlist + one-owner guard so old and new schedulers never send the same report period together.
- Step-by-step cutover with rollback. Details: Report Subscription.
"Job was skipped/deactivated — why did it not send?"
When automation is skipped/deactivated, there is no delivery. The reason is in AutomationScheduler.DispatchResultJson (SkippedReasonCode/ConflictReasonCode/Summary) and support screens — not in the send pipeline.
Real-world example: Support is asked why a schedule reminder did not send. → Look in the right place instead of digging through the pipeline. →
DispatchResultJsonshowsSkippedReasonCode→ skipped intentionally, not send-failed.
“Notification history cannot be found by invoice/account”
Supported flows use source-keyed OutgoingMessage partitions (invoice:{guid}, account:{guid}, ...). Check source resolution and query the source partition; booking carts may retain OrderGuids for filtering. Do not scan the entire Table or change historical keys.
Real-world example: A job contains three invoices but support needs the second one. → Do not open the job GUID as shared history. → Use the second invoice's
invoice:{invoice.Guid}and inspect each recipient/channel.
“BU contact receives a duplicate of the Site contact”
Check both copy flags and recipient normalization. Delivery deduplicates the address after resolution; a non-site-level notification does not receive Site copy.
Real-world example: Site and BU contacts are both
ops@example.com. → Create one message only. → Deduplicate recipients beforeOutgoingMessage, without changing the subscriber source.
“Report Subscription ran without an attachment”
This can be correct: every run stores a file and provides a secure link; attachment is optional and an oversized file becomes link-only. Check run history, attachment/link state and per-recipient delivery.
Real-world example: A 25 MB Roll Sheet exceeds the mail limit. → The report remains available. → History records link-only and the email points to the authorized view/download.