Sign in

Agentic editing

AI agents can modify the same Sync document as the humans in your application using server-side APIs. Connected users receive changes in realtime, meaning agents can visibly work alongside humans, and take advantage of the same conflict resolution that enables human collaboration.

Edit Sync documents

Agents can read and modify your realtime Sync documents concurrently with your users. Generate changes with AI, then apply them with Liveblocks.mutateStorage—edits appear instantly for connected users.

import { LiveObject } from "@liveblocks/client";import { Liveblocks } from "@liveblocks/node";import { generateText, Output } from "ai";import { z } from "zod";
const liveblocks = new Liveblocks({ secret: "",});
await liveblocks.mutateStorage("my-room-id", async ({ root }) => { const shapes = root.get("shapes");
const { output: shape } = await generateText({ model: "openai/gpt-5.6-sol", output: Output.object({ schema: z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number(), color: z.string(), }), }), prompt: `Create a rectangle for the canvas. Here are current shapes: ${shapes.toJSON()}`, });
shapes.set("shape-1", new LiveObject(shape));});

Other APIs can be used to enable server-side editing, such as the JSON Patch endpoint. AI models are already familiar with JSON Patch, allowing them to easily generate modifications from natural language instructions.

PATCH /v2/rooms/my-room/storage/json-patchContent-Type: application/json
[ { "op": "add", "path": "/shapes/-", "value": { "x": 50, "y": 100, "width": 20, "height": 60, "color": "red" } }]

Learn more in our JSON Patch guide.

Saving versions

It’s recommended to use version history to save versions of the Sync document before a modification is applied. This way, the user can revert their document to the previous state, in case they’d like to revert modifications made by the agent. You can manually create a version snapshot using Liveblocks.createVersionHistorySnapshot.

import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: "",});
await liveblocks.createVersionHistorySnapshot("my-room-id");
// Apply modification// ...

In React, you can display a list of versions with useHistoryVersions, show snapshot previews with useHistoryVersionStorageData, and create revert buttons with useRestoreToStorageVersion.

import {  useHistoryVersions,  useHistoryVersionStorageData,  useRestoreToStorageVersion,} from "@liveblocks/react/suspense";import {  HistoryVersionSummaryList,  HistoryVersionSummary,} from "@liveblocks/react-ui";import { useState } from "react";
function VersionHistory() { const { versions, error, isLoading } = useHistoryVersions(); const [selectedVersionId, setSelectedVersionId] = useState(null);
return ( <HistoryVersionSummaryList> {versions?.map((version) => ( <div> <ClientSideSuspense fallback={<div>Loading...</div>}> <VersionHistoryPreview versionId={version.id} /> </ClientSideSuspense> <HistoryVersionSummary onClick={() => { setSelectedVersionId(version.id); }} key={version.id} version={version} selected={version.id === selectedVersionId} /> </div> ))} </HistoryVersionSummaryList> );}
function VersionHistoryPreview({ versionId }: { versionId: string }) { const { data, error, isLoading } = useHistoryVersionStorageData(versionId); const restoreToStorageVersion = useRestoreToStorageVersion(versionId);
return ( <div> <code>{JSON.stringify(data, null, 2)}</code> <button onClick={async () => { await restoreToStorageVersion(); }} > ↩️ Restore </button> </div> );}

Note that HistoryVersionSummary and HistoryVersionSummaryList are optional ready-made components that display version history information with styled components.

Show agent presence

Show what agents are working on inside your app by giving them live presence updates, such as selections, typing indicators, and online avatars. Liveblocks.setPresence allows you to set your agent’s presence. In this example, selectedShapeId represents a shape that the agent is focused on, and editing.

import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: "",});
await liveblocks.setPresence("my-room-id", { userId: "ai-agent", userInfo: { name: "AI agent", color: "#7c3aed" }, data: { selectedShapeId: "shape-1" }, ttl: 60,});

After setting presence, you can read it in your app with useOthers like any human, so existing avatar stacks, cursors, and focus indicators show AI activity with no extra UI.

import { useOthers } from "@liveblocks/react/suspense";
function AgentPresence() { const others = useOthers(); const agent = others.find((other) => other.id === "ai-agent");
return <div>{agent?.presence.data.selectedShapeId}</div>;}

Agent presence appears for a specific amount of seconds, the ttl value, before it disappears. To make it disappear, set ttl to the minimum value of 2 seconds.

await liveblocks.setPresence("my-room-id", {  userId: "ai-agent",  userInfo: { name: "AI agent", color: "#7c3aed" },  data: { selectedShapeId: "shape-1" },  ttl: 2,});

Learn more under the presence use case.

Display agent status

Presence shows that an agent is in the room right now, but it disappears when the process ends. You can use feeds to stream the agent’s working state (e.g. thinking, writing, complete), and save it permanently in a history. Create a status message with Liveblocks.createFeedMessage when work starts, then update it with Liveblocks.updateFeedMessage as the agent moves through each stage.

import { LiveObject } from "@liveblocks/client";import { Liveblocks } from "@liveblocks/node";import { generateText, Output } from "ai";import { z } from "zod";
const liveblocks = new Liveblocks({ secret: "",});
await liveblocks.createFeedMessage({ roomId: "my-room-id", feedId: "agent-status", id: "current", data: { status: "searching", label: "Searching documents…" },});
await liveblocks.updateFeedMessage({ roomId: "my-room-id", feedId: "agent-status", messageId: "current", data: { status: "writing", label: "Updating the launch plan…" }, updatedAt: Date.now(),});

In React, read the latest status with useFeedMessages and render it in your UI.

import { useFeedMessages } from "@liveblocks/react/suspense";
function AgentStatus() { const { messages } = useFeedMessages("agent-status"); const status = messages[messages.length - 1];
if (!status) { return null; }
return <p>{status.data.label}</p>;}

Feeds can also be used to build complete AI chat interfaces, from which agents can issue modifications. Learn more under the chat use case.

Leave AI comments

Agents can review content and leave contextual feedback with Comments. Generate feedback with AI, convert it with markdownToCommentBody, and post it with Liveblocks.createThread under the agent’s own user ID.

import { Liveblocks, markdownToCommentBody } from "@liveblocks/node";import { generateText } from "ai";
const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!,});
const { text } = await generateText({ model: "openai/gpt-5.6-sol", prompt: `Review this paragraph and suggest one improvement: ${paragraph}`,});
await liveblocks.createThread({ roomId: "my-room-id", data: { comment: { userId: "ai-agent", body: markdownToCommentBody(text), }, metadata: { paragraphId: "paragraph-4" }, },});

In React, read the comments with useThreads and render them in your UI with Thread.

import { useThreads } from "@liveblocks/react/suspense";import { Thread } from "@liveblocks/react-ui";
function Comments() { const { threads } = useThreads();
return ( <div> {threads.map((thread) => ( <Thread key={thread.id} thread={thread} /> ))} </div> );}

You can also automatically trigger an agent when a user mentions it in a thread using the commentCreated webhook. Learn more under the comments use case.

Putting it all together

Combining each of the features above, you can create a complete AI agentic editing experience that will modify a document, save a snapshot, display what it’s editing, stream live status, and save a history of the run.

server.ts
"use server";
import { LiveObject } from "@liveblocks/client";import { Liveblocks } from "@liveblocks/node";import { streamText, Output } from "ai";import { z } from "zod";
const liveblocks = new Liveblocks({ secret: "",});
export async function generateShape(roomId: string, feedId: string) { const newShapeId = "shape-" + crypto.randomUUID();
await Promise.all([ // Create a feed for this run liveblocks.createFeed({ roomId, feedId, });
// Display the agent's presence liveblocks.setPresence(roomId, { userId: "ai-agent", userInfo: { name: "AI agent", color: "#7c3aed" }, data: { selectedShapeId: newShapeId }, ttl: 60, });
// Save a version of the document liveblocks.createVersionHistorySnapshot(roomId); ]);
await liveblocks.mutateStorage(roomId, async ({ root }) => { const shapes = root.get("shapes");
// Stream in an AI response const { partialOutputStream, output } = streamText({ model: "openai/gpt-5.6-sol", output: Output.object({ schema: z.object({ x: z.number(), y: z.number(), width: z.number(), height: z.number(), color: z.string(), }), }), prompt: `Create a shape for the canvas. Here are current shapes: ${shapes.toJSON()}`, });
// Push each stream chunk into the feed for await (const partialShape of partialOutputStream) { await liveblocks.updateFeedMessage({ roomId, feedId, messageId: "current", data: { status: "editing", shape: partialShape }, updatedAt: Date.now(), }); }
// Add the new shape to Sync data const shape = await output; shapes.set(newShapeId, new LiveObject(shape));
// Send completion status to the feed await liveblocks.updateFeedMessage({ roomId, feedId, messageId: "current", data: { status: "complete", shape }, updatedAt: Date.now(), });
// Hide the agent's presence liveblocks.setPresence(roomId, { userId: "ai-agent", userInfo: { name: "AI agent", color: "#7c3aed" }, data: { selectedShapeId: newShapeId }, ttl: 2, }); });}

In React, you can show all this live in the UI. A list of shapes is rendered, each shape with its own properties, with an outline displayed if the AI is editing the shape. A button to generate a new shape is shown, and when clicked, the agentic editing process is triggered, and the UI displays streamed updates instead of the button.

app.tsx
import {  useFeedMessages,  useStorage,  useOthers,  useRoom,} from "@liveblocks/react/suspense";import { useCallback } from "react";import { generateShape } from "./server";
function Canvas() { // Get a list of all shapes const shapes = useStorage((root) => root.shapes);
// Find the connected agent's presence const others = useOthers(); const agent = others.find((other) => other.id === "ai-agent");
return ( <div style={{ position: "relative", width: "100%", height: "100%" }}> <CreateShapeButton /> {shapes.map((shape) => ( <div key={shape.id} style={{ position: "absolute",
// Render the shape's parameters left: shape.x, top: shape.y, width: shape.width, height: shape.height, backgroundColor: shape.color,
// Show an outline if the AI is editing this shape outline: agent?.presence?.selectedShapeId === shape.id ? "2px solid red" : "none", }} /> ))} </div> );}
function CreateShapeButton() const roomId = useRoom().id; const [feedId, setFeedId] = useState("feed-" + crypto.randomUUID()); const { messages } = useFeedMessages(feedId);
const handleCreateShape = useCallback(async () => { // Run agentic editing await generateShape(roomId, feedId);
// Completed, set a fresh feedId setFeedId("feed-" + crypto.randomUUID()); }, [roomId]);
if (messages.length === 0) { return ( <button onClick={handleCreateShape}> ➕ Create Shape </button> ); }
// Get the last message (we only use one here) const lastMessage = messages[messages.length - 1];
// Leave a status update, e.g. "editing: { x: 100, y: 100, ... }" return ( <div> <div>{lastMessage.data.status}</div> <code>{JSON.stringify(lastMessage.data.shape, null, 2)}</code> </div> )}