Skip to main content

Notifications (Admin/Operator)

CoPilot can send a message when something happens — a new alert lands, an AI investigation finishes, an analyst is assigned a case. You choose what triggers it, who receives it, which channel carries it, and how the message is worded. This guide covers the notification routing engine as a whole, plus setup for each channel: Email (Resend), Microsoft Teams, Shuffle, and direct webhooks.
Not to be confused with Notification Workflows. CoPilot has an older, separate per-customer feature that fires a single Shuffle workflow on alert creation, configured under Customers → (customer) → Notification Workflows. Both still work. See Overlap with Notification Workflows before enabling both.

The idea

A route is one rule. It answers four questions: You can have as many routes as you like. Every route matching an event fires independently, so one alert can notify a customer’s Teams channel and a webhook into your automation platform and nothing else, depending on what you’ve configured.

Two kinds of route

This distinction matters more than any other setting. Customer routes deliver to the end customer. They live under Customers → (customer) → Notifications and carry that customer’s code. Use them for things the customer should know about: an alert was raised, an investigation concluded. Internal routes deliver to your SOC. They live under Notifications → Internal Routes in the main navigation, belong to no customer, and are admin-only. Use them for things your team should know about: who picked up which alert. Assignment notifications are internal by design. If you assign an ACME alert to an analyst, that notification reaches your team — never ACME’s channel. This isn’t configurable, and it isn’t meant to be: telling a customer which of your analysts is handling their incident is rarely intended.

Which trigger do I want?

This is the single most common source of confusion, so it’s worth a section of its own. When an alert arrives and AI analysis is enabled for that customer, two separate events happen minutes apart:
They are different triggers. A route configured for an alert is created will never fire when the investigation finishes, no matter how the rest of it is set up. That matters because AI content only rides the second event. If you want the investigation’s findings — the summary, or the full report — you need a route with the An AI investigation completes trigger. A customer with only an alert-creation route gets the alert notification and nothing else, forever, and nothing anywhere reports an error, because a trigger with no subscriber isn’t a failure — it’s just an event nobody asked about. Since v0.1.85 the backend log records it explicitly:
If you expected an AI notification and didn’t get one, that line is the first thing to look for.

Triggers

Customer-facing

Internal

Both

This is the only trigger offered on both customer and internal routes, because analyst sign-off is legitimately both audiences’ business. It’s what lets you hold customer delivery until a human has checked the AI’s work — see Two audiences, two moments. It counts as AI-written content, so a customer-facing review route is governed by the same Customers → (customer) → AI Report opt-in as the rest. An internal route is not: if the switch is off, the customer’s route is suppressed and your internal one still fires. Assignment triggers only fire on an actual change. Re-saving the same assignee sends nothing. By default, assigning something to yourself also sends nothing — there’s a per-route Notify on self-assign option if your team wants the audit trail anyway.

Severity filtering

Minimum severity is inclusive: a route set to High fires on High and Critical, not Medium. For alert creation, severity comes from the Wazuh rule level: Alerts from sources with no rule level default to High. Office 365, CrowdStrike, Carbon Black, Huntress and similar integrations carry no Wazuh rule level, and they are deliberately loud by default rather than silently filtered out. Change it deployment-wide with DEFAULT_ALERT_SEVERITY in your .env:
The AI’s severity is a different number. An investigation assesses the finding, which often disagrees with the alert. Alert #14 in our own testing was Critical by rule level while its investigation concluded Medium. An AI investigation completes route filters on the AI’s assessment, not the alert’s — so a route gated at High will drop investigations the AI graded Medium, even for a Critical alert. If that surprises you, set AI routes lower than you’d set alert-creation routes.
Assignment notifications are Informational — being assigned something isn’t a security severity. Set assignment routes to Informational and above, or they’ll never fire.

Channels

Only email can address a person. A webhook targets a fixed URL and Teams targets a fixed channel, so “notify whoever this was assigned to” requires the Resend channel. Only email renders HTML. Markdown and HTML templates are both converted to a formatted email body, with a plain-text part alongside for clients that refuse HTML. On chat channels markdown is already the native format. Shuffle is unavailable on internal routes. A Shuffle integration belongs to a specific customer, and an internal route has no customer — so the option isn’t offered there.

Worked examples

Complete recipes. The confusion usually comes from the combination of settings rather than any single field, so each of these shows every choice together.

Email the customer a full AI report after every investigation

The setup most people are actually after when they ask “why isn’t the report being emailed?” Also required: Customers → (customer) → AI Report must be enabled. It’s opt-in, and while it’s off every AI notification to that customer is suppressed and logged as skipped. The customer receives the complete write-up — timeline, IOC verdicts, recommended actions — with its tables rendered, in their brand colours. To send just a short summary instead, use the AI investigation — customer summary template. Same route, different template.

Teams card for Critical alerts only

Leave the template empty. Teams renders its own card with a severity-coloured header, a facts table and an Open in CoPilot link; a custom template replaces the body but not the card structure.

Email an analyst when they’re handed an alert

Two things bite here. Assignment events are always Informational, so any higher floor means the route never fires. And this must be an internal route — assignment triggers aren’t offered on customer routes because they could never match. Analysts need an email address on their CoPilot account. A missing one is recorded as failed with the reason rather than silently falling back to anyone else.

Push everything into your own automation

Leave the template empty so CoPilot sends its structured JSON object. A template replaces the entire body with its rendered output, which is what you want for a provider-specific shape like Discord’s {"content": …} — and not what you want when you’re parsing the payload.

Two audiences, two moments

Internal first, customer only after a human has checked it. This is the configuration the An AI report is reviewed trigger exists for:
  • Route Ainternal, An AI investigation completes, Teams. Your team sees every investigation the moment it lands, unreviewed.
  • Route Bcustomer, An AI report is reviewed, email, full-report template. The customer hears nothing until an analyst has signed off in AI Analyst → (report) → Review.
Nothing reaches the customer automatically; the analyst’s review is the release gate. An AI report is reviewed is the only trigger available on both scopes, so you can also run a third route — internal, same trigger — if your team wants a record of who signed off on what. A review fires the notification once per alert. A second reviewer, or someone revising their own review, does not re-notify.

Setting up Email (Resend)

Resend is a transactional email service. Its free tier covers 1,000 emails per month.

1. Create a Resend account and API key

  1. Sign up at resend.com
  2. Create an API key. A send-only (“Sending access”) key is sufficient and recommended — CoPilot never needs to manage your account.
  3. Verify a sending domain under Domains. Until you do, Resend only delivers to the email address that owns the account, which is fine for testing and useless in production.

2. Configure the connector

Add these to your .env and restart CoPilot:
Then go to Connectors, find Resend, and click Verify.
A send-only key is expected to report success here even though CoPilot probes a management endpoint it can’t reach. Verification recognises restricted keys and treats them as valid. If you used a full-access key, verification additionally reports which sending domains are verified.

3. Create a route

Under Customers → (customer) → Notifications (or Notifications → Internal Routes for assignments): Click Send test to confirm delivery before relying on it.

Watch the quota

The 1,000/month free tier is shared across your entire deployment — every customer’s routes draw from the same allowance. That’s roughly 33 emails per day for everyone combined. The route form shows current usage and warns as the month fills. Two things keep it under control:
  • Minimum severity. An alert-creation route set to Informational on a busy customer will exhaust the tier quickly. Start at High.
  • Max emails per hour. A per-route ceiling, so one noisy route can’t drain the allowance for everybody. Throttled sends are recorded in the dispatch log with the reason.
If you need more, Resend’s paid plans raise the limit and nothing in CoPilot needs to change.

Notifying the assignee

Set Deliver toWhoever it’s assigned to. At delivery time CoPilot looks up the assignee’s CoPilot account and uses the email address on it. This means analysts must have a valid email on their user account. If one is missing, the notification is recorded as failed with the reason — it won’t silently vanish, and it won’t fall back to the static list.

Setting up Microsoft Teams

1. Create a webhook in Teams

Microsoft retired the old Office 365 connectors in 2026. The current mechanism is the Workflows app:
  1. In Teams, go to the channel you want notifications in
  2. More options (…) next to the channel → Workflows
  3. Choose the Send webhook alerts to a channel template
  4. Configure and save, then copy the webhook URL

2. Create a route

Choose Microsoft Teams as the channel and paste the URL. That’s the only field. Then click Send test — and actually look at Teams.
Why that matters here specifically. If the payload shape is wrong, Teams responds 200 OK and displays nothing. A successful-looking test with no visible card means something is wrong. A correct test produces a card with a coloured header and a working Open in CoPilot link.

What the card looks like

The header is coloured by severity — red for Critical and High, amber for Medium, green for Low — which makes a busy channel scannable. Below it, a facts table (customer, entity, assignee) and the message body.

Limits worth knowing

  • 28 KB per message. Teams rejects anything larger. A long AI report is truncated with a note saying so, and the deep link is preserved so the full detail is one click away.
  • ~4 requests per second. Above that Teams throttles and CoPilot records the notification as failed with a rate-limit message. This is transient — it means retry later, not that anything is misconfigured.

Setting up Shuffle

Shuffle routes deliver through a customer’s own authenticated Shuffle org, giving access to its app catalogue (Slack, Outlook, Jira, and several thousand more).
  1. Add a Shuffle integration for the customer — Customers → (customer) → Notifications → Shuffle integrations
  2. Create a route with Shuffle as the channel
  3. Pick the integration, then the app within it
  4. Set a destination hint — a channel name, email address or handle. This is prepended to the outgoing message so Shuffle’s app agent knows where to deliver.
For the wider Shuffle setup, see CoPilot ↔ Shuffle Integration.

Setting up a webhook

The generic option: an HTTPS POST to any endpoint. By default CoPilot sends a structured JSON object:
Include full AI report is a checkbox on the route, not something you reference in a template. When it’s on, the report’s extra fields are merged into that same JSON object. If you set a message template, its rendered output is sent as the raw body instead of the structured object — which is how you match a provider-specific shape like Discord’s {"content": …} or Slack’s {"text": …}. If you’re parsing the payload programmatically, leave the template empty.

Message templates

Every route sends a sensible default message. Templates let you change the wording, the structure, and — on email — the formatting. There are two ways to set one, and they don’t behave the same. Notifications → Message Templates in the main navigation. Write a template once, attach it to as many routes as you like, edit it in one place. A named template carries:

Inline templates (per-route override)

The Custom message body field on an individual route. Use it when one route needs to deviate without forking a shared template.

Precedence

Inline → named → channel default. An inline body always wins; if there’s none, the route’s named template is used; if there’s neither, you get the channel’s built-in wording.

Writing a template

Templates are Jinja — conditionals, loops and filters all work:
Available variables: {{ alert_id }}, {{ alert_name }} and {{ report_url }} are kept as aliases so anything written before templates became Jinja still works.
A misspelled variable is an error, not an empty string. {{ summry }} doesn’t render blank — the whole template fails and the route falls back to the channel default, with the reason recorded on the dispatch-log row. The editor validates when you save, and the live preview shows exactly what will be sent. The one exception is context.…, where a missing key does render empty. The same trigger legitimately carries different keys on different events — one alert has an asset name, the next doesn’t — so guard with {% if context.asset_name %} rather than assuming it’s there.

Including the AI investigation report

{{ context.ai_report }} gives a template the full investigation: Always guard it — most alerts have no investigation:
The report is only fetched when a template mentions ai_report, so templates that don’t reference it cost nothing.

Built-in templates

Seven ship with every deployment, as working starting points and as worked examples of the syntax: Built-ins are read-only — the next startup would recreate them anyway. Use Duplicate to get an editable copy.

Sending something manually

Sometimes you want to push one specific alert or case to a channel — outside the triggers and severity filters entirely. Open an alert or case and choose Send to channel…. This sends a real notification: it consumes provider quota and is recorded in the dispatch log with trigger_source = manual. Sending twice sends twice — deliberately, since clicking send twice on purpose should work. The customer AI opt-out still applies. If Customers → (customer) → AI Report is off, including the report is refused rather than quietly dropped.

Testing and troubleshooting

Send test

Every saved route has a Send test button. It sends a real notification through the real path — so it consumes quota and appears in the dispatch log, exactly like a live one. That’s deliberate: a test that took a different path wouldn’t prove much.

The dispatch log

Customers → (customer) → Notifications → Dispatch log records every attempt: skipped is not an error. It’s CoPilot recording a decision it made, which is usually what you want to see when asking “why didn’t I get an email”. A row also carries a template error when a custom template failed and the channel default went out in its place — so a broken template is visible without having to reproduce it.

Nothing arrived

Work down this list:
  1. Is there a route for that trigger at all? The most common cause by far. An alert is created route does not fire when an AI investigation completes — see Which trigger do I want?. Check the backend log for no route is configured for this trigger.
  2. Is the route enabled?
  3. Is the severity high enough? Assignment routes must be set to Informational and above. AI routes filter on the AI’s assessment, which is often lower than the alert’s.
  4. Right scope? Assignment notifications only reach internal routes. A customer route with an assignment trigger can never fire — which is why the UI doesn’t offer that combination.
  5. Check the dispatch log. A failed row carries the provider’s own message. A skipped row says which rule applied. No row at all means no route matched — go back to step 1.
  6. Is the connector verified? Connectors → Resend → Verify.

AI report notifications aren’t arriving for one customer

There’s a separate switch: Customers → (customer) → AI Report. It controls whether AI-written findings may reach that customer at all, and it’s opt-in — a customer who has never been enabled is disabled. When it’s off, AI notifications to that customer’s routes are suppressed and logged as skipped. Internal routes are unaffected, so you can keep running investigations while keeping results in-house.

The email arrived but the formatting is wrong

  • Asterisks showing as literal *text* — the template’s format is text. Change it to markdown and email will render it. Chat channels treat markdown natively either way.
  • Tables showing as rows of | characters — same cause. Markdown or HTML format renders them properly on email; chat channels can’t render tables at all.
  • HTML tags visible in the message — an html template sent to a chat channel. Only email renders HTML.

The template previewed fine but the real message is the default

Almost always a context.… variable that exists on the sample event and not on the real one. The preview uses a fully-populated sample; real events are sparser. Guard every context.… reference with {% if %}.

Overlap with Notification Workflows

CoPilot has an older per-customer feature — Customers → (customer) → Notification Workflows — that fires a single Shuffle workflow when an alert is created. That still works and is untouched. But it fires on the same event as an alert is created route, so a customer with both configured gets two notifications per alert. Both are opt-in, so this only happens if you set up both. The route form warns you when it applies. If you see that warning, disable whichever you don’t want.

Reference

Trigger → scope

Channel capabilities

Environment variables

Teams and webhook routes need no environment configuration — their URLs live on the route. Shuffle uses the existing deployment-wide SHUFFLER_API_KEY.