# Plugin Events and Segment Rules

> Record your own events in a plugin, and let people build segments from them, such as total spent in the last 90 days.

Source: https://sendbroadcast.net/docs/plugins-segment-rules

> Coming in Broadcast 3.0. Until then, this page applies only to the Upcoming (edge) build.

A plugin can record things that people do, such as orders. It can then add a rule to the segment editor, so that an admin can build a segment like "spent over 100 in the last 90 days".

Do not confuse this with the [Events Hook](https://sendbroadcast.net/docs/plugins-hook-events). That hook *listens* to what Broadcast publishes. This page is about events your plugin *records*.

## How it works

1. The top-level `events` key in the manifest declares each event and its fields.
2. Your code records events with `ctx.broadcast.events.record`.
3. The top-level `segmentRules` key declares a rule on those events.
4. The rule appears in the segment editor under **Plugins**.

You never write SQL. Broadcast builds the query from your rule, and only from the fixed options on this page.

## Full example: order events

This plugin has an API endpoint that records an order, and a rule named **Total spent**.

`manifest.json`:

```json
{
  "id": "local.order-events",
  "name": "Order events",
  "version": "1.0.0",
  "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" },
  "hooks": {
    "apiEndpoints": [
      { "name": "orders", "method": "POST", "permission": "subscribers_write" }
    ]
  },
  "events": {
    "order.placed": {
      "value": "decimal",
      "properties": { "plan": "string" }
    }
  },
  "segmentRules": {
    "total_spent": {
      "label": "Total spent",
      "source": "order.placed",
      "aggregate": "sum",
      "params": { "amount": "decimal", "days": "integer" },
      "where": [["occurred_at", "within_days", "$days"]],
      "having": [">=", "$amount"]
    }
  }
}
```

`dist/main.js`:

```javascript
export default definePlugin({
  apiEndpoints: {
    async orders(request, ctx) {
      const { order_id, email, total, plan } = request.body ?? {};

      const result = await ctx.broadcast.events.record({
        name: "order.placed",
        email,
        value: total,
        properties: { plan },
        // The same order sent twice is stored once.
        idempotency_key: `order:${order_id}`,
      });

      return result.ok ? { status: 201, body: { saved: true } } : { status: 422, body: result.error };
    },
  },
});
```

## Declare an event

Event names are lower case words joined by dots, such as `order.placed`.

| Field | Options |
|---|---|
| `value` | `decimal` or `integer`. Leave it out if the event has no number |
| `properties` | Field names, each `string`, `decimal`, `integer` or `boolean` |
| `webhook` | `true` lets an admin send the event to a webhook endpoint. See [Send an event to webhooks](#send-an-event-to-webhooks) |

## Record an event

```javascript
await ctx.broadcast.events.record({
  name: "order.placed",
  email: "ada@example.com",     // or subscriber_id: 12
  value: "19.50",
  properties: { plan: "pro" },
  occurred_at: "2026-11-01T10:00:00Z",   // optional
  idempotency_key: "order:1001",         // optional, and strongly advised
});
```

The call answers `{ ok: true }`, or `{ ok: false, error: { code, message } }` for a bad event. It does not throw for a bad event, so check `ok`.

With the same `idempotency_key`, an event is stored once. To record many events, use `bulkRecord`, with up to 1,000 items and a required `idempotency_key` for the whole call.

An event can arrive before the person subscribes. Broadcast keeps it by email, and links it when that email becomes an active subscriber of the channel.

## Declare a rule

| Field | Meaning |
|---|---|
| `label` | The name in the segment editor. Required |
| `source` | An event that this plugin declares |
| `aggregate` | `count`, `sum`, `min`, `max`, `last` or `exists`. All but `count` and `exists` need the event to declare a `value` |
| `params` | Values the admin types in the editor: `integer`, `decimal` or `string` |
| `where` | Up to 5 filters, each `[field, operator, operand]` |
| `having` | `[operator, number or $param]`. Required, except for `exists`, which refuses it |

In `where`, a field is one of these:

| Field | Operators | Operand |
|---|---|---|
| `occurred_at` | `within_days`, `before_days` | Whole days, or a `$param` |
| `value` | `=`, `!=`, `>`, `>=`, `<`, `<=` | A number or a `$param` |
| `properties.` | For numbers, the same six. For `string` and `boolean`, `=` and `!=` | A value or a `$param` | A plugin can have up to 10 rules. ## Send an event to webhooks A plugin event can reach another system through Broadcast's webhooks. Set `webhook` on the event:
```json
{
  "events": {
    "poll.answered": {
      "webhook": true,
      "properties": { "poll": "string", "answer": "string" }
    }
  }
}
```

 This adds a sensitive permission. The admin sees **Its events in webhooks** when installing the plugin. Nothing is sent yet. An admin must turn the event on: 1. Open **Webhooks** and edit an endpoint. 2. Under **Plugin events**, tick `plugin..`, such as `plugin.broadcast.polls.poll.answered`. 3. Save. **Select all** does not tick plugin events. The API cannot turn one on. Each endpoint has its own choice. A delivery uses the normal webhook format, with the same signature, retries and delivery log:
```json
{
  "id": 8123,
  "type": "plugin.broadcast.polls.poll.answered",
  "created_at": "2026-11-01T18:51:16Z",
  "data": {
    "event": "poll.answered",
    "plugin": "broadcast.polls",
    "subscriber_id": 12,
    "value": null,
    "properties": { "poll": "ship-it", "answer": "yes" },
    "occurred_at": "2026-11-01T18:51:15Z"
  },
  "delivery": { "attempt": 1, "delayed": false, "held_since": null, "reason": null }
}
```

 Broadcast builds `data` from the stored event. Your code cannot add a field. - There is never an email address. A `string` property that holds an address is dropped. - `subscriber_id` is `null` when the event is not linked to a subscriber. - `value` is text for a `decimal`, and a number for an `integer`. - A plugin that is off or removed sends nothing. A delivery that is still waiting is cancelled. - A plugin can send 1,000 events a minute in each channel. Over the limit the event is saved, no webhook goes out, and the plugin log says so. - A repeat with the same `idempotency_key` sends nothing new. - When a subscriber is deleted, Broadcast deletes these webhook records too. ## Common mistakes **You expect a webhook with no admin step.** `"webhook": true` only makes the event available. An admin must tick it on an endpoint. **You record a field you did not declare.** You get `plan is not a declared property of this event.` Add the field to the manifest. Only add to events: renaming a field breaks rules that people have saved. **You skip `idempotency_key`.** A retry stores the event twice, and a sum counts it twice. **You send a bad value.** `"lots"` for a `decimal` gives `value must be a decimal.` **You forget the rule depends on the plugin.** If the plugin is off, its rule matches nobody. Admins see the field as not available. Events are kept for two years. Events with no subscriber are kept for 90 days. Each plugin can keep 2,000,000 events in a channel. When a subscriber is deleted, their events are deleted.
