---
description: Connect to Cloudflare Agents from browsers or server runtimes using useAgent, AgentClient, and agentFetch.
title: Client SDK
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt  
> Use this file to discover all available pages before exploring further.

# Client SDK

Last updated Aug 24, 2026|Copy as Markdown|[View as Markdown](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agents/communication-channels/chat/client-sdk/index.md)|[Agent setup](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agent-setup/)

Connect to agents from any JavaScript runtime — browsers, Node.js, Deno, Bun, or edge functions — using WebSockets or HTTP. The SDK provides real-time state synchronization, RPC method calls, and streaming responses.

## Overview

The client SDK offers two ways to connect with a WebSocket connection, and one way to make HTTP requests.

| Client      | Use Case                                                    |
| ----------- | ----------------------------------------------------------- |
| useAgent    | React hook with automatic reconnection and state management |
| AgentClient | Vanilla JavaScript/TypeScript class for any environment     |
| agentFetch  | HTTP requests when WebSocket is not needed                  |

All clients provide:

* **Bidirectional state sync** \- Push and receive state updates in real-time
* **RPC calls** \- Call agent methods with typed arguments and return values
* **Streaming** \- Handle chunked responses for AI completions
* **Auto-reconnection** \- Automatic reconnection with exponential backoff

## Quick start

### React

```js
import { useAgent } from "agents/react";

function Chat() {
	const agent = useAgent({
		agent: "ChatAgent",
		name: "room-123",
		onStateUpdate: (state) => {
			console.log("New state:", state);
		},
	});

	const sendMessage = async () => {
		const response = await agent.call("sendMessage", ["Hello!"]);
		console.log("Response:", response);
	};

	return <button onClick={sendMessage}>Send</button>;
}
```

```ts
import { useAgent } from "agents/react";

function Chat() {
	const agent = useAgent({
		agent: "ChatAgent",
		name: "room-123",
		onStateUpdate: (state) => {
			console.log("New state:", state);
		},
	});

	const sendMessage = async () => {
		const response = await agent.call("sendMessage", ["Hello!"]);
		console.log("Response:", response);
	};

	return <button onClick={sendMessage}>Send</button>;
}
```

### Vanilla JavaScript

```js
import { AgentClient } from "agents/client";

const client = new AgentClient({
	agent: "ChatAgent",
	name: "room-123",
	host: "your-worker.your-subdomain.workers.dev",
	onStateUpdate: (state) => {
		console.log("New state:", state);
	},
});

// Call a method
const response = await client.call("sendMessage", ["Hello!"]);
```

```ts
import { AgentClient } from "agents/client";

const client = new AgentClient({
	agent: "ChatAgent",
	name: "room-123",
	host: "your-worker.your-subdomain.workers.dev",
	onStateUpdate: (state) => {
		console.log("New state:", state);
	},
});

// Call a method
const response = await client.call("sendMessage", ["Hello!"]);
```

## Connecting to agents

### Agent naming

The `agent` parameter is your agent class name. It is automatically converted from camelCase to kebab-case for the URL:

```js
// These are equivalent:
useAgent({ agent: "ChatAgent" }); // → /agents/chat-agent/...
useAgent({ agent: "MyCustomAgent" }); // → /agents/my-custom-agent/...
useAgent({ agent: "LOUD_AGENT" }); // → /agents/loud-agent/...
```

```ts
// These are equivalent:
useAgent({ agent: "ChatAgent" }); // → /agents/chat-agent/...
useAgent({ agent: "MyCustomAgent" }); // → /agents/my-custom-agent/...
useAgent({ agent: "LOUD_AGENT" }); // → /agents/loud-agent/...
```

### Instance names

The `name` parameter identifies a specific agent instance. If omitted, defaults to `"default"`:

```js
// Connect to a specific chat room
useAgent({ agent: "ChatAgent", name: "room-123" });

// Connect to a user's personal agent
useAgent({ agent: "UserAgent", name: userId });

// Uses "default" instance
useAgent({ agent: "ChatAgent" });
```

```ts
// Connect to a specific chat room
useAgent({ agent: "ChatAgent", name: "room-123" });

// Connect to a user's personal agent
useAgent({ agent: "UserAgent", name: userId });

// Uses "default" instance
useAgent({ agent: "ChatAgent" });
```

### Connection options

Both `useAgent` and `AgentClient` accept connection options:

```js
useAgent({
	agent: "ChatAgent",
	name: "room-123",

	// Connection settings
	host: "my-worker.workers.dev", // Custom host (defaults to current origin)
	path: "/custom/path", // Custom path prefix

	// Query parameters (sent on connection)
	query: {
		token: "abc123",
		version: "2",
	},

	// Event handlers
	onOpen: () => console.log("Connected"),
	onClose: () => console.log("Disconnected"),
	onError: (error) => console.error("Error:", error),
});
```

```ts
useAgent({
	agent: "ChatAgent",
	name: "room-123",

	// Connection settings
	host: "my-worker.workers.dev", // Custom host (defaults to current origin)
	path: "/custom/path", // Custom path prefix

	// Query parameters (sent on connection)
	query: {
		token: "abc123",
		version: "2",
	},

	// Event handlers
	onOpen: () => console.log("Connected"),
	onClose: () => console.log("Disconnected"),
	onError: (error) => console.error("Error:", error),
});
```

### Async query parameters

For authentication tokens or other async data, pass a function that returns a Promise:

```js
useAgent({
	agent: "ChatAgent",
	name: "room-123",

	// Async query - called before connecting
	query: async () => {
		const token = await getAuthToken();
		return { token };
	},

	// Dependencies that trigger re-fetching the query
	queryDeps: [userId],

	// Cache TTL for the query result (default: 5 minutes)
	cacheTtl: 60 * 1000, // 1 minute
});
```

```ts
useAgent({
	agent: "ChatAgent",
	name: "room-123",

	// Async query - called before connecting
	query: async () => {
		const token = await getAuthToken();
		return { token };
	},

	// Dependencies that trigger re-fetching the query
	queryDeps: [userId],

	// Cache TTL for the query result (default: 5 minutes)
	cacheTtl: 60 * 1000, // 1 minute
});
```

The query function is cached and only re-called when:

* `queryDeps` change
* `cacheTtl` expires
* The WebSocket connection closes (automatic cache invalidation)
* The component remounts

Automatic cache invalidation on disconnect

When the WebSocket connection closes — whether due to network issues, server restarts, or explicit disconnection — the async query cache is automatically invalidated. This ensures that when the client reconnects, the query function is re-executed to fetch fresh data. This is particularly important for authentication tokens that may have expired during the disconnection period.

## State synchronization

Agents can maintain state that syncs bidirectionally with all connected clients.

### Reading current state

Both `useAgent` and `AgentClient` expose a `state` property that reflects the current agent state. It starts as `undefined` until the first state message is received from the server.

```js
const agent = useAgent({ agent: "GameAgent", name: "game-123" });

// Read the current state at any time
console.log("Current score:", agent.state?.score);
```

```ts
const agent = useAgent({ agent: "GameAgent", name: "game-123" });

// Read the current state at any time
console.log("Current score:", agent.state?.score);
```

With `useAgent`, state updates trigger a React re-render, so `agent.state` always reflects the latest value in your JSX. With `AgentClient`, the `state` field is updated synchronously on each incoming server broadcast or `setState` call.

### Receiving state updates

```js
const agent = useAgent({
	agent: "GameAgent",
	name: "game-123",
	onStateUpdate: (state, source) => {
		// state: The new state from the agent
		// source: "server" (agent pushed) or "client" (you pushed)
		console.log(`State updated from ${source}:`, state);
		setGameState(state);
	},
});
```

```ts
const agent = useAgent({
	agent: "GameAgent",
	name: "game-123",
	onStateUpdate: (state, source) => {
		// state: The new state from the agent
		// source: "server" (agent pushed) or "client" (you pushed)
		console.log(`State updated from ${source}:`, state);
		setGameState(state);
	},
});
```

### Pushing state updates

```js
// Update the agent's state from the client
agent.setState({ score: 100, level: 5 });
```

```ts
// Update the agent's state from the client
agent.setState({ score: 100, level: 5 });
```

When you call `setState()`:

1. The state is sent to the agent over WebSocket
2. The agent's `onStateChanged()` method is called
3. The agent broadcasts the new state to all connected clients
4. Your `onStateUpdate` callback fires with `source: "client"`

### State flow

sequenceDiagram
    participant Client
    participant Agent
    Client->>Agent: setState()
    Agent-->>Client: onStateUpdate (broadcast)

## Calling agent methods (RPC)

Call methods on your agent that are decorated with `@callable()`.

Note

The `@callable()` decorator is only required for methods called from external runtimes (browsers, other services). When calling from within the same Worker, you can use standard [Durable Object RPC](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoke-rpc-methods) directly on the stub without the decorator.

### Using call()

```js
// Basic call
const result = await agent.call("getUser", [userId]);

// Call with multiple arguments
const result = await agent.call("createPost", [title, content, tags]);

// Call with no arguments
const result = await agent.call("getStats");
```

```ts
// Basic call
const result = await agent.call("getUser", [userId]);

// Call with multiple arguments
const result = await agent.call("createPost", [title, content, tags]);

// Call with no arguments
const result = await agent.call("getStats");
```

### Using the stub proxy

The `stub` property provides a cleaner syntax for method calls:

```js
// Instead of:
const user = await agent.call("getUser", ["user-123"]);

// You can write:
const user = await agent.stub.getUser("user-123");

// Multiple arguments work naturally:
const post = await agent.stub.createPost(title, content, tags);
```

```ts
// Instead of:
const user = await agent.call("getUser", ["user-123"]);

// You can write:
const user = await agent.stub.getUser("user-123");

// Multiple arguments work naturally:
const post = await agent.stub.createPost(title, content, tags);
```

### TypeScript integration

For full type safety, pass your Agent class as a type parameter:

```js
const agent = useAgent({
	agent: "MyAgent",
	name: "instance-1",
});

// Now stub methods are fully typed
const result = await agent.stub.processData({ input: "test" });
```

```ts
import type { MyAgent } from "./agents/my-agent";

const agent = useAgent<MyAgent>({
	agent: "MyAgent",
	name: "instance-1",
});

// Now stub methods are fully typed
const result = await agent.stub.processData({ input: "test" });
```

### Streaming responses

Mark a callable method as streaming. The framework passes a `StreamingResponse` as its first argument:

```js
// Agent-side
import { Agent, callable } from "agents";

class MyAgent extends Agent {
	@callable({ streaming: true })
	async generateText(stream, prompt) {
		for await (const chunk of llm.stream(prompt)) {
			stream.send(chunk);
		}
		stream.end();
	}
}

// Client-side
await agent.call("generateText", [prompt], {
	onChunk: (chunk) => {
		// Called for each chunk
		appendToOutput(chunk);
	},
	onDone: (finalResult) => {
		// Called when stream completes
		console.log("Complete:", finalResult);
	},
	onError: (error) => {
		// Called if streaming fails
		console.error("Stream error:", error);
	},
});
```

```ts
// Agent-side
import { Agent, callable, type StreamingResponse } from "agents";

class MyAgent extends Agent {
	@callable({ streaming: true })
	async generateText(stream: StreamingResponse, prompt: string) {
		for await (const chunk of llm.stream(prompt)) {
			stream.send(chunk);
		}
		stream.end();
	}
}

// Client-side
await agent.call("generateText", [prompt], {
	onChunk: (chunk) => {
		// Called for each chunk
		appendToOutput(chunk);
	},
	onDone: (finalResult) => {
		// Called when stream completes
		console.log("Complete:", finalResult);
	},
	onError: (error) => {
		// Called if streaming fails
		console.error("Stream error:", error);
	},
});
```

## HTTP requests with agentFetch

For one-off requests without maintaining a WebSocket connection:

```js
import { agentFetch } from "agents/client";

// GET request
const response = await agentFetch({
	agent: "DataAgent",
	name: "instance-1",
	host: "my-worker.workers.dev",
});

const data = await response.json();

// POST request with body
const response = await agentFetch(
	{
		agent: "DataAgent",
		name: "instance-1",
		host: "my-worker.workers.dev",
	},
	{
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ action: "process" }),
	},
);
```

```ts
import { agentFetch } from "agents/client";

// GET request
const response = await agentFetch({
	agent: "DataAgent",
	name: "instance-1",
	host: "my-worker.workers.dev",
});

const data = await response.json();

// POST request with body
const response = await agentFetch(
	{
		agent: "DataAgent",
		name: "instance-1",
		host: "my-worker.workers.dev",
	},
	{
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ action: "process" }),
	},
);
```

**When to use `agentFetch` vs WebSocket:**

| Use agentFetch                  | Use useAgent/AgentClient    |
| ------------------------------- | --------------------------- |
| One-time requests               | Real-time updates needed    |
| Server-to-server calls          | Bidirectional communication |
| Simple REST-style API           | State synchronization       |
| No persistent connection needed | Multiple RPC calls          |

## MCP server integration

If your agent uses MCP (Model Context Protocol) servers, you can receive updates about their state:

```js
const agent = useAgent({
	agent: "AssistantAgent",
	name: "session-123",
	onMcpUpdate: (mcpServers) => {
		// mcpServers is a record of server states
		for (const [serverId, server] of Object.entries(mcpServers)) {
			console.log(`${serverId}: ${server.connectionState}`);
			console.log(`Tools: ${server.tools?.map((t) => t.name).join(", ")}`);
		}
	},
});
```

```ts
const agent = useAgent({
	agent: "AssistantAgent",
	name: "session-123",
	onMcpUpdate: (mcpServers) => {
		// mcpServers is a record of server states
		for (const [serverId, server] of Object.entries(mcpServers)) {
			console.log(`${serverId}: ${server.connectionState}`);
			console.log(`Tools: ${server.tools?.map((t) => t.name).join(", ")}`);
		}
	},
});
```

## Error handling

### Connection errors

```js
const agent = useAgent({
	agent: "MyAgent",
	onError: (error) => {
		console.error("WebSocket error:", error);
	},
	onClose: () => {
		console.log("Connection closed, will auto-reconnect...");
	},
});
```

```ts
const agent = useAgent({
	agent: "MyAgent",
	onError: (error) => {
		console.error("WebSocket error:", error);
	},
	onClose: () => {
		console.log("Connection closed, will auto-reconnect...");
	},
});
```

### RPC errors

```js
try {
	const result = await agent.call("riskyMethod", [data]);
} catch (error) {
	// Error thrown by the agent method
	console.error("RPC failed:", error.message);
}
```

```ts
try {
	const result = await agent.call("riskyMethod", [data]);
} catch (error) {
	// Error thrown by the agent method
	console.error("RPC failed:", error.message);
}
```

### Streaming errors

```js
await agent.call("streamingMethod", [data], {
	onChunk: (chunk) => handleChunk(chunk),
	onError: (errorMessage) => {
		// Stream-specific error handling
		console.error("Stream error:", errorMessage);
	},
});
```

```ts
await agent.call("streamingMethod", [data], {
	onChunk: (chunk) => handleChunk(chunk),
	onError: (errorMessage) => {
		// Stream-specific error handling
		console.error("Stream error:", errorMessage);
	},
});
```

## Best practices

### 1\. Use typed stubs

```js
// Prefer this:
const user = await agent.stub.getUser(id);

// Over this:
const user = await agent.call("getUser", [id]);
```

```ts
// Prefer this:
const user = await agent.stub.getUser(id);

// Over this:
const user = await agent.call("getUser", [id]);
```

### 2\. Reconnection is automatic

The client auto-reconnects and the agent automatically sends the current state on each connection. Your `onStateUpdate` callback will fire with the latest state — no manual re-sync is needed. If you use an async `query` function for authentication, the cache is automatically invalidated on disconnect, ensuring fresh tokens are fetched on reconnect.

### 3\. Optimize query caching

```js
// For auth tokens that expire hourly:
useAgent({
	query: async () => ({ token: await getToken() }),
	cacheTtl: 55 * 60 * 1000, // Refresh 5 min before expiry
	queryDeps: [userId], // Refresh if user changes
});
```

```ts
// For auth tokens that expire hourly:
useAgent({
	query: async () => ({ token: await getToken() }),
	cacheTtl: 55 * 60 * 1000, // Refresh 5 min before expiry
	queryDeps: [userId], // Refresh if user changes
});
```

### 4\. Clean up connections

In vanilla JS, close connections when done:

```js
const client = new AgentClient({ agent: "MyAgent", host: "..." });

// When done:
client.close();
```

```ts
const client = new AgentClient({ agent: "MyAgent", host: "..." });

// When done:
client.close();
```

React's `useAgent` handles cleanup automatically on unmount.

## React hook reference

### UseAgentOptions

```ts
type UseAgentOptions<State> = {
	// Required
	agent: string; // Agent class name

	// Optional
	name?: string; // Instance name (default: "default")
	host?: string; // Custom host
	path?: string; // Custom path prefix

	// Query parameters
	query?: Record<string, string> | (() => Promise<Record<string, string>>);
	queryDeps?: unknown[]; // Dependencies for async query
	cacheTtl?: number; // Query cache TTL in ms (default: 5 min)

	// Callbacks
	onStateUpdate?: (state: State, source: "server" | "client") => void;
	onMcpUpdate?: (mcpServers: MCPServersState) => void;
	onOpen?: () => void;
	onClose?: () => void;
	onError?: (error: Event) => void;
	onMessage?: (message: MessageEvent) => void;
};
```

### Return value

The `useAgent` hook returns an object with the following properties and methods:

| Property/Method               | Type    | Description                |
| ----------------------------- | ------- | -------------------------- |
| agent                         | string  | Kebab-case agent name      |
| name                          | string  | Instance name              |
| setState(state)               | void    | Push state to agent        |
| call(method, args?, options?) | Promise | Call agent method          |
| stub                          | Proxy   | Typed method calls         |
| send(data)                    | void    | Send raw WebSocket message |
| close()                       | void    | Close connection           |
| reconnect()                   | void    | Force reconnection         |

## Vanilla JS reference

### AgentClientOptions

```ts
type AgentClientOptions<State> = {
	// Required
	agent: string; // Agent class name
	host: string; // Worker host

	// Optional
	name?: string; // Instance name (default: "default")
	path?: string; // Custom path prefix
	query?: Record<string, string>;

	// Callbacks
	onStateUpdate?: (state: State, source: "server" | "client") => void;
};
```

### AgentClient methods

| Property/Method               | Type    | Description                |
| ----------------------------- | ------- | -------------------------- |
| agent                         | string  | Kebab-case agent name      |
| name                          | string  | Instance name              |
| setState(state)               | void    | Push state to agent        |
| call(method, args?, options?) | Promise | Call agent method          |
| send(data)                    | void    | Send raw WebSocket message |
| close()                       | void    | Close connection           |
| reconnect()                   | void    | Force reconnection         |

The client also supports WebSocket event listeners:

```js
client.addEventListener("open", () => {});
client.addEventListener("close", () => {});
client.addEventListener("error", () => {});
client.addEventListener("message", () => {});
```

```ts
client.addEventListener("open", () => {});
client.addEventListener("close", () => {});
client.addEventListener("error", () => {});
client.addEventListener("message", () => {});
```

## Agent-tool events

If your chat UI renders retained child runs from [Agents as tools](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agents/runtime/execution/agent-tools/), use `useAgentToolEvents()` alongside `useAgent()` and `useAgentChat()`. The hook subscribes to the parent connection, replays retained child timelines, and groups runs by parent tool call ID.

```js
import { useAgent, useAgentToolEvents } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

const agent = useAgent({ agent: "Assistant", name: userId });
const { messages } = useAgentChat({ agent });
const agentTools = useAgentToolEvents({ agent });
```

```tsx
import { useAgent, useAgentToolEvents } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

const agent = useAgent({ agent: "Assistant", name: userId });
const { messages } = useAgentChat({ agent });
const agentTools = useAgentToolEvents({ agent });
```

## Next steps

### [Routing](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agents/runtime/communication/routing/)

URL patterns and custom routing options.

### [Callable methods](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agents/runtime/lifecycle/callable-methods/)

RPC over WebSocket for client-server method calls.

### [Cross-domain authentication](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agents/runtime/operations/cross-domain-authentication/)

Secure WebSocket connections across domains.

### [Build a chat agent](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/agents/examples/chat-agent/)

Complete client integration with AI chat.

Was this helpful?

YesNo

## On this page

[![](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://docs-durable-objects-instance-replaced-errors.previews.developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/communication-channels/chat/client-sdk/#page","headline":"Client SDK · Cloudflare Agents docs","description":"Connect to Cloudflare Agents from browsers or server runtimes using useAgent, AgentClient, and agentFetch.","url":"https://developers.cloudflare.com/agents/communication-channels/chat/client-sdk/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-24","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
