Skip to main content

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:

FieldPurpose
userIdRecipient user. Every list, read, and delete operation is scoped to this user.
typeProducer-defined event type, for example approval_pending, mention, or document_ready.
titleShort inbox label.
bodyOptional longer text.
entityType / entityIdOptional link target, such as an approval, message, document, or generation.
readAtnull until the user marks the notification as read.
createdAtSort 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:

TypeProducerEntity
approval_pendingAgent runner and approvals routes when HITL review is required or escalated.approval
mentionCollaboration features when a user is directly referenced in a thread or document.message, comment, or document
document_readyDocument generation and knowledge ingestion when an output becomes available.document or document_generation
systemOperational 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.

OperationEndpointNotes
List inboxGET /api/v1/notifications?limit=20&offset=0Returns items, pagination, and unreadCount.
Filter unreadGET /api/v1/notifications?unreadOnly=trueUses readAt IS NULL.
Mark one readPATCH /api/v1/notifications/:id/readIdempotent; already-read rows are returned unchanged.
Mark all readPOST /api/v1/notifications/read-allUpdates all unread rows for the current user.
Delete oneDELETE /api/v1/notifications/:idRemoves 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:

  1. connect with ?token=<jwt>, or
  2. 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 readAt is null.
  • Marking a notification as read sets readAt to the current server time.
  • read-all updates 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_pending and document_ready rows longer when the linked approval or document needs to stay discoverable for audit;
  • deletes in small batches ordered by createdAt to 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:

PreferenceBehavior
directShow the WebSocket event immediately and keep the row in the inbox.
digestKeep the row in the inbox, suppress immediate UI interruption, and include it in a scheduled summary.
mutedKeep 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:

  1. create the Notification row inside the same logical flow as the source event;
  2. set entityType and entityId whenever the user should navigate to something;
  3. call emitNotification(notification) after the row is committed;
  4. add or extend tests for REST inbox behavior and WebSocket broadcast;
  5. document whether the event defaults to direct or digest delivery.