diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts index 9e1567392f..744795ed61 100644 --- a/src/data/nav/aitransport.ts +++ b/src/data/nav/aitransport.ts @@ -50,6 +50,10 @@ export default { name: 'Online status', link: '/docs/ai-transport/sessions-identity/online-status', }, + { + name: 'Push notifications', + link: '/docs/ai-transport/sessions-identity/push-notifications', + }, { name: 'Resuming sessions', link: '/docs/ai-transport/sessions-identity/resuming-sessions', diff --git a/src/pages/docs/ai-transport/sessions-identity/push-notifications.mdx b/src/pages/docs/ai-transport/sessions-identity/push-notifications.mdx new file mode 100644 index 0000000000..ac71cae241 --- /dev/null +++ b/src/pages/docs/ai-transport/sessions-identity/push-notifications.mdx @@ -0,0 +1,641 @@ +--- +title: "Push notifications" +meta_description: "Notify users via push notifications when an AI agent completes work while they are offline" +meta_keywords: "push notifications, offline notifications, background processing, async AI, agent completion, deep linking, mobile notifications" +--- + +When agents perform long-running tasks, users may go offline before the work completes. Rather than requiring users to keep their app open, agents can send push notifications to bring them back when results are ready. + +Push notifications complement the [online status](/docs/ai-transport/sessions-identity/online-status) capabilities of AI Transport. Use presence to detect when a user goes offline, then use push notifications to reach them on their devices when there's something to come back to. + +## When to use push notifications + +Push notifications are suited to scenarios where there is a delay between a user's request and the agent's completion: + +- Long-running agent tasks such as data analysis, report generation, or multi-step research that may take minutes to complete. +- Async workflows where users submit a request and move on, such as "generate a summary of this quarter's sales data and notify me when it's done". +- Background processing where agents continue work after users close their app, such as processing uploads, running batch operations, or monitoring for specific conditions. + +If a user is still online when the agent completes work, deliver results directly through the channel. Push notifications are for reaching users who have already disconnected. + +## Set up push notifications + +Before agents can send push notifications, the user's devices must be registered with Ably's push notification service. + + + + + +## Opt-in patterns + +Users should have control over when they receive push notifications. There are two common opt-in patterns. + +### Per-session opt-in + +Users can request a notification for a specific task. This is useful for tasks that are typically fast but occasionally take longer, where a blanket notification preference would be noisy. + +The user sends a message to the agent indicating they want to be notified when the current task completes: + + +```javascript +// Client code +const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}"); + +// Request notification when the current task finishes +await channel.publish("request", { + prompt: "Analyze last quarter's sales data and generate a report", + notifyOnComplete: true +}); +``` +```python +# Client code +channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}") + +# Request notification when the current task finishes +await channel.publish("request", { + "prompt": "Analyze last quarter's sales data and generate a report", + "notifyOnComplete": True +}) +``` +```java +// Client code +Channel channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}"); + +// Request notification when the current task finishes +JsonObject data = new JsonObject(); +data.addProperty("prompt", "Analyze last quarter's sales data and generate a report"); +data.addProperty("notifyOnComplete", true); +channel.publish("request", data); +``` + + +The agent reads this flag and stores the preference for the duration of the task: + + +```javascript +// Agent code +let notifyUserOnComplete = false; + +await channel.subscribe("request", (message) => { + notifyUserOnComplete = message.data.notifyOnComplete === true; + // Begin processing the task... +}); +``` +```python +# Agent code +notify_user_on_complete = False + +async def on_request(message): + global notify_user_on_complete + notify_user_on_complete = message.data.get("notifyOnComplete", False) + # Begin processing the task... + +await channel.subscribe("request", on_request) +``` +```java +// Agent code +AtomicBoolean notifyUserOnComplete = new AtomicBoolean(false); + +channel.subscribe("request", message -> { + JsonObject data = (JsonObject) message.data; + notifyUserOnComplete.set(data.has("notifyOnComplete") && data.get("notifyOnComplete").getAsBoolean()); + // Begin processing the task... +}); +``` + + +### App-wide opt-in + +Users can enable notifications for all agent tasks through their app settings. The agent checks this preference before sending any notification. Store the preference in your application's user settings and pass it to the agent as configuration: + + +```javascript +// Agent code +async function shouldNotifyUser(userId) { + // Check the user's notification preference from your application's settings + const userSettings = await getUserSettings(userId); + return userSettings.pushNotificationsEnabled === true; +} +``` +```python +# Agent code +async def should_notify_user(user_id): + # Check the user's notification preference from your application's settings + user_settings = await get_user_settings(user_id) + return user_settings.get("push_notifications_enabled", False) +``` +```java +// Agent code +boolean shouldNotifyUser(String userId) { + // Check the user's notification preference from your application's settings + UserSettings userSettings = getUserSettings(userId); + return userSettings.isPushNotificationsEnabled(); +} +``` + + +## Sending notifications conditionally + +Sending a push notification every time an agent completes work would be disruptive if the user is already looking at the results. Check the user's online status before deciding whether to send a push notification. + +### Check if the user is online + +Use [presence](/docs/ai-transport/sessions-identity/online-status#detecting-offline) to determine if the user is still connected. If they are online, publish results to the channel as normal. If they are offline, send a push notification: + + +```javascript +// Agent code +async function onTaskComplete(channel, userId, result) { + const members = await channel.presence.get(); + const userIsOnline = members.some(m => m.clientId === userId); + + if (userIsOnline) { + // User is online, deliver results through the channel + await channel.publish("result", result); + } else { + // User is offline, send a push notification + await sendPushNotification(userId, channel.name, result); + } +} +``` +```python +# Agent code +async def on_task_complete(channel, user_id, result): + members = await channel.presence.get() + user_is_online = any(m.client_id == user_id for m in members) + + if user_is_online: + # User is online, deliver results through the channel + await channel.publish("result", result) + else: + # User is offline, send a push notification + await send_push_notification(user_id, channel.name, result) +``` +```java +// Agent code +void onTaskComplete(Channel channel, String userId, JsonObject result) throws AblyException { + PresenceMessage[] members = channel.presence.get(); + boolean userIsOnline = Arrays.stream(members) + .anyMatch(m -> m.clientId.equals(userId)); + + if (userIsOnline) { + // User is online, deliver results through the channel + channel.publish("result", result); + } else { + // User is offline, send a push notification + sendPushNotification(userId, channel.name, result); + } +} +``` + + + + +### Multi-device awareness + +A user may be connected on their desktop but not on their mobile phone. When the user has entered presence with [device metadata](/docs/ai-transport/sessions-identity/online-status#multiple-devices), the agent can use this information to make smarter notification decisions. + +For example, skip push notifications entirely if the user has any active device, or only send to mobile when the user is not on desktop: + + +```javascript +// Agent code +async function shouldSendPush(channel, userId) { + const members = await channel.presence.get(); + const userDevices = members.filter(m => m.clientId === userId); + + if (userDevices.length === 0) { + // User is completely offline, send push notification + return true; + } + + // User has at least one active device, no push needed + // Results will be delivered via the channel + return false; +} +``` +```python +# Agent code +async def should_send_push(channel, user_id): + members = await channel.presence.get() + user_devices = [m for m in members if m.client_id == user_id] + + if len(user_devices) == 0: + # User is completely offline, send push notification + return True + + # User has at least one active device, no push needed + # Results will be delivered via the channel + return False +``` +```java +// Agent code +boolean shouldSendPush(Channel channel, String userId) throws AblyException { + PresenceMessage[] members = channel.presence.get(); + long userDeviceCount = Arrays.stream(members) + .filter(m -> m.clientId.equals(userId)) + .count(); + + if (userDeviceCount == 0) { + // User is completely offline, send push notification + return true; + } + + // User has at least one active device, no push needed + // Results will be delivered via the channel + return false; +} +``` + + +## Send a push notification + +Use the [Push Admin API](/docs/push/publish#direct-publishing) to send a notification directly to a user by their `clientId`. This delivers the notification to all devices registered to that user: + + +```javascript +// Agent code +const rest = new Ably.Rest("{{API_KEY}}"); + +var recipient = { + clientId: "user-123" +}; + +var data = { + notification: { + title: "Task complete", + body: "Your sales report is ready to view" + }, + data: { + channelName: "session-abc-123", + type: "task-complete" + } +}; + +rest.push.admin.publish(recipient, data); +``` +```python +# Agent code +rest = AblyRest("{{API_KEY}}") + +recipient = { + "clientId": "user-123" +} + +data = { + "notification": { + "title": "Task complete", + "body": "Your sales report is ready to view" + }, + "data": { + "channelName": "session-abc-123", + "type": "task-complete" + } +} + +rest.push.admin.publish(recipient, data) +``` +```java +// Agent code +AblyRest rest = new AblyRest("{{API_KEY}}"); + +JsonObject payload = JsonUtils.object() + .add("notification", JsonUtils.object() + .add("title", "Task complete") + .add("body", "Your sales report is ready to view") + ) + .add("data", JsonUtils.object() + .add("channelName", "session-abc-123") + .add("type", "task-complete") + ) + .toJson(); + +rest.push.admin.publish(new Param[]{new Param("clientId", "user-123")}, payload); +``` + + + + +## Notification payloads + +Structure your notification payloads to give users enough context to decide whether to act immediately and to navigate directly to the relevant session when they do. + +### Deep linking + +Include the channel name or session ID in the notification's `data` field so your app can navigate directly to the conversation when the user taps the notification: + + +```javascript +// Agent code +var recipient = { clientId: userId }; + +var data = { + notification: { + title: "Research complete", + body: "Your market analysis is ready" + }, + data: { + channelName: channel.name, + sessionId: "session-abc-123", + action: "view-results" + } +}; + +rest.push.admin.publish(recipient, data); +``` +```python +# Agent code +recipient = {"clientId": user_id} + +data = { + "notification": { + "title": "Research complete", + "body": "Your market analysis is ready" + }, + "data": { + "channelName": channel.name, + "sessionId": "session-abc-123", + "action": "view-results" + } +} + +rest.push.admin.publish(recipient, data) +``` +```java +// Agent code +JsonObject payload = JsonUtils.object() + .add("notification", JsonUtils.object() + .add("title", "Research complete") + .add("body", "Your market analysis is ready") + ) + .add("data", JsonUtils.object() + .add("channelName", channel.name) + .add("sessionId", "session-abc-123") + .add("action", "view-results") + ) + .toJson(); + +rest.push.admin.publish(new Param[]{new Param("clientId", userId)}, payload); +``` + + +On the client side, read the `data` fields when handling the notification to navigate to the correct session: + + +```javascript +// Client code - handling a push notification tap +function onNotificationTap(notification) { + const { channelName, sessionId } = notification.data; + // Navigate to the session and reattach to the channel + navigateToSession(sessionId, channelName); +} +``` +```python +# Client code - handling a push notification tap +def on_notification_tap(notification): + channel_name = notification.data["channelName"] + session_id = notification.data["sessionId"] + # Navigate to the session and reattach to the channel + navigate_to_session(session_id, channel_name) +``` +```java +// Client code - handling a push notification tap +void onNotificationTap(Intent intent) { + String channelName = intent.getStringExtra("channelName"); + String sessionId = intent.getStringExtra("sessionId"); + // Navigate to the session and reattach to the channel + navigateToSession(sessionId, channelName); +} +``` + + +### Completion summary + +Include a meaningful summary of what completed in the notification body so users can triage without opening the app: + + +```javascript +// Agent code +const summary = generateSummary(result); + +var recipient = { clientId: userId }; + +var data = { + notification: { + title: "Report ready", + body: summary // e.g. "Q4 sales report: revenue up 12%, 3 action items identified" + }, + data: { + channelName: channel.name, + sessionId: sessionId, + action: "view-results" + } +}; + +rest.push.admin.publish(recipient, data); +``` +```python +# Agent code +summary = generate_summary(result) + +recipient = {"clientId": user_id} + +data = { + "notification": { + "title": "Report ready", + "body": summary # e.g. "Q4 sales report: revenue up 12%, 3 action items identified" + }, + "data": { + "channelName": channel.name, + "sessionId": session_id, + "action": "view-results" + } +} + +rest.push.admin.publish(recipient, data) +``` +```java +// Agent code +String summary = generateSummary(result); + +JsonObject payload = JsonUtils.object() + .add("notification", JsonUtils.object() + .add("title", "Report ready") + .add("body", summary) // e.g. "Q4 sales report: revenue up 12%, 3 action items identified" + ) + .add("data", JsonUtils.object() + .add("channelName", channel.name) + .add("sessionId", sessionId) + .add("action", "view-results") + ) + .toJson(); + +rest.push.admin.publish(new Param[]{new Param("clientId", userId)}, payload); +``` + + +## Complete example + +The following example demonstrates the full flow: an agent completes a task, checks whether the user is online, and either delivers results through the channel or sends a push notification with deep linking data: + + +```javascript +// Agent code +const ably = new Ably.Realtime({ key: "{{API_KEY}}", clientId: "agent" }); +const rest = new Ably.Rest("{{API_KEY}}"); +const channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}"); + +let notifyOnComplete = false; +let targetUserId = null; + +// Listen for task requests +await channel.subscribe("request", async (message) => { + targetUserId = message.clientId; + notifyOnComplete = message.data.notifyOnComplete === true; + + // Process the task + const result = await processTask(message.data.prompt); + + // Publish results to the channel for history and any connected clients + await channel.publish("result", { + summary: result.summary, + completedAt: Date.now() + }); + + // Check if user wants a push notification and is offline + if (notifyOnComplete) { + const members = await channel.presence.get(); + const userIsOnline = members.some(m => m.clientId === targetUserId); + + if (!userIsOnline) { + rest.push.admin.publish( + { clientId: targetUserId }, + { + notification: { + title: "Task complete", + body: result.summary + }, + data: { + channelName: channel.name, + action: "view-results" + } + } + ); + } + } +}); +``` +```python +# Agent code +ably = AblyRealtime(key="{{API_KEY}}", client_id="agent") +rest = AblyRest("{{API_KEY}}") +channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}") + +notify_on_complete = False +target_user_id = None + +# Listen for task requests +async def on_request(message): + global notify_on_complete, target_user_id + target_user_id = message.client_id + notify_on_complete = message.data.get("notifyOnComplete", False) + + # Process the task + result = await process_task(message.data["prompt"]) + + # Publish results to the channel for history and any connected clients + await channel.publish("result", { + "summary": result["summary"], + "completedAt": int(time.time() * 1000) + }) + + # Check if user wants a push notification and is offline + if notify_on_complete: + members = await channel.presence.get() + user_is_online = any(m.client_id == target_user_id for m in members) + + if not user_is_online: + rest.push.admin.publish( + {"clientId": target_user_id}, + { + "notification": { + "title": "Task complete", + "body": result["summary"] + }, + "data": { + "channelName": channel.name, + "action": "view-results" + } + } + ) + +await channel.subscribe("request", on_request) +``` +```java +// Agent code +ClientOptions options = new ClientOptions(); +options.key = "{{API_KEY}}"; +options.clientId = "agent"; + +AblyRealtime ably = new AblyRealtime(options); +AblyRest rest = new AblyRest("{{API_KEY}}"); +Channel channel = ably.channels.get("{{RANDOM_CHANNEL_NAME}}"); + +channel.subscribe("request", message -> { + try { + JsonObject requestData = (JsonObject) message.data; + String targetUserId = message.clientId; + boolean notifyOnComplete = requestData.has("notifyOnComplete") + && requestData.get("notifyOnComplete").getAsBoolean(); + + // Process the task + JsonObject result = processTask(requestData.get("prompt").getAsString()); + + // Publish results to the channel for history and any connected clients + JsonObject resultData = new JsonObject(); + resultData.addProperty("summary", result.get("summary").getAsString()); + resultData.addProperty("completedAt", System.currentTimeMillis()); + channel.publish("result", resultData); + + // Check if user wants a push notification and is offline + if (notifyOnComplete) { + PresenceMessage[] members = channel.presence.get(); + boolean userIsOnline = Arrays.stream(members) + .anyMatch(m -> m.clientId.equals(targetUserId)); + + if (!userIsOnline) { + JsonObject payload = JsonUtils.object() + .add("notification", JsonUtils.object() + .add("title", "Task complete") + .add("body", result.get("summary").getAsString()) + ) + .add("data", JsonUtils.object() + .add("channelName", channel.name) + .add("action", "view-results") + ) + .toJson(); + + rest.push.admin.publish( + new Param[]{new Param("clientId", targetUserId)}, + payload + ); + } + } + } catch (AblyException e) { + e.printStackTrace(); + } +}); +``` +