Notifications
AgentCore stores in-app notifications per user and pushes new events over a per-user WebSocket channel. Notifications cover work that needs attention outside the current chat: approvals, mentions, generated documents, and operational alerts.
Data Model
Notifications are stored in the notifications table:
| Field | Purpose |
|---|---|
userId | Recipient user. Every list, read, and delete operation is scoped to this user. |
type | Producer-defined event type, for example approval_pending, mention, or document_ready. |
title | Short inbox label. |
body | Optional longer text. |
entityType / entityId | Optional link target, such as an approval, message, document, or generation. |
readAt | null until the user marks the notification as read. |
createdAt | Sort key for inbox and retention jobs. |
Indexes on (userId, readAt) and (userId, createdAt) keep unread counts, pagination, and retention scans cheap.
Notification Types
type is a string so new producers can be added without a database migration. Current and expected types:
| Type | Producer | Entity |
|---|---|---|
approval_pending | Agent runner and approvals routes when HITL review is required or escalated. | approval |
mention | Collaboration features when a user is directly referenced in a thread or document. | message, comment, or document |
document_ready | Document generation and knowledge ingestion when an output becomes available. | document or document_generation |
system | Operational notices that are not tied to a business object. | Optional |
The notification row is the durable record. WebSocket is live transport only — clients still need to hit the REST inbox after reconnecting.
REST API
All routes require JWT auth and operate on the current user only.
| Operation | Endpoint | Notes |
|---|---|---|
| List inbox | GET /api/v1/notifications?limit=20&offset=0 | Returns items, pagination, and unreadCount. |
| Filter unread | GET /api/v1/notifications?unreadOnly=true | Uses readAt IS NULL. |
| Mark one read | PATCH /api/v1/notifications/:id/read | Idempotent; already-read rows are returned unchanged. |
| Mark all read | POST /api/v1/notifications/read-all | Updates all unread rows for the current user. |
| Delete one | DELETE /api/v1/notifications/:id | Removes only rows owned by the current user. |
The route layer checks both id and userId on every call, so users can't read or delete another user's notifications by guessing ids.
WebSocket Broadcasting
Live delivery is on /ws/notifications.
Two auth options:
- connect with
?token=<jwt>, or - connect, then send
{ "action": "auth", "token": "<jwt>" }as the first message.
If auth doesn't complete within five seconds, the socket closes with code 4001.
After auth, the socket subscribes to an in-process EventEmitter keyed by userId. Producers create the database row, then call emitNotification(). The WebSocket sends:
{
"type": "notification.created",
"payload": {
"id": "clx...",
"userId": "clu...",
"type": "approval_pending",
"title": "Approval required",
"body": "A generated reply needs review",
"entityType": "approval",
"entityId": "clp...",
"readAt": null,
"createdAt": "2026-04-16T10:15:00.000Z"
}
}
Because the emitter is process-local, horizontal deployments must add a shared pub/sub layer before multiple API replicas can broadcast to sockets connected to different pods. Redis pub/sub is the natural fit because Redis is already required for BullMQ.
Read State
Unread state is derived from readAt.
- A notification is unread when
readAtisnull. - Marking a notification as read sets
readAtto the current server time. read-allupdates every unread notification for the current user.- Deleting a row removes it from the inbox and from future unread counts.
Clients should update local read state optimistically, then re-sync with GET /notifications after reconnect or a failed write.
Retention Policy
There's no automatic purge in the request path. For production, run a scheduled cleanup job that:
- keeps unread notifications until they're read or deleted;
- keeps read notifications for 90 days;
- keeps
approval_pendinganddocument_readyrows longer when the linked approval or document needs to stay discoverable for audit; - deletes in small batches ordered by
createdAtto avoid long table locks.
The cleanup job must never delete the linked business entity — it only removes the inbox pointer.
Client Preferences
Preferences live at the client/product layer as direct or digest delivery:
| Preference | Behavior |
|---|---|
direct | Show the WebSocket event immediately and keep the row in the inbox. |
digest | Keep the row in the inbox, suppress immediate UI interruption, and include it in a scheduled summary. |
muted | Keep audit-critical rows but do not surface them in proactive UI. |
Until preferences are persisted server-side, clients keep local preferences per notification type and fetch the canonical unread count from the server.
Producer Checklist
When adding a new producer:
- create the
Notificationrow inside the same logical flow as the source event; - set
entityTypeandentityIdwhenever the user should navigate to something; - call
emitNotification(notification)after the row is committed; - add or extend tests for REST inbox behavior and WebSocket broadcast;
- document whether the event defaults to direct or digest delivery.