Skip to content

Notifications

View .md

When a subscription run notifies, Beacon sends the result to each of the subscription’s recipients. A recipient is one destination on one channel: an email address, a Slack or Teams webhook, a Jira project, or any HTTPS endpoint. Data-quality alerts use the same recipients.

ChannelWhat Beacon sendsInline rowsAttachment
EmailAn HTML email through the host’s IEmailAdapterThe preview rows, as an HTML tableCSV or XLSX, when the subscription includes one
TeamsAn Adaptive Card (1.5) posted to a Teams webhookFirst 10 rows, first 3 columnsNone
SlackA Block Kit message posted to an incoming webhook, the rows as a fixed-width text table in a code blockFirst 25 rows, first 5 columns, each column cut to 20 charactersNone
JiraAn issue (type Task) or a comment on it, through the Jira Cloud REST APIThe preview rows, as a tableNone
WebhookA JSON POST to any allowed HTTPS URLThe preview rows, as JSONNone

What every channel gets from a run:

  • Title. [Beacon] {data source} - {query name} (Email subject, Slack header; the Teams card shows the data source and query name).
  • Preview rows. A short preview of the result’s first rows (at most 20), cut further by the subscription’s Max rows, and then by the channel’s own limits above. The total row count is always stated.
  • Query text, only when the subscription has Show query text in notification on.
  • Anomaly details (Slack and webhook), when anomaly detection flagged the run.
  • A link back to Beacon, {BaseUrl}/notifications/{id}; see Links back to Beacon.

Rows are PII-masked before they reach any channel, the attachment included.

A subscription with Include results as attachment builds one file per run, CSV or XLSX as its Attachment format says. The file holds the result’s rows up to the subscription’s Max rows (and never more than Beacon:Query:MaxExecutionRows, since a larger result fails the run), PII-masked like every other row. Only email delivers it: Teams, Slack and webhooks ignore it, and Jira logs a warning and posts its comment without it.

Messages link to the run’s notification page as {BaseUrl}/notifications/{id} (Teams View Query Results, Slack View Full Results, the webhook’s notificationUrl). Without a base URL, messages carry no link.

The React app is served under /beacon, so the base URL must include /beacon: https://beacon.example.com/beacon, not https://beacon.example.com. The host sets it in code:

builder.Services.AddBeaconServices(builder.Configuration, options =>
{
options.BaseUrl = builder.Configuration["Beacon:BaseUrl"]; // e.g. https://beacon.example.com/beacon
});

An Admin can change it at runtime under Admin Settings → Base URL (see Admin Settings).

Recipients are managed on the Recipients page (/beacon/recipients). Only an Admin or a SecurityAdmin creates, changes or deletes them (POST, PUT, DELETE /beacon/api/recipients). Everyone else with Write on a subscription or data contract can attach existing recipients to it (an API key also needs the Execute scope), but never sees their destinations; Admins and SecurityAdmins see them masked. A SecurityAdmin changes where a recipient delivers (its type, destination or headers) only when it can read every subscription and data contract the recipient is attached to (403 otherwise); renaming is always allowed.

Click Add recipient and fill in:

FieldWhat it holds
Notification typeEmail, Slack, Teams, Jira or Webhook.
NameHow the recipient is listed and picked.
DestinationDepends on the type; see the table below.
DescriptionOptional.
Custom headers (JSON)Webhook only: a JSON object of HTTP headers (see Webhook).
Body templateWebhook only: a custom request body (see Body templates).
TypeDestination fieldExample
EmailEmail addressops@example.com (a bare address, no display name)
SlackSlack webhook URLhttps://hooks.slack.com/services/T…/B…/…
TeamsTeams webhook URLA Teams Workflows webhook URL (see Microsoft Teams)
JiraJira connection stringexample;OPS;bot@example.com;<api-token>
WebhookWebhook URLhttps://hooks.example.com/beacon

There is no test button: create the recipient, attach it to a subscription, and use Test now on the subscription to send a delivery.

Destinations and header values are secrets: they are encrypted when saved, shown masked, and never returned by the API in full. Recipients saved before Beacon encrypted them keep working until re-saved or encrypted with IRecipientSecretEncryptionService, unless the host sets Beacon:Notifications:RequireEncryptedSecrets. A recipient whose stored destination cannot be decrypted (saved under another encryption key) is marked Re-enter destination; open it and enter the destination again.

Every destination is checked when it is saved and again when Beacon connects (Beacon:Notifications, see Notification Destinations):

  • HTTPS only, and no user name or password in the URL.
  • Vendor hosts for the vendor channels: Slack hooks.slack.com; Teams *.webhook.office.com, *.logic.azure.com and *.environment.api.powerplatform.com; Jira *.atlassian.net and api.atlassian.com. AllowedHosts:{Type} extends a list.
  • Webhooks may use any public HTTPS host, unless AllowedHosts:Webhook is set, which then is the whole list.
  • No private networks. Private, loopback and link-local addresses are unreachable unless listed in AllowedPrivateNetworks:{Webhook|Slack|Teams|Jira}; Beacon connects only to an address it has checked.
  • Email takes bare addresses; AllowedHosts:Email limits the domains.
  • DisabledTypes turns channels off; UseSystemProxy sends deliveries through the system proxy, which then decides where the name resolves.

A failed delivery shows a generic reason in the history, never the destination’s response; the details are in the server log.

This applies only while the approval workflow is on (it is off by default; a host turns it on in code with options.EnableApprovalWorkflow(...)) and covers recipients (CoversRecipientDestinations, on by default once the workflow is on). Then where a recipient delivers is a reviewed change:

  • A recipient created, or whose type, destination or custom headers change, receives nothing until a second person approves exactly where it now delivers. The save is stored and submits a change request (the saver is the requester); the dialog says “Submitted for approval: the recipient receives no deliveries until a second person approves where it delivers.” An Approver (or an Admin) other than the requester approves it on the Approvals page under Recipients. A recipient changed again since the reviewer opened the request is not approved (409).
  • Deliveries skip a recipient that waits. Every other recipient is still notified; the skipped one is logged as a Warning with ids only, and the recipients list and the subscription’s Recipients tab mark it Awaiting approval.
  • Saves that change nothing where it delivers (name, description, body template) submit nothing. Putting back the approved destination needs no approval and withdraws the requester’s own pending change. A change made while another person’s change of the same recipient waits is refused (409).
  • A rejected change leaves the recipient skipped until it is changed back to its approved destination or saved again; the recipients list and the Recipients tab show Rejected: change it and save again with the reviewer’s comment.
  • Attaching an existing recipient to a subscription or data contract is not reviewed.
  • Rotating a credential (a new webhook token, a new header value) is a change of where it delivers and waits for approval like any other.

Recipients that existed before the workflow was turned on are not governed and keep delivering. Turning the workflow (or CoversRecipientDestinations) off again lets waiting and rejected recipients receive deliveries as they are; startup warns with their count.

A reviewer sees where data will go without seeing any credential. The change request shows, before and after:

  • the type, and what identifies the destination: a webhook’s scheme, host and port, with each path segment masked (so the path’s shape is visible) and user info, query values and a fragment masked. Only the parts a provider documents as not secret are shown: Slack’s workspace and webhook ids (the token masked); a Teams workflow’s fixed path words and workflow id (its signature masked); a legacy Teams connector’s group and tenant ids. Email addresses are shown as they are; a Jira destination shows its site, project and email;
  • the custom header names, each with a fingerprint of its value (12 hex characters), never the value;
  • a fingerprint of the whole definition (type, destination and headers).

A fingerprint is a keyed digest (an HMAC under a key derived from Beacon:EncryptionKey): equal fingerprints mean equal values, and without the key it reveals nothing. Admins and SecurityAdmins see the same fingerprints on the Recipients page, so requester and reviewer can compare them out of band before the approval.

When the before and after look the same but the fingerprints differ, the change lies only in a masked part (a new path, token, signature or header value). The approval page says so, and approving it requires a comment saying how the reviewer checked the change (400 without one). Recipient requests are visible to Approvers and Admins only: recipients belong to no project.

The check sits in INotificationService.SendNotification, the single path to every channel: the copy of the destination and headers about to be sent is decrypted, hashed and compared with the recipient’s approved hash read at that moment. A recipient changed or archived after a run loaded it is therefore never sent the unapproved copy, and a recipient that no longer exists receives nothing. A host that calls SendNotification itself with a RecipientId gets the same check; a delivery without a RecipientId is not a governed recipient’s and is not checked.

The approved hash is an HMAC-SHA256 under a key derived from Beacon:EncryptionKey, never a plain hash of the destination. A hash written under a key listed in Beacon:PreviousEncryptionKeys still matches, and the re-encryption run (IStoredValueReEncryptionService, see Configuration → Replacing the Key) recomputes the approved hashes of governed recipients and their pending requests under the current key. A hash it cannot recompute (an approved destination the recipient has since moved away from while a change waits) is reported as an unresolved key dependency: decide the pending change, or save the recipient back to its approved destination, then run the re-encryption again before removing the previous key.

A run whose every recipient waits for approval notifies nobody: it is recorded as Skipped with “Skipped: every recipient waits for approval of where it delivers, so nobody was notified. Approve the recipients’ changes on the Approvals page.”, a Warning is logged, and Test now answers 409 with that reason. A run that notified some recipients names each skipped one in its comment. A skipped run is not a baseline for the result-count trigger or for anomaly detection.

Beacon does not send email itself: the host implements IEmailAdapter and registers it.

using Beacon.Core.Adapters;
using Beacon.Core.Adapters.Mail;
public sealed class SmtpEmailAdapter : IEmailAdapter
{
public async Task SendEmailAsync(string to, string subject, string body, QueryResultFile? queryResultAttachmentFile)
{
// body is HTML. queryResultAttachmentFile, when present, has Name, ContentType and Data (bytes).
// Send with your provider (SMTP, SendGrid, Amazon SES, ...).
}
}
builder.Services.AddBeaconServices(builder.Configuration, options =>
{
options.AddEmailAdapter<SmtpEmailAdapter>();
});

Register an adapter before you create email recipients. The email has the subject [Beacon] {data source} - {query name}, and an HTML body with the query text (when shown), the total row count and the preview rows as a table; every value in it is HTML-encoded. See also Configuration → Email Adapter.

Office 365 connector URLs on outlook.office.com are retired and refused by the default destination policy. Use a Teams Workflows webhook: in Teams, create a workflow (Power Automate) that starts when a webhook request is received and posts the card it receives to your channel, then copy its URL. Workflow URLs are on *.logic.azure.com or *.environment.api.powerplatform.com; legacy *.webhook.office.com URLs are still accepted.

Beacon posts an Adaptive Card (version 1.5) as the request body: a “Beacon” title, {data source}: {query name}, the query text when shown, and a table of the first 10 rows and 3 columns (headed “First 10 records” when there are more), plus a View Query Results button when a base URL is set.

Create a Slack app with Incoming Webhooks on, add a webhook to the channel, and copy its URL (https://hooks.slack.com/services/T…/B…/…). Anyone holding the URL can post to the channel.

Beacon posts a Block Kit message: a header [Beacon] {data source} - {query name}, an anomaly block when the run was flagged, the query in a code block when shown, the line Results: Showing X of Y total records, the rows as a fixed-width text table inside a code block (at most 25 rows and 5 columns, each column cut to 20 characters), and a View Full Results button when a base URL is set.

The destination is one string, domain;project-key;email;api-key:

example;OPS;beacon-bot@example.com;ATATT3xFfGF0...
https://example.atlassian.net;OPS;beacon-bot@example.com;ATATT3xFfGF0...
https://api.atlassian.com/ex/jira/<cloud-id>;OPS;beacon-bot@example.com;ATATT3xFfGF0...

The email and API token are a Jira Cloud account’s that may create issues in the project. Beacon uses the Jira Cloud REST API; an on-premises Jira is reachable only when the host allows its host (AllowedHosts:Jira) and network (AllowedPrivateNetworks:Jira).

A delivery creates an issue of type Task in the project, with the summary BEACON: {query name}, the label Beacon and priority Medium, its description holding the total row count, the query text when shown and the preview rows as a table; the same content is added as a comment. A later delivery adds a comment to that issue when Beacon finds it, and creates a new one otherwise. Jira gets no attachment.

A webhook recipient receives an HTTPS POST with a JSON body (Content-Type: application/json). By default the body is:

{
"timestamp": "2026-10-15T09:00:04.512Z",
"source": "Beacon",
"subscriptionName": "Unpaid completed orders",
"subscriptionId": 42,
"dataSourceName": "Orders DB",
"sqlQuery": "SELECT id, status, payment_status FROM orders WHERE ...",
"totalRecords": 3,
"executionTimeMs": 184.2,
"previousResultCount": 1,
"timedOut": false,
"records": [
{ "id": 1017, "status": "completed", "payment_status": "pending" }
],
"anomaly": { "isAnomaly": true, "severity": "Medium", "explanation": "..." },
"notificationUrl": "https://beacon.example.com/beacon/notifications/981"
}
FieldWhat it holds
timestampWhen the message was built (UTC).
sourceAlways Beacon.
subscriptionName, subscriptionIdThe query name the subscription is shown by, and the subscription id.
dataSourceNameThe data source the result came from.
sqlQueryThe query text; present only when the subscription shows the query.
totalRecordsThe run’s full row count.
executionTimeMsHow long the run took.
previousResultCountThe row count of the last run that was not skipped; absent on the first run.
timedOutWhether the run timed out.
recordsThe preview rows (PII-masked), as objects keyed by column name.
anomalyisAnomaly, severity, explanation; present only when anomaly detection flagged the run.
notificationUrlThe link back to Beacon; present only when a base URL is set.

Fields without a value are left out.

Custom headers. Custom headers (JSON) takes a JSON object of string values, at most 20 headers, each value at most 4,096 characters without control characters:

{ "Authorization": "Bearer <token>" }

Only these names are allowed: Authorization, Api-Key, X-Api-Key and Ocp-Apim-Subscription-Key, plus the names the host adds in Beacon:Notifications:AllowedHeaders. Headers that control the connection, routing, cookies or cloud metadata access (Host, Content-Type, Cookie, X-Forwarded-For, Metadata-Flavor and others) are never allowed. Header values are encrypted like the destination; when editing, a masked value keeps the stored one, and clearing the field removes all headers.

A recipient’s Body template replaces the default message body. The dialog offers it for webhooks; the Teams and Slack channels honour one too when it is set through the API (bodyTemplate on POST/PUT /beacon/api/recipients), where the template must produce that channel’s own JSON.

Placeholders are written {{Name}}, case-insensitive: {{Timestamp}}, {{Source}}, {{SubscriptionName}}, {{SubscriptionId}}, {{DataSourceName}}, {{SqlQuery}}, {{TotalRecords}}, {{ExecutionTimeMs}}, {{PreviousResultCount}}, {{TimedOut}}, {{Records}}, {{NotificationUrl}}, and {{Anomaly.IsAnomaly}}, {{Anomaly.Severity}}, {{Anomaly.Explanation}}.

  • {{Records}} becomes compact JSON; add _Pretty to a placeholder ({{Records_Pretty}}) for indented JSON.
  • Dates are ISO 8601, booleans true/false, decimals two places; a missing value is empty.
  • Text values are inserted as they are, without JSON escaping: quote string placeholders yourself, and prefer {{Records}} for row data.
  • An unknown placeholder is left as {{UNKNOWN:Name}}. If the template cannot be processed, Beacon sends the default body.
{
"text": "{{SubscriptionName}}: {{TotalRecords}} rows",
"rows": {{Records}},
"link": "{{NotificationUrl}}"
}

Notifications (/beacon/notifications) lists subscription runs: When, Query, Recipients, Rows and Status, filtered by status and paged on the server (20 rows per page, at most 200). The statuses are those of Which runs notify: Created, Sent, Silenced, No results, Timeout, Below threshold, Failed and Skipped.

A notification’s page (/beacon/notifications/{id}) shows the delivery (type, created and sent times), the recipient, the query and subscription, the execution time and result count, and the Stored results when the subscription stores result rows.

A failed run’s comment is shown only when it is a recorded failure reason (for example Recipient 7: Notification delivery failed: the destination did not respond in time.); any other stored text is shown as a generic reason, and the details are in the server log.

The notification list and pages show stored runs, including their result rows; the Home activity feed, the Control Tower detail, a task’s executions and result history and a subscription’s anomaly chart list the same runs. Each run records the data sources it read, and a run is visible:

  • to an Admin, always;
  • to a signed-in user, when one project they may read holds every data source the run read, or when every data source it read is openly reachable;
  • to an API key or MCP caller restricted to projects, only when one of its projects holds every data source the run read (by the projects’ current data sources). Runs recorded before Beacon kept their data sources are hidden from such callers;
  • to an API key or MCP caller with no project, or an anonymous request: never.

Runs you may not see are left out of lists, and their pages read as not found.

Every recipient of a run is tried; one failing does not stop the others. A run with any failed delivery is recorded as Failed, naming the failed recipient ids with a generic reason. Beacon does not retry a failed delivery: fix the recipient, then use Test now on the subscription or wait for the next run.

The React app receives updates over a SignalR hub at /beacon/api/hub: NotificationCreated (scoped to the current user) alongside JobStatusChanged and ApprovalUpdated. A host that does not want them sets AddBeaconApiServices(x => x.Realtime = false), and the app falls back to manual refresh.

Email does not arrive. Check that the host registered an IEmailAdapter (options.AddEmailAdapter<T>()), the adapter’s own logs and provider settings, the address, and the spam folder.

Saving a recipient is refused. The destination is outside the destination policy: not HTTPS, not a vendor host, a private address, a display name in an email address, or a header that is not allowed. The message says which.

Teams delivers nothing. Use a Workflows URL; outlook.office.com connector URLs are refused. Check that the workflow posts the card it receives to the channel.

Jira delivers nothing. Check the four parts of the connection string, that the token’s account may create issues in the project, and that the site is on *.atlassian.net (or allowed by the host).

The link in a message leads to a blank or wrong page. The base URL is missing /beacon; see Links back to Beacon.

A recipient shows Awaiting approval. Its destination waits for a second person’s approval on the Approvals page.