Agents SDK foundations
Sync state and call approved methods
Use setState for persisted synchronized state and @callable methods for validated client-to-agent RPC.
8 minute lesson
An agent instance has state, and clients need to see it change. The SDK gives you one mechanism for both problems.
Read typed agent state through this.state and update it with this.setState(). The SDK persists and broadcasts the new state to connected clients:
export class ProjectAgent extends Agent {
initialState = { name: 'untitled', tasks: [] }
addTask(title) {
this.setState({
...this.state,
tasks: [...this.state.tasks, { title, done: false }],
})
}
}
Every browser holding a WebSocket to this instance receives the update — the client hooks expose it as a reactive value, so the UI follows without polling.
One rule saves you from a subtle bug: important state belongs in Agent state or SQLite, not ordinary class fields lost on hibernation. Durable Objects hibernate when idle. A plain this.pendingItems = [] evaluates fresh on the next wake, and the data is gone. this.state survives because it is persisted storage, not memory.
Expose methods, not trust
Clients also need to trigger actions. Expose only methods decorated as callable, validate every argument, and authorize the connection for the instance:
@callable()
async renameProject(name) {
if (typeof name !== 'string' || name.length < 1 || name.length > 80) {
throw new Error('invalid name')
}
this.setState({ ...this.state, name })
}
Client hooks can use the typed stub over WebSocket, so calling renameProject from the browser feels like a local function call. That convenience is exactly why the validation matters: the arguments arrive from an untrusted client, no matter how typed the stub looks in your editor. TypeScript types are erased at runtime — a hostile client can send anything.
Methods without the decorator stay internal. That is your authorization boundary at the method level: the model or client can only invoke what you deliberately exported.
Now put both halves together. Add a callable renameProject method and reject invalid names and unauthorized users. Test it three ways: a valid rename updates every connected client, an empty name throws without touching state, and a connection for a different user’s instance never reaches the method. Watch the second browser window during the valid rename — seeing the state change arrive without a refresh is the moment the model clicks.
Lesson completed