---
sidebar_label: Inbox rules
toc_max_heading_level: 3
doc_id: 4a1949eb-56d9-4ac1-8e56-ef3ecbfe9797
description: >-
  Manage the inbox rules that decide what reaches people's inbox in
  nullplatform, and create your own to send notifications from CI pipelines,
  workflows, or scripts.
keywords:
  - communications
  - inbox rules
  - custom notifications
  - emission
  - inbox
  - API
title: Inbox rules
canonical: 'https://docs.nullplatform.com/docs/communications/inbox-rules'
---
# Inbox rules

An inbox rule describes something worth telling people about: what triggers it, who receives it, and what it says. nullplatform ships built-in rules for approvals, deployments, builds, and more. Create your own rules to announce events nullplatform doesn't observe, such as a security finding from your CI scanner or a budget alert from your cost tool.

Every rule also shows up in each person's [inbox settings](/docs/communications#inbox-settings), so people can tune or mute your rules like the built-in ones.

## Manage inbox rules from the UI

Go to **Platform Settings > Communications > Inbox rules**. The list shows the built-in rules and your organization's own, with what triggers each one and where it applies. Use the **Rule** filter to show only **Built-in** or **Custom** rules.

Walk through the whole flow, from the list to sending a rule from your pipeline:

<GuidedTour
  alt="The Inbox rules page and the inbox rule editor, recreated as an HTML scene with a step-by-step walkthrough"
  steps={[
    {
      title: '1. Find your rules',
      body: 'Built-in rules and your own, with what triggers each one and where it applies. Edited marks a built-in rule you changed.',
      anchor: 'rules-list',
      placement: 'bottom',
    },
    {
      title: '2. Create a rule',
      body: 'New inbox rule opens the editor. You can also Duplicate any rule from its actions menu.',
      anchor: 'rules-new',
      placement: 'bottom',
      align: 'end',
    },
    {
      title: '3. Start from an example',
      body: 'Custom events are sent by your workflows or CI. Platform events fire on what happens in nullplatform, like a failed deployment.',
      anchor: 'rules-examples',
      placement: 'bottom',
    },
    {
      title: '4. Edit the definition',
      body: 'The YAML says what triggers the rule, the data it takes, and who receives it. It is checked as you type.',
      anchor: 'rules-yaml',
      placement: 'right',
    },
    {
      title: '5. Read it back',
      body: 'Overview spells out when the rule fires and who it notifies, and previews the notification as people will see it.',
      anchor: 'rules-overview',
      placement: 'left',
    },
    {
      title: '6. Post it to Slack',
      body: 'Also post to sends every notification of the rule to the Slack channels you add, besides the people it notifies.',
      anchor: 'rules-slack',
      placement: 'left',
    },
    {
      title: '7. Send it',
      body: 'Emission shows the request to call from your pipeline, the fields it takes, and a test you can send to yourself.',
      anchor: 'rules-emission',
      placement: 'left',
    },
  ]}
>
  <InboxRulesScene />
</GuidedTour>

### Built-in rules

From a built-in rule's actions menu, you can:

- **Open** it and edit it for your organization. An edited rule shows an **Edited** chip, and **Reset to default** discards your changes.
- **Deactivate** it, so nobody in your organization receives it until you activate it again.
- **Duplicate** it to start a new rule from it. A built-in rule keeps its ID and source, so this is how you get a different trigger.

### Create your own rule

Click **New inbox rule** to open the editor. It validates the YAML on the left as you type, and **Overview**, next to it, explains in plain words when the rule fires and who it notifies, with a preview of the notification.

<InboxRulesScene view="editor" />

:::tip Start from an example
Pick one of the **Examples** instead of starting from scratch: custom events that your workflows or CI send, or platform events that fire on what happens in nullplatform.

<InboxRulesScene view="examples" />
:::

Once the rule is saved, its **Emission** tab shows a ready-to-run `curl` command, and you can **Send test to me** to see it in your own inbox.

To also post the rule to Slack, add a channel under **Also post to**, at the end of **Overview**. See [Post inbox rules to Slack](#post-inbox-rules-to-slack).

Without permission to manage inbox rules on a rule's resource, the editor opens in view-only mode.

**Deactivate** stops a rule right away and frees one of your slots, and you can activate it again later. Notifications already sent stay in people's inboxes.

:::warning
**Delete** removes the rule and everyone's inbox settings for it. To stop a rule for a while, deactivate it instead.
:::

:::info Limits
An organization can have up to 50 active rules of its own, edited built-in rules included, and up to 10 active rules watching the same entity, like deployments.
:::

## Post inbox rules to Slack

Besides the people a rule notifies, you can post every notification of an inbox rule to Slack channels, from **Also post to** in the rule's editor. Each channel gets the rule wherever it happens in your organization.

<InboxRulesScene view="also-post-to" />

The rule has to be saved first. The first time, **Also post to** offers **Connect Slack**: it installs the nullplatform app in your Slack workspace, and you only do it once per organization. Announcements use the same connection.

:::warning Invite the app to each channel
The nullplatform app only posts to channels it's a member of, private channels included. Invite it to every channel you want to post to. The channel picker marks the ones it isn't in yet with **App not in channel**.
:::

If the connection stops working, for example because the app was removed from your workspace, **Also post to** shows **Slack is disconnected** and a **Reconnect Slack** button. Nothing is posted until you reconnect it, and the rule keeps its channels.

### What a Slack message looks like

Each post carries the notification's title in bold, its body, and a button that opens it in nullplatform. Maintenance, degraded, and outage notifications also show their severity.

## What triggers a rule

A rule's `source` says what makes it fire:

| Source | Fires when | Example |
|--------|------------|---------|
| `{ "kind": "invoked" }` | Your code calls the emission endpoint. | A CI scanner reports a vulnerability. |
| A source name, like `"deployment"` | The entity changes status in nullplatform. | A deployment in a given namespace fails. |
| `{ "kind": "stale", ... }` | An entity stays in a status for too long. | A deployment has been running for more than an hour. |

A [GET request](/docs/api/inbox-rule-source-list) to `/communications/inbox_rule/source` lists the source names you can use and the statuses and fields each one exposes.

The rest of this page focuses on **invoked** rules, the only kind you can send through the API.

## Create an invoked rule with the API

Send a [POST request](/docs/api/inbox-rule-create) to `https://api.nullplatform.com/communications/inbox_rule` with the rule definition:

```bash
curl -L -X POST 'https://api.nullplatform.com/communications/inbox_rule' \
  -H "Authorization: Bearer $NP_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "acme.security_finding",
    "source": { "kind": "invoked" },
    "context_schema": {
      "type": "object",
      "required": ["id", "service", "owner_id"],
      "properties": {
        "id": { "type": "string", "description": "The scanner finding ID" },
        "service": { "type": "string", "description": "The affected service" },
        "summary": { "type": "string", "description": "One line on what was found" },
        "owner_id": { "type": "integer", "description": "The nullplatform user who owns the service" }
      }
    },
    "category": {
      "group": "security",
      "label": "Security findings",
      "description": "Vulnerabilities your CI scanners report.",
      "configurable": true,
      "allows_all": true,
      "default_mode": "involved"
    },
    "severity": "degraded",
    "audience": [
      {
        "as": "owner",
        "who": { "user": "${{ context.owner_id }}" },
        "action": "acme.security_finding",
        "content": {
          "title": "Security finding on ${{ context.service }}",
          "body": "${{ context.summary }}"
        }
      }
    ]
  }'
```

:::tip
Add `?dry_run=true` to validate the definition without saving it.
:::

### The parts of a rule

- **`id`**: lowercase words with exactly one dot, like `acme.security_finding`. It's also the rule's ID in inbox settings, and it can't change later. It can't start with `platform.`, and using a built-in rule's ID edits that built-in rule instead of creating a new one.
- **`context_schema`**: a JSON Schema for the data you send each time. An invoked rule must require `id`, because sends are idempotent by it. Only basic keywords are allowed: `type`, `properties`, `required`, `enum`, `minLength`, `maxLength`, `minimum`, `maximum`, `items`, `writeOnly`, `description`, and a boolean `additionalProperties`.
- **`category`**: how the rule shows up in inbox settings. All six fields are required. `description` is one line of up to 70 characters. `allows_all` lets people choose **Everything** for this rule under a resource, and it requires `configurable`. `default_mode` (`off`, `involved`, or `all`) is what people get until they change it. For email, set `channels.email.default_mode` too: it defaults to `off`.
- **`severity`**: `informational`, `maintenance`, `degraded`, or `outage`. It sets the badge color.
- **`audience`**: one entry per reading. Each entry is a separate notification with its own wording, named by `as`. `action` must be the rule ID or start with it (for example, `acme.security_finding.unowned`).
- **`visible_to`** (optional): the one NRN the rule applies under, like `["organization=1:account=2"]`. It defaults to the organization root. Sends for resources outside it are refused.

Templates read the context you send with `${{ context.<field> }}`. Titles and bodies accept inline Markdown (`**bold**`, `*italic*`, `` `code` ``), and bodies also accept links and lists.

To word a reading differently by email or in Slack, add `content.email` (`subject`, `title`, `body`) or `content.slack` (`title`, `body`) next to its title and body. Any field you leave out falls back to the reading's own text.

### Who receives it

Each audience entry picks its recipients with `who`:

- `{ "user": "${{ context.owner_id }}" }`: one user ID from the context. Pass a list of templates and the first one that renders wins.
- `{ "can": "approval_request:approve" }`: everyone who holds that action on the resource.
- No `who`: only the people who chose **Everything** for this rule on the resource or above it.

Add a `fallback` entry, with the same fields, to reach someone else when `who` resolves nobody. For example, a security lead when the service has no owner.

## Send a notification

Once the rule exists, send it from your pipeline, workflow, or script. Send a [POST request](/docs/api/inbox-rule-emit) to `https://api.nullplatform.com/communications/inbox_rule/<id>/emission` with the resource the event is about (`nrn`) and the data the rule's templates read (`context`):

```bash
curl -L -X POST 'https://api.nullplatform.com/communications/inbox_rule/acme.security_finding/emission' \
  -H "Authorization: Bearer $NP_ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "nrn": "organization=1:account=2:namespace=3:application=4",
    "context": {
      "id": "finding-8812",
      "service": "payments-api",
      "summary": "CVE-2026-1234 in a transitive dependency",
      "owner_id": 50
    }
  }'
```

A successful send answers `201`:

```json
{
  "emitted": [{ "id": "6f1c2b9e-3a41-4f0e-9d55-2b7c1e0a9f13", "as": "owner", "recipients": 1 }],
  "rejected": 0
}
```

> **Note:** `recipients` and `rejected` are counts, never user IDs.

### What happens to the recipients

- **Access is checked.** A recipient who can't see the resource at `nrn` doesn't receive the notification and is counted in `rejected`.
- **Inbox settings are applied.** People who turned the rule off for that resource don't receive it.
- **Followers are added.** People who chose **Everything** for the rule on that resource, or above it, receive it too, when the rule allows it and at least one reading reached someone. They get the reading without `who` if the rule declares one, or else the first reading that reached someone.

### Retries are safe

Sends are idempotent by `context.id`, so your pipeline can retry without notifying anyone twice:

- Sending the same `id` for the same `nrn` again answers `200`, marks each entry in `emitted` with `"existing": true`, and creates nothing.
- Sending the same `id` for a different `nrn` answers `409`.

:::warning
Use a distinct `context.id` per resource. Reusing one under another resource is refused.
:::

### Test before you send

:::tip Try it without notifying anyone
- Add `?dry_run=true` to see what would be sent, to how many people, and why a reading would reach nobody. Nothing is written.
- Add `"deliver_to": "me"` to the body to send the first reading to yourself only.
:::

### Responses

| Status | Meaning |
|--------|---------|
| `201` | At least one notification was created. |
| `200` | A retry of an earlier send, or nobody was left to notify. |
| `202` | The rule declares a delay or batch behavior, so delivery was deferred. |
| `400` | The context doesn't match `context_schema`, the NRN isn't valid or is outside your organization, or the rule isn't invoked. |
| `404` | The rule doesn't exist, is deactivated, or doesn't apply to that NRN. |
| `409` | That `context.id` was already sent for another resource. |

## Manage rules with the API

| Request | What it does |
|---------|--------------|
| [`GET /communications/inbox_rule`](/docs/api/inbox-rule-list) | Lists the built-in rules and your organization's own. |
| [`GET /communications/inbox_rule/<id>`](/docs/api/inbox-rule-read) | Returns one rule. |
| [`PUT /communications/inbox_rule/<id>`](/docs/api/inbox-rule-update) | Replaces one of your rules. Add `?dry_run=true` to validate only. |
| [`DELETE /communications/inbox_rule/<id>`](/docs/api/inbox-rule-deactivate) | Deactivates one of your rules. |
| [`GET /communications/inbox_rule/schema`](/docs/api/inbox-rule-schema) | Returns the JSON Schema of a rule definition, for editor completion. |

:::note
Editing a rule never changes notifications that were already sent.
:::

## Next steps

- [Communications](/docs/communications): your inbox, inbox settings, and announcements
- [API keys](/docs/authorization/api-keys): create the key your scripts use to get an access token
