diff --git a/src/pages/docs/ai-transport/messaging/tool-calls.mdx b/src/pages/docs/ai-transport/messaging/tool-calls.mdx
index 9a00922a28..4004b459b1 100644
--- a/src/pages/docs/ai-transport/messaging/tool-calls.mdx
+++ b/src/pages/docs/ai-transport/messaging/tool-calls.mdx
@@ -592,6 +592,496 @@ channel.subscribe("tool_result", message -> {
```
+## Progress updates
+
+Some tool calls take significant time to complete, such as processing large files, performing complex calculations, or executing multi-step operations. For long-running tools, streaming progress updates to users provides visibility into execution status and improves the user experience by showing that work is actively happening.
+
+You can deliver progress updates using two approaches:
+
+- Messages: Best for discrete status updates and milestone events
+- LiveObjects: Best for continuous numeric progress and shared state synchronization
+
+### Progress updates via messages
+
+Publish progress messages to the channel as the tool executes, using the `toolCallId` to correlate progress updates with the specific tool call:
+
+
+```javascript
+const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}');
+
+// Publish initial tool call
+await channel.publish({
+ name: 'tool_call',
+ data: {
+ name: 'process_document',
+ args: { documentId: 'doc_123', pages: 100 }
+ },
+ extras: {
+ headers: {
+ responseId: 'resp_abc123',
+ toolCallId: 'tool_456'
+ }
+ }
+});
+
+// Publish progress updates as tool executes
+await channel.publish({
+ name: 'tool_progress',
+ data: {
+ name: 'process_document',
+ status: 'Processing page 25 of 100',
+ percentComplete: 25
+ },
+ extras: {
+ headers: {
+ responseId: 'resp_abc123',
+ toolCallId: 'tool_456'
+ }
+ }
+});
+
+// Continue publishing progress as work progresses
+await channel.publish({
+ name: 'tool_progress',
+ data: {
+ name: 'process_document',
+ status: 'Processing page 75 of 100',
+ percentComplete: 75
+ },
+ extras: {
+ headers: {
+ responseId: 'resp_abc123',
+ toolCallId: 'tool_456'
+ }
+ }
+});
+
+// Publish final result
+await channel.publish({
+ name: 'tool_result',
+ data: {
+ name: 'process_document',
+ result: { processedPages: 100, summary: 'Document processed successfully' }
+ },
+ extras: {
+ headers: {
+ responseId: 'resp_abc123',
+ toolCallId: 'tool_456'
+ }
+ }
+});
+```
+```python
+channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}')
+
+# Publish initial tool call
+await channel.publish(Message(
+ name='tool_call',
+ data={
+ 'name': 'process_document',
+ 'args': {'documentId': 'doc_123', 'pages': 100}
+ },
+ extras={
+ 'headers': {
+ 'responseId': 'resp_abc123',
+ 'toolCallId': 'tool_456'
+ }
+ }
+))
+
+# Publish progress updates as tool executes
+await channel.publish(Message(
+ name='tool_progress',
+ data={
+ 'name': 'process_document',
+ 'status': 'Processing page 25 of 100',
+ 'percentComplete': 25
+ },
+ extras={
+ 'headers': {
+ 'responseId': 'resp_abc123',
+ 'toolCallId': 'tool_456'
+ }
+ }
+))
+
+# Continue publishing progress as work progresses
+await channel.publish(Message(
+ name='tool_progress',
+ data={
+ 'name': 'process_document',
+ 'status': 'Processing page 75 of 100',
+ 'percentComplete': 75
+ },
+ extras={
+ 'headers': {
+ 'responseId': 'resp_abc123',
+ 'toolCallId': 'tool_456'
+ }
+ }
+))
+
+# Publish final result
+await channel.publish(Message(
+ name='tool_result',
+ data={
+ 'name': 'process_document',
+ 'result': {'processedPages': 100, 'summary': 'Document processed successfully'}
+ },
+ extras={
+ 'headers': {
+ 'responseId': 'resp_abc123',
+ 'toolCallId': 'tool_456'
+ }
+ }
+))
+```
+```java
+Channel channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Helper method to create message extras with headers
+MessageExtras createExtras(String responseId, String toolCallId) {
+ JsonObject extrasJson = new JsonObject();
+ JsonObject headers = new JsonObject();
+ headers.addProperty("responseId", responseId);
+ headers.addProperty("toolCallId", toolCallId);
+ extrasJson.add("headers", headers);
+ return new MessageExtras(extrasJson);
+}
+
+// Publish initial tool call
+JsonObject toolCallData = new JsonObject();
+toolCallData.addProperty("name", "process_document");
+JsonObject args = new JsonObject();
+args.addProperty("documentId", "doc_123");
+args.addProperty("pages", 100);
+toolCallData.add("args", args);
+
+Message toolCall = new Message(
+ "tool_call",
+ toolCallData.toString(),
+ createExtras("resp_abc123", "tool_456")
+);
+channel.publish(toolCall);
+
+// Publish progress updates as tool executes
+JsonObject progress1 = new JsonObject();
+progress1.addProperty("name", "process_document");
+progress1.addProperty("status", "Processing page 25 of 100");
+progress1.addProperty("percentComplete", 25);
+
+Message progressMsg1 = new Message(
+ "tool_progress",
+ progress1.toString(),
+ createExtras("resp_abc123", "tool_456")
+);
+channel.publish(progressMsg1);
+
+// Continue publishing progress as work progresses
+JsonObject progress2 = new JsonObject();
+progress2.addProperty("name", "process_document");
+progress2.addProperty("status", "Processing page 75 of 100");
+progress2.addProperty("percentComplete", 75);
+
+Message progressMsg2 = new Message(
+ "tool_progress",
+ progress2.toString(),
+ createExtras("resp_abc123", "tool_456")
+);
+channel.publish(progressMsg2);
+
+// Publish final result
+JsonObject resultData = new JsonObject();
+resultData.addProperty("name", "process_document");
+JsonObject result = new JsonObject();
+result.addProperty("processedPages", 100);
+result.addProperty("summary", "Document processed successfully");
+resultData.add("result", result);
+
+Message resultMsg = new Message(
+ "tool_result",
+ resultData.toString(),
+ createExtras("resp_abc123", "tool_456")
+);
+channel.publish(resultMsg);
+```
+
+
+Subscribe to progress updates on the client by listening for the `tool_progress` message type:
+
+
+```javascript
+const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}');
+
+// Track tool execution progress
+const toolProgress = new Map();
+
+await channel.subscribe((message) => {
+ const { responseId, toolCallId } = message.extras?.headers || {};
+
+ switch (message.name) {
+ case 'tool_call':
+ toolProgress.set(toolCallId, {
+ name: message.data.name,
+ status: 'Starting...',
+ percentComplete: 0
+ });
+ renderProgressBar(toolCallId, 0);
+ break;
+
+ case 'tool_progress':
+ const progress = toolProgress.get(toolCallId);
+ if (progress) {
+ progress.status = message.data.status;
+ progress.percentComplete = message.data.percentComplete;
+ renderProgressBar(toolCallId, message.data.percentComplete);
+ }
+ break;
+
+ case 'tool_result':
+ toolProgress.delete(toolCallId);
+ renderCompleted(toolCallId, message.data.result);
+ break;
+ }
+});
+```
+```python
+channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}')
+
+# Track tool execution progress
+tool_progress = {}
+
+async def handle_message(message):
+ headers = message.extras.get('headers', {}) if message.extras else {}
+ response_id = headers.get('responseId')
+ tool_call_id = headers.get('toolCallId')
+
+ if message.name == 'tool_call':
+ tool_progress[tool_call_id] = {
+ 'name': message.data.get('name'),
+ 'status': 'Starting...',
+ 'percentComplete': 0
+ }
+ render_progress_bar(tool_call_id, 0)
+
+ elif message.name == 'tool_progress':
+ progress = tool_progress.get(tool_call_id)
+ if progress:
+ progress['status'] = message.data.get('status')
+ progress['percentComplete'] = message.data.get('percentComplete')
+ render_progress_bar(tool_call_id, message.data.get('percentComplete'))
+
+ elif message.name == 'tool_result':
+ if tool_call_id in tool_progress:
+ del tool_progress[tool_call_id]
+ render_completed(tool_call_id, message.data.get('result'))
+
+# Subscribe to all messages on the channel
+await channel.subscribe(handle_message)
+```
+```java
+Channel channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}");
+
+// Track tool execution progress
+Map toolProgress = new HashMap<>();
+
+// Subscribe to all messages on the channel
+channel.subscribe(message -> {
+ JsonObject headers = message.extras != null
+ ? message.extras.asJsonObject().getAsJsonObject("headers")
+ : null;
+
+ String responseId = headers != null && headers.has("responseId")
+ ? headers.get("responseId").getAsString()
+ : null;
+ String toolCallId = headers != null && headers.has("toolCallId")
+ ? headers.get("toolCallId").getAsString()
+ : null;
+
+ switch (message.name) {
+ case "tool_call":
+ JsonObject newProgress = new JsonObject();
+ newProgress.addProperty("name", ((JsonObject) message.data).get("name").getAsString());
+ newProgress.addProperty("status", "Starting...");
+ newProgress.addProperty("percentComplete", 0);
+ toolProgress.put(toolCallId, newProgress);
+ renderProgressBar(toolCallId, 0);
+ break;
+
+ case "tool_progress":
+ JsonObject progress = toolProgress.get(toolCallId);
+ if (progress != null) {
+ JsonObject progressData = (JsonObject) message.data;
+ progress.addProperty("status", progressData.get("status").getAsString());
+ progress.addProperty("percentComplete", progressData.get("percentComplete").getAsInt());
+ renderProgressBar(toolCallId, progressData.get("percentComplete").getAsInt());
+ }
+ break;
+
+ case "tool_result":
+ toolProgress.remove(toolCallId);
+ renderCompleted(toolCallId, ((JsonObject) message.data).get("result"));
+ break;
+ }
+});
+```
+
+
+Message-based progress is useful for:
+
+- Step-by-step status descriptions
+- Milestone notifications
+- Workflow stages with distinct phases
+- Audit trails requiring discrete event records
+
+### Progress updates via LiveObjects
+
+Use [LiveObjects](/docs/liveobjects) for state-based progress tracking. LiveObjects provides a shared data layer where progress state is automatically synchronized across all subscribed clients, making it ideal for continuous progress tracking.
+
+Use [LiveCounter](/docs/liveobjects/counter) for numeric progress values like completion percentages or item counts. Use [LiveMap](/docs/liveobjects/map) to track complex progress state with multiple fields.
+
+First, import and initialize the LiveObjects plugin:
+
+
+```javascript
+import * as Ably from 'ably';
+import { LiveObjects, LiveMap, LiveCounter } from 'ably/liveobjects';
+
+// Initialize client with LiveObjects plugin
+const realtime = new Ably.Realtime({
+ key: '{{API_KEY}}',
+ plugins: { LiveObjects }
+});
+
+// Get channel with LiveObjects capabilities
+const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}', {
+ modes: ['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH']
+});
+
+// Get the channel's LiveObjects root
+const root = await channel.object.get();
+```
+
+
+Create a LiveMap to track tool progress:
+
+
+```javascript
+// Create a LiveMap to track tool progress
+await root.set('tool_456_progress', LiveMap.create({
+ status: 'starting',
+ itemsProcessed: LiveCounter.create(0),
+ totalItems: 100,
+ currentItem: ''
+}));
+
+// Update progress as tool executes
+const progress = root.get('tool_456_progress');
+
+await progress.set('status', 'processing');
+await progress.set('currentItem', 'item_25');
+await progress.get('itemsProcessed').increment(25);
+
+// Continue updating as work progresses
+await progress.set('currentItem', 'item_75');
+await progress.get('itemsProcessed').increment(50);
+
+// Final increment to reach 100%
+await progress.set('currentItem', 'item_100');
+await progress.get('itemsProcessed').increment(25);
+
+// Mark complete
+await progress.set('status', 'completed');
+```
+
+
+Subscribe to LiveObjects updates on the client to render realtime progress:
+
+
+```javascript
+import * as Ably from 'ably';
+import { LiveObjects } from 'ably/liveobjects';
+
+// Initialize client with LiveObjects plugin
+const realtime = new Ably.Realtime({
+ key: '{{API_KEY}}',
+ plugins: { LiveObjects }
+});
+
+// Get channel with LiveObjects capabilities
+const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}', {
+ modes: ['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH']
+});
+
+// Get the channel's LiveObjects root
+const root = await channel.object.get();
+
+// Subscribe to progress updates
+const progress = root.get('tool_456_progress');
+progress.subscribe(() => {
+ const status = progress.get('status').value();
+ const itemsProcessed = progress.get('itemsProcessed').value();
+ const totalItems = progress.get('totalItems').value();
+ const percentComplete = Math.round((itemsProcessed / totalItems) * 100);
+
+ renderProgressBar('tool_456', percentComplete, status);
+});
+```
+
+
+
+
+LiveObjects-based progress is useful for:
+
+- Continuous progress bars with frequent updates
+- Distributed tool execution across multiple workers
+- Complex progress state with multiple fields
+- Scenarios where multiple agents or processes contribute to the same progress counter
+
+### Choosing the right approach
+
+Choose messages when:
+
+- Progress updates are infrequent (every few seconds or at specific milestones)
+- You need a complete audit trail of all progress events
+- Progress information is descriptive text rather than numeric
+- Each update represents a distinct event or stage transition
+
+Choose LiveObjects when:
+
+- Progress updates are frequent (multiple times per second)
+- You're tracking numeric progress like percentages or counts
+- Multiple processes or workers contribute to the same progress counter
+- You want to minimize message overhead for high-frequency updates
+
+You can combine both approaches for comprehensive progress tracking. Use LiveObjects for high-frequency numeric progress and messages for important milestone notifications:
+
+
+```javascript
+// Update numeric progress continuously via LiveObjects
+await progress.get('itemsProcessed').increment(1);
+
+// Publish milestone messages at key points
+if (itemsProcessed === totalItems / 2) {
+ await channel.publish({
+ name: 'tool_progress',
+ data: {
+ name: 'process_document',
+ status: 'Halfway complete - 50 of 100 items processed'
+ },
+ extras: {
+ headers: {
+ responseId: 'resp_abc123',
+ toolCallId: 'tool_456'
+ }
+ }
+ });
+}
+```
+
+
## Human-in-the-loop workflows
Tool calls resolved by humans are one approach to implementing human-in-the-loop workflows. When an agent encounters a tool call that needs human resolution, it publishes the tool call to the channel and waits for the human to publish the result back over the channel.