# Broadcasts & Notifications

Campaigns and in-app plus email notifications


Send engage campaigns over email, messenger, and SMS, and deliver in-app and email notifications to team members.

For inbound mail, DKIM, and the mail worker, see [Email Delivery](https://erxes.io/docs/api-email).

## Broadcasts

`backend/core-api/src/modules/broadcast/`:

```text
graphql/schemas/{index,engage}.ts
graphql/resolvers/{queries,mutations,customResolvers}/
db/definitions/{engages,deliveryReports,broadcastTraces,smsRequest,log,common}.ts
db/models/{Engages,DeliveryReports,BroadcastTraces,SmsRequests,Logs,factories}.ts
worker/{broadcast,email,messenger,notification,index}.ts
trackers/{engage,sendgrid,index}.ts
routes/{index,telnyx,tracker,unsubscribe}.ts
utils/{broadcast,email,engage,outboundEmail,sender,targeting,
      telnyx,transporter,worker,widget,...}.ts
trpc/{broadcast,index}.ts
```

An `EngageMessage` ([engages.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/broadcast/db/definitions/engages.ts)) carries `kind` (`auto`, `visitorAuto`, `manual`), `method` (`messenger`, `email`, `notification`), `title`, `fromEmail`, `fromUserId`, `cpId`, `targetType`/`targetIds`/`targetCount`, `isDraft`, `isLive`, channel payloads (`email`, `messenger`, `notification`), plus send progress (`totalBatches`, `processedBatches`, `successCount`, `failureCount`, `lastUpdated`) and `status` (`sending`, `completed`, `failed`).

### GraphQL API

| Operation | Key arguments |
| --------- | ------------- |
| `engageMessages` | `kind`, `status`, `method`, `brandId`, `fromUserId`, `searchValue`, cursor params; returns `EngageMessageListResponse` |
| `engageMessageDetail` / `engageMessagesTotalCount` | `_id` / same filters |
| `engageMessageCounts` | `name!`, `kind`, `status` |
| `engageMembers` | `isVerified`, cursor params |
| `engageReportsList` | `page`, `perPage`, `customerId`, `status`, `searchValue`; returns `EngageDeliveryReport` |
| `engageSmsDeliveries` | `type!`, `to`, `page`, `perPage` |
| `engageBroadcastTraces` | `engageMessageId!` |
| `engageEmailPercentages`, `engageVerifiedEmails`, `engagesConfigDetail`, `emailSenderOptions` | `scope` for the last |
| `engageMessageAdd` / `engageMessageEdit` | `title`, `kind`, `method`, `fromEmail`, `fromUserId`, `cpId`, `targetType`, `targetIds`, `targetCount`, `isDraft`, `isLive`, `email`, `messenger`, `notification` |
| `engageMessageSetLive` / `SetPause` / `SetLiveManual` | `_id!` |
| `engageMessageRemove` / `engageMessageCopy` | `_ids` / `_id!` |
| `engageMessageVerifyEmail` / `engageMessageRemoveVerifiedEmail` | `email!`, `name`, `replyTo`, `scope` / `email!`, `scope` |
| `engageMessageSendTestEmail` | `from!`, `to!`, `content!`, `title!` |
| `engagesUpdateConfigs` / `broadcastUpdateConfigs` | `configsMap: JSON!` |
| `engageSendMail` | `integrationId`, `conversationId`, `subject!`, `body`, `to: [String]!`, `cc`, `bcc`, `from!` |

### Delivery pipeline

Sending runs through `worker/` batch processors (`broadcast`, `email`, `messenger`, `notification`); a broadcast is not transactional: create it, set it live, then track per-recipient state in `DeliveryReports`, `BroadcastTraces`, `SmsRequests`, and `Logs`. Open/click tracking lands via `routes/tracker.ts` and `trackers/`; Telnyx webhooks arrive on `routes/telnyx.ts`; unsubscribe links hit `routes/unsubscribe.ts`. Outbound email builds SES headers in `utils/email.ts` (`X-SES-CONFIGURATION-SET`, default config set `erxes`) and sends through the SES transporter in [utils/transporter.ts](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/core-api/src/modules/broadcast/utils/transporter.ts) (`AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`).

> [!WARNING]
> **Stuck or suppressed sends**
>
> A broadcast stuck at `sending` means a batch worker is down or failing: compare `processedBatches` to `totalBatches` and check the worker logs; failed batches show in `failureCount` and `Logs`. If email never leaves, check `emailAddresses` for suppression lanes and `emailRampStatus` for throttling, then release with `emailAddressRelease`/`emailRampRelease`.

## Notifications

`backend/core-api/src/modules/notifications/`:

```text
graphql/schema/{index,subscription}.ts
graphql/resolver/{queries,mutations,utils}.ts
graphql/customResolvers/{notification,emailAddress}.ts
db/models/{EmailSenders,EmailAddresses,EmailDeliveries,EmailRamp}.ts
trpc/{email,index}.ts
routes/{index,senderConfirm}.ts
```

Notification records, settings, and the outbound-email pipeline (senders, addresses, deliveries, ramp) share one module. In-app delivery uses the `notificationInserted` GraphQL subscription:

```graphql
subscription OnNotification($id: String!) {
  notificationInserted(_id: $id) { _id }
}
```

| Operation | Key arguments |
| --------- | ------------- |
| `notifications` | `ids`, `status` (`READ`/`UNREAD`/`ALL`), `priority` (`LOW`/`MEDIUM`/`HIGH`/`URGENT`), `type` (`INFO`/`SUCCESS`/`WARNING`/`ERROR`), `fromDate`, `endDate`, `fromUserId`, `module`, cursor params |
| `notificationDetail` / `unreadNotificationsCount` | `_id!` / none |
| `pluginsNotifications` | none; returns every plugin's declared modules/events |
| `notificationSettings` | none; returns the per-user event/channel matrix |
| `emailDeliveries` / `emailDeliveryDetail` | `status`, `source`, `provider`, `searchValue`, date range, cursor params / `_id!` |
| `emailAddresses` | `lane`, `suppressionReason`, `searchValue`, `emails`, cursor params |
| `emailRampStatus` | none |
| `markNotificationAsRead` / `markAsReadNotifications` | `_id!` / notification filters |
| `archiveNotification` / `archiveNotifications` | `_id!` / `ids`, `archiveAll`, `filters` |
| `updateNotificationSettingsEvent` / `Channel` | `input: { event, enabled, channels }` / `input: { channel, enabled, metadata }` |
| `emailAddressRelease` / `emailRampRelease` | `email!`, `note!` / `note!` |

### How plugins register notifications

A plugin declares its notification modules and events in `src/meta/notifications.ts`:

```ts
// backend/plugins/sales_api/src/meta/notifications.ts (trimmed)
export const notifications = {
  plugin: 'sales',
  modules: [
    {
      name: 'deals',
      description: 'Deals',
      icon: 'IconChecklist',
      events: [
        { name: 'dealAssignee', title: 'Deal assignee', description: 'Triggered when a user is assigned to a deal' },
      ],
    },
  ],
};
```

`pluginsNotifications` merges these with `CORE_NOTIFICATION_MODULES` (`modules/notifications/constants.ts`) for the settings UI, where users toggle events and channels via `updateNotificationSettingsEvent`. To emit, a plugin calls `sendNotification(subdomain, { userIds, kind, notificationType, ... })` from [erxes-api-shared/core-modules](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/erxes-api-shared/src/core-modules/notifications/utils.ts); it validates the payload, creates the in-app record over tRPC (`module: 'notifications'`, `action: 'create'`), then `sendNotificationChannels` fans out to email (`sendNotificationEmail`) when the user has that channel enabled. Frontend plugins render entries in their own notification widgets under `frontend/libs/ui-modules` / plugin `NotificationsWidgets`.

The email path is detailed in [Email Delivery](https://erxes.io/docs/api-email): `EmailSenders` (verified from-addresses, `senderConfirm` route), `EmailAddresses` (suppression lanes; release with `emailAddressRelease`), `EmailDeliveries` (per-message state), `EmailRamp` (sending warmup; release with `emailRampRelease`).

## Permissions

`broadcastRead` (`always`), `broadcastCreate`, `broadcastUpdate`, `broadcastDelete`, `broadcastConfigsManage`; scope `own` (records in `createdBy`) or `all`. Notifications have no admin actions: users manage only their own feed and settings.

## Example

```graphql
mutation SendTest {
  engageMessageSendTestEmail(
    from: "news@example.com"
    to: ["qa@example.com"]
    title: "Test"
    content: "<p>Hello</p>"
  )
}

query MyNotifications {
  notifications(status: UNREAD, limit: 20) {
    list { _id }
    totalCount
  }
}
```

