The @cap-js/notifications package is a CDS plugin that provides support for publishing business notifications in SAP Build Work Zone.
- Setup
- Getting Started
- Running the Sample
- Define Notification Types
- Send Notifications
- API Reference
- Test-drive Locally
- Run in Production
- Advanced Usage
- Contributing
- Code of Conduct
- Licensing
Requirements: Node.js >= 20, @sap/cds >= 8.
To enable notifications, simply add this self-configuring plugin package to your project:
npm add @cap-js/notificationsAfter installing the plugin, you can send notifications in two ways:
Send a simple notification using notify():
const alert = await cds.connect.to('notifications')
await alert.notify({
recipients: ['user@example.com'],
title: 'Book Order Received',
description: 'Your order for "Wuthering Heights" is being processed.'
})Define a notification event in your service model with @notification:
using { CatalogService } from './cat-service';
extend service CatalogService with {
@notification: {
template: {
title: 'Book {{title}} Ordered',
subtitle: '{{buyer}} ordered {{title}}'
}
}
event BookOrdered {
title : String;
buyer : String;
}
}Emit the event from your service handler:
this.on('submitOrder', async req => {
// ... process order ...
await this.emit('BookOrdered', {
title: book.title,
buyer: req.user.id,
recipients: ['user@example.com']
})
})The plugin intercepts the event and sends the notification automatically. During local development, notifications are printed to the console. No BTP connection required.
This section uses the Bookshop sample included in this repository.
cd tests/bookshop
npm install
cds watchThe server starts at http://localhost:4004. During local development, the plugin prints the notifications to the console thus no BTP account is yet needed.
The bookshop's submitOrder action reduces book stock and emits a BookOrderedNotify event. The plugin intercepts that event and sends a notification.
Open tests/bookshop/test/http/CatalogService.http in VS Code and click Send Request above the submitOrder block. This file has the server URL and credentials pre-configured.
In the server console you will see the notification printed to confirm the notification was successful.
Open tests/bookshop/srv/notifications.cds. This file defines the BookOrderedNotify event and all of its @notification annotations. Change the subtitle template to include the quantity:
// Before
subtitle: '{i18n>BOOK_ORDERED_SUBTITLE}',
// After
subtitle: '{{buyer}} ordered {{quantity}}x {{title}}',Save the file and cds watch reloads automatically. Send the request again and the updated subtitle appears in the console output. From here, try adjusting the priority expression, adding a @description, or exploring the other annotations on the event.
To see the notification appear in the Work Zone, you need a BTP subaccount with SAP Build Work Zone and the SAP Alert Notification service configured.
- Follow the SAP Build Work Zone setup guide to subscribe to the service and configure the required
SAP_Notificationsdestination in your subaccount. - Bind your local environment to the destination service instance in CF using
cds bind, then runcds watch --profile hybridto connect to BTP destinations from your local machine.
On startup the plugin registers your notification types automatically. Submitting an order will now deliver a notification to the bell in Work Zone for the recipient.
Note
The bookshop sample uses in-app notifications by default. The Work Zone bell icon shows notifications for recipients identified by their SAP BTP Global User ID (UUID). To test email delivery as well, additional setup is required. For enabling, see email delivery and default email delivery. For required BTP configuration see the SMTP mail destination guide.
Notifications are based on notification types: templates that define how a notification looks, including titles, subtitles, and email content. These types can be defined in two ways, both of which can be used together and are merged at startup.
The recommended approach is to define the notification type directly in your service model by annotating events with @notification. The plugin discovers and registers them automatically during startup.
Define an event in your srv/ model:
using { CatalogService } from './cat-service';
extend service CatalogService with {
@description: 'Sent when a book is ordered'
@notification: {
template: {
title : 'Book {{title}} Ordered',
publicTitle : 'Book Ordered',
subtitle : '{{buyer}} ordered {{title}}',
groupedTitle : 'Bookshop Updates'
}
}
@Common.SemanticObject: 'Books'
@Common.SemanticObjectAction: 'display'
event BookOrdered {
title : String;
buyer : String;
}
}Any event with at least one @notification annotation (the bare @notification flag or any @notification.* property) is picked up. The notification type key is derived from the event name. Namespace prefixes are stripped, my.bookshop.BookOrdered becomes BookOrdered.
Note
The plugin automatically injects a recipients element into every notification event at model-load time; you don't need to declare it yourself.
Important
The event must be contained within a service either by defining it directly inside a service or by using extend service / using to include it in an existing one.
Common annotations:
@notification: {
template: {
title : 'Book {{title}} Ordered',
publicTitle : 'Book Ordered',
subtitle : '{{buyer}} ordered {{title}}',
groupedTitle : 'Bookshop Updates', // Group header for multiple notifications
email: {
subject: 'Your order: {{title}}',
html : './email-template.html' // Path to HTML template or inline HTML
}
},
priority: #HIGH // Priority: LOW, NEUTRAL, MEDIUM, HIGH
}
event BookOrdered { ... }For a complete list of supported annotations and their mappings, see Annotation Reference.
The @notification annotation values support {i18n>key} syntax. Keys are automatically resolved against your project's i18n bundles at startup. Templates are generated for each locale where at least one translation differs from the default language.
@notification.template.title: '{i18n>BOOK_ORDERED_TITLE}'
@notification.template.subtitle: '{i18n>BOOK_ORDERED_SUBTITLE}'
event BookOrdered { ... }The email.html annotation accepts either an inline HTML string or a path to an .html file relative to the .cds source file. The file is read at startup and i18n placeholders within it are resolved.
@notification: {
template: {
email: {
subject: 'Your order: {{title}}',
html : './book-ordered-email.html'
}
}
}
event BookOrdered { ... }Notifications can be assigned different priority levels: LOW, NEUTRAL (default), MEDIUM, or HIGH. These priorities affect how notifications are displayed and sorted in SAP Build Work Zone.
Priorities can be set statically using # enum values, or dynamically using CDS Expression Language (CXL) with conditions evaluated at runtime.
Static priority:
@notification.priority: #HIGH
event BookOrdered { ... }Dynamic priority:
Priority can be computed at runtime from event data using CDS ternary expressions evaluated against the database:
@notification.priority: (quantity > 5 ? #HIGH : #LOW)
event BookOrdered {
quantity : Integer;
title : String;
}Complex expressions using CDS functions are also supported:
@notification.priority: (days_between(orderDate, deliveryDate) > 7 ? #HIGH : #LOW)
event LateDelivery {
orderDate : Date;
deliveryDate : Date;
}Dynamic priority requires the event to be emitted via this.emit(...) so the plugin can intercept and evaluate it.
Running cds build processes all @notification annotated events and generates a merged notification-types.json to the build output, combining types from CDS annotations with any types from the JSON file.
An alternative approach is to define notification types statically in srv/notification-types.json or custom path (see Custom Notification Types Path):
[
{
"NotificationTypeKey": "BookOrdered",
"NotificationTypeVersion": "1",
"Templates": [
{
"Language": "en",
"TemplatePublic": "Book Ordered",
"TemplateSensitive": "Book '{{title}}' Ordered",
"TemplateGrouped": "Bookshop Updates",
"TemplateLanguage": "mustache",
"Subtitle": "{{buyer}} ordered {{title}}."
}
]
}
]Note
i18n resolution, HTML file paths, and priority annotations (@notification.priority) are only available when using CDS annotations (Option A). The JSON file format uses pre-resolved strings.
Email delivery can be configured for notification types in both approaches. It requires the SAP Alert Notification Service with the business-notifications plan and a configured SMTP mail destination.
Warning
The business-notifications plan validates all of your notification types at registration time, not only the ones with email. This means every notification type in your app, even purely in-app ones, must have TemplatePublic (mapped from publicTitle) and TemplateGrouped (mapped from groupedTitle) set, or startup registration will fail.
Via CDS annotations:
@notification: {
template: {
title : 'Book {{title}} Ordered',
publicTitle : 'Book Ordered',
subtitle : '{{buyer}} ordered {{title}}',
groupedTitle : 'Bookshop Updates',
email: {
subject: 'Your order: {{title}}',
html : './book-ordered-email.html'
}
},
deliveryChannels: [{ channel: #Mail, enabled: true, defaultPreference: true, editablePreference: true }]
}
event BookOrdered { ... }Via JSON:
{
"NotificationTypeKey": "BookOrdered",
"Templates": [
{
"Language": "en",
"TemplatePublic": "Book Ordered",
"TemplateSensitive": "Book '{{title}}' Ordered",
"TemplateGrouped": "Bookshop Updates",
"TemplateLanguage": "mustache",
"EmailSubject": "Your order: {{title}}",
"EmailHtml": "<p>Thanks for ordering <b>{{title}}</b>!</p>"
}
],
"DeliveryChannels": [
{ "Type": "MAIL", "Enabled": true, "DefaultPreference": true, "EditablePreference": true }
]
}There are two patterns for sending notifications.
If you defined your notification type as a CDS event with a @notification annotation, the plugin hooks into your service automatically. Simply emit the event from your service handler.
this.on('submitOrder', async req => {
const book = await SELECT.one.from('Books').where({ ID: req.data.book })
await this.emit('BookOrdered', {
title: book.title,
buyer: req.user.id,
recipients: ['reader@bookshop.example'],
})
})The plugin registers an event handler which forwards the notification to ANS. You can still register your own event handlers if you need to process the event yourself. The plugin's handler runs alongside them.
You can also connect to the notification service and call notify() directly. This works with or without pre-defined notification types.
Simple notification (no pre-defined type needed):
const alert = await cds.connect.to('notifications')
await alert.notify({
recipients: [ ...readers() ],
priority: "HIGH",
title: "New book arrived!",
description: "Book 'Wuthering Heights' has been added to the catalog."
})Note
The simple API supports only recipients, priority, title, and description. For advanced properties use a named notification type or the low-level API.
Named notification type:
await alert.notify('BookOrdered', {
recipients: [ buyer.id ],
data: {
title: book.title,
buyer: buyer.name,
}
})It is possible to pass an array to notify() to send multiple notifications in a single call. This triggers only one outbox event, reducing the number of transactions when notifying many recipients. If some items fail, the successful ones are still delivered. Failures are logged as warnings, and the call only throws if all items fail.
alert.notify('BookOrdered', [
{ recipients: [ buyer1.id ], data: { title: book.title, buyer: buyer1.name } },
{ recipients: [ buyer2.id ], data: { title: book.title, buyer: buyer2.name } },
])Warning
Batch sending is only available via notify([...]). CDS event emission dispatches one event per call.
Alternatively, the default notification template can be used:
await alert.notify([
{ type: 'BookOrdered', recipients: [buyer1.id], data: { title: book1.title, buyer: buyer1.name } },
{ type: 'BookOrdered', recipients: [buyer2.id], data: { title: book2.title, buyer: buyer2.name } },
])For notify({ recipients, title, ... }), no pre-defined notification type is needed. The plugin uses a built-in Default template.
| Parameter | Required | Description |
|---|---|---|
recipients |
yes | Array of recipient identifiers: email addresses or SAP BTP Global User IDs (UUIDs) |
title |
yes | Notification title string |
priority |
no | LOW, NEUTRAL (default), MEDIUM, or HIGH |
description |
no | Subtitle text |
For notify('TypeKey', payload) or notify({ type: 'TypeKey', ... }), a notification using a pre-defined notification type is sent.
| Parameter | Required | Description |
|---|---|---|
recipients |
yes | Array of recipient identifiers: email addresses or SAP BTP Global User IDs (UUIDs) |
type |
yes | Notification type key (e.g. 'BookOrdered') |
data |
no | Key-value pairs used to fill mustache placeholders in the type template |
priority |
no | LOW, NEUTRAL (default), MEDIUM, or HIGH |
Note: Recipients can be email addresses (e.g.
user@example.com) or SAP BTP Global User IDs (UUID format, e.g.a1b2c3d4-...). Inautomode (default), the plugin detects the format per recipient and uses the correct key automatically. See Authentication Identifier for details.
- Property values must not exceed 255 characters. Longer values cause the notification to be rejected.
- TargetParameters values longer than 250 characters are silently dropped.
- Event element names must not exceed 128 characters. Violations are caught at
cds buildtime.
Complete mapping of CDS annotations to notification fields:
| Annotation | ANS Field | Description |
|---|---|---|
@description |
Description |
Notification type description |
@notification.template.title |
TemplateSensitive |
Main notification title (supports placeholders) |
@notification.template.publicTitle |
TemplatePublic |
Public fallback title |
@notification.template.subtitle |
Subtitle |
Subtitle text |
@notification.template.groupedTitle |
TemplateGrouped |
Group header for multiple notifications |
@notification.template.email.subject |
EmailSubject |
Email subject line |
@notification.template.email.html |
EmailHtml |
Inline HTML or path to .html file |
@Common.SemanticObject |
NavigationTargetObject |
Navigation target object |
@Common.SemanticObjectAction |
NavigationTargetAction |
Navigation action |
@notification.priority |
Priority |
LOW, NEUTRAL, MEDIUM, or HIGH |
During local development, notifications are mocked and printed to the console. No external service is required.
As a prerequisite, configure a destination named SAP_Notifications in your BTP subaccount. The plugin uses this destination by default to connect to the notification service in hybrid and production environments.
Notification types are automatically registered and kept in sync with the notification service each time the application starts. Any additions, changes, or removals to your notification types, whether from CDS annotations or the JSON file, are applied on the next startup. No manual cds build or content deployment step is required.
Once the application is deployed and integrated with SAP Build Work Zone, notifications appear under the Fiori notifications icon.
The plugin reads srv/notification-types.json as the default JSON types file. To use a different path:
"cds": {
"requires": {
"notifications": {
"types": "srv/my-notification-types.json"
}
}
}To make notification type keys unique per application, the plugin prefixes them with the application name from package.json by default. To use a custom prefix:
"cds": {
"requires": {
"notifications": {
"prefix": "my-custom-prefix"
}
}
}To override the default SAP_Notifications destination name:
"cds": {
"requires": {
"notifications": {
"destination": "MY_CUSTOM_DESTINATION"
}
}
}cds.env.requires.notifications.authenticationIdentifier controls which recipient key is used when publishing notifications.
auto(default): the recipient key is chosen per recipient. Values in UUID format are treated as SAP BTP Global User IDs and published withGlobalUserId; everything else is published withRecipientId. A warning is logged if a value is neither a UUID nor an email address. This allows mixing UUIDs and email addresses in the samerecipientsarray without any configuration.UserUUID: always useGlobalUserId. Use this when the Work Zone authentication identifier is set toUser ID.RecipientId: always useRecipientId. Use this when recipients are identified by email or login name.
Note
For email notifications sent with a User ID, a destination to the Identity Directory Service (IDS) must be configured for the email address lookup.
For Work Zone authentication identifier configuration, see Work Zone Subaccount Settings.
To enable email delivery for all notification types without annotating each one individually, set defaultEmailDelivery to true:
"cds": {
"requires": {
"notifications": {
"defaultEmailDelivery": true
}
}
}This adds a MAIL delivery channel (enabled, default preference on, user-editable) to every notification type that does not already have a deliveryChannels annotation.
By default, the notification service uses an outbox (outbox: true): notify() resolves as soon as the message is queued, not when it has been sent to ANS. This means the HTTP response from ANS is not returned. To send synchronously and receive the HTTP response:
"cds": {
"requires": {
"notifications": {
"outbox": false
}
}
}To disable the plugin without removing it, set enabled: false:
"cds": {
"requires": {
"notifications": {
"enabled": false
}
}
}This prevents the plugin from registering its hooks which results into no automatic this.emit() interception, no notification type registration, and no build task. Direct calls to cds.connect.to('notifications').notify() are not affected by this flag.
For full control, pass the complete notification object directly as described in the ANS API documentation.
alert.notify({
recipients: [...readers()],
type: "BookOrdered",
priority: 'NEUTRAL',
data: {
title: book.title,
buyer: buyer.name,
},
OriginId: "Example Origin Id",
NotificationTypeVersion: "1",
ProviderId: "/SAMPLEPROVIDER",
ActorId: "BACKENDACTORID",
ActorDisplayText: "ActorName",
ActorImageURL: "https://some-url",
NotificationTypeTimestamp: "2022-03-15T09:58:42.807Z",
TargetParameters: [
{ "Key": "string", "Value": "string" }
]
})alert.notify({
NotificationTypeKey: 'BookOrdered',
NotificationTypeVersion: '1',
Priority: 'NEUTRAL',
Properties: [
{ Key: 'title', IsSensitive: false, Language: 'en', Value: 'Wuthering Heights', Type: 'String' },
{ Key: 'buyer', IsSensitive: false, Language: 'en', Value: 'reader@bookshop.com', Type: 'String' }
],
Recipients: [
{ RecipientId: "reader1@bookshop.com" },
{ RecipientId: "reader2@bookshop.com" }
]
})This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.
Copyright 2026 SAP SE or an SAP affiliate company and contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.

