docs/edge/en/guides/frontend/shared-state.mdx
Shared state is a single state object that the agent and the UI both read and write. The agent updates it as it works and your React components render it live. When the user edits that same state in the UI, the change flows back so the agent sees it on its next turn.
The classic example is a recipe: the agent drafts it, the user tweaks an ingredient or an instruction, and the agent picks up from the edited version. Neither side owns the state; they share it.
<Note> Shared state relies on a Flow with custom state. Define an `AgentState` that subclasses `CopilotKitState` and type your Flow as `Flow[AgentState]`. Crews do not carry custom state, so this pattern is Flow-only. </Note>Subclass CopilotKitState so the agent keeps CopilotKit's message plumbing, then add your own fields. Here the shared field is recipe.
# recipe_flow.py
import json
from typing import List, Optional
from pydantic import BaseModel, Field
from crewai.flow.flow import Flow, start, router, listen
from litellm import acompletion
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
class Ingredient(BaseModel):
name: str
amount: str
class Recipe(BaseModel):
title: str
ingredients: List[Ingredient] = Field(default_factory=list)
instructions: List[str] = Field(default_factory=list)
class AgentState(CopilotKitState):
recipe: Optional[Recipe] = None
The agent reads the current state by dumping it into the system prompt, and writes it back by assigning to self.state.recipe. A generate_recipe tool lets the model return the updated recipe as structured arguments.
GENERATE_RECIPE_TOOL = {
"type": "function",
"function": {
"name": "generate_recipe",
"description": "Generate or modify the recipe.",
"parameters": {
"type": "object",
"properties": {"recipe": {"type": "object"}},
"required": ["recipe"],
},
},
}
class SharedStateFlow(Flow[AgentState]):
@start()
@listen("route_follow_up")
async def start_flow(self):
pass
@router(start_flow)
async def chat(self):
# The current shared state is visible to the model.
system_prompt = f"""You help the user build a recipe.
Current recipe: {self.state.model_dump_json(indent=2)}
Modify it by calling generate_recipe."""
response = await copilotkit_stream(
await acompletion(
model="openai/gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
*self.state.messages,
],
tools=[*self.state.copilotkit.actions, GENERATE_RECIPE_TOOL],
parallel_tool_calls=False,
stream=True,
)
)
message = response.choices[0].message
self.state.messages.append(message)
if message.tool_calls:
call = message.tool_calls[0]
if call.function.name == "generate_recipe":
args = json.loads(call.function.arguments)
self.state.recipe = Recipe(**args["recipe"]) # write to shared state
self.state.messages.append({
"role": "tool",
"content": "Recipe updated.",
"tool_call_id": call.id,
})
return "route_follow_up"
return "route_end"
@listen("route_end")
async def end(self):
pass
Two things make this shared rather than one-way: dumping self.state into the prompt means the agent always works from the latest recipe (including edits the user made in the UI), and assigning self.state.recipe puts the new value into the state snapshot sent to connected clients at the end of the step. For updates during a long step, emit explicitly with copilotkit_emit_state (see Agentic Generative UI).
Expose the Flow from your FastAPI app with add_crewai_flow_fastapi_endpoint, then register it in the CopilotKit runtime. See the Frontend Overview for the full server and runtime setup.
# server.py
from fastapi import FastAPI
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
from recipe_flow import SharedStateFlow
app = FastAPI(title="CrewAI Agent Server")
add_crewai_flow_fastapi_endpoint(
app=app,
flow=SharedStateFlow(),
path="/shared_state",
)
useAgent gives you both directions in one hook. Read the shared state off agent.state, and write it back with agent.setState(...). Subscribe to OnStateChanged so your component re-renders whenever the agent updates the state.
"use client";
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";
function RecipeEditor() {
const { agent } = useAgent({
agentId: "shared_state",
updates: [UseAgentUpdate.OnStateChanged],
});
const state = agent?.state as { recipe?: Recipe } | undefined;
const isLoading = agent?.isRunning;
const recipe = state?.recipe;
// setState replaces the whole state object, so spread the current
// state and override only the field you changed. Passing just
// `{ recipe }` would drop messages and other runtime fields.
const updateRecipe = (patch: Partial<Recipe>) =>
agent?.setState({ ...(agent.state ?? {}), recipe: { ...(recipe ?? {}), ...patch } });
return (
<div>
<input
value={recipe?.title ?? ""}
disabled={isLoading}
onChange={(e) => updateRecipe({ title: e.target.value })}
/>
</div>
);
}
agent.state reads the shared state, agent.setState(...) writes it back so the agent sees the change on its next turn, and agent.isRunning reflects whether the agent is currently working.
Putting the pieces together, a single recipe object is kept in sync in both directions:
self.state.recipe, the new value ships in the step's state snapshot, and OnStateChanged re-renders your inputs.agent.setState(...), and because the Flow dumps self.state into its prompt, the agent works from the edited recipe on its next turn.