---
title: Build Agents on Cloudflare
description: Most AI applications today are stateless — they process a request, return a response, and forget everything. Real agents need more. They need to remember conversations, act on schedules, call tools, coordinate with other agents, and stay connected to users in real-time. The Agents SDK gives you all of this as a TypeScript class.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ AI ](https://developers.cloudflare.com/search/?tags=AI) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build Agents on Cloudflare

Most AI applications today are stateless — they process a request, return a response, and forget everything. Real agents need more. They need to remember conversations, act on schedules, call tools, coordinate with other agents, and stay connected to users in real-time. The Agents SDK gives you all of this as a TypeScript class.

Each agent runs on a [Durable Object](https://developers.cloudflare.com/durable-objects/) — a stateful micro-server with its own SQL database, WebSocket connections, and scheduling. Deploy once and Cloudflare runs your agents across its global network, scaling to tens of millions of instances. No infrastructure to manage, no sessions to reconstruct, no state to externalize.

### Get started

Three commands to a running agent. No API keys required — the starter uses [Workers AI](https://developers.cloudflare.com/workers-ai/) by default.

Terminal window

```

npx create-cloudflare@latest --template cloudflare/agents-starter

cd agents-starter && npm install

npm run dev


```

The starter includes streaming AI chat, server-side and client-side tools, human-in-the-loop approval, and task scheduling — a foundation you can build on or tear apart. You can also swap in [OpenAI, Anthropic, Google Gemini, or any other provider](https://developers.cloudflare.com/agents/api-reference/using-ai-models/).

[ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) Step-by-step tutorial that walks through the starter and shows how to customize it. 

[ Add to an existing project ](https://developers.cloudflare.com/agents/getting-started/add-to-existing-project/) Install the agents package into a Workers project and wire up routing. 

### What agents can do

* **Remember everything** — Every agent has a built-in [SQL database](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) and key-value state that syncs to connected clients in real-time. State survives restarts, deploys, and hibernation.
* **Build AI chat** — [AIChatAgent](https://developers.cloudflare.com/agents/api-reference/chat-agents/) gives you streaming AI chat with automatic message persistence, resumable streams, and tool support. Pair it with the [useAgentChat](https://developers.cloudflare.com/agents/api-reference/chat-agents/) React hook to build chat UIs in minutes.
* **Think with any model** — Call [any AI model](https://developers.cloudflare.com/agents/api-reference/using-ai-models/) — Workers AI, OpenAI, Anthropic, Gemini — and stream responses over [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/) or [Server-Sent Events](https://developers.cloudflare.com/agents/api-reference/http-sse/). Long-running reasoning models that take minutes to respond work out of the box.
* **Use and serve tools** — Define server-side tools, client-side tools that run in the browser, and [human-in-the-loop](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) approval flows. Expose your agent's tools to other agents and LLMs via [MCP](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/).
* **Act on their own** — [Schedule tasks](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) on a delay, at a specific time, or on a cron. Agents can wake themselves up, do work, and go back to sleep — without a user present.
* **Browse the web** — Spin up [headless browsers](https://developers.cloudflare.com/agents/api-reference/browse-the-web/) to scrape, screenshot, and interact with web pages.
* **Orchestrate work** — Run multi-step [workflows](https://developers.cloudflare.com/agents/api-reference/run-workflows/) with automatic retries, or coordinate across multiple agents.
* **React to events** — Handle [inbound email](https://developers.cloudflare.com/agents/api-reference/email/), HTTP requests, WebSocket messages, and state changes — all from the same class.

### How it works

An agent is a TypeScript class. Methods marked with `@callable()` become typed RPC that clients can call directly over WebSocket.

* [  JavaScript ](#tab-panel-2112)
* [  TypeScript ](#tab-panel-2113)

JavaScript

```

import { Agent, callable } from "agents";


export class CounterAgent extends Agent {

  initialState = { count: 0 };


  @callable()

  increment() {

    this.setState({ count: this.state.count + 1 });

    return this.state.count;

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


export class CounterAgent extends Agent<Env, { count: number }> {

  initialState = { count: 0 };


  @callable()

  increment() {

    this.setState({ count: this.state.count + 1 });

    return this.state.count;

  }

}


```

```

import { useAgent } from "agents/react";


function Counter() {

  const [count, setCount] = useState(0);

  const agent = useAgent({

    agent: "CounterAgent",

    onStateUpdate: (state) => setCount(state.count),

  });


  return <button onClick={() => agent.stub.increment()}>{count}</button>;

}


```

For AI chat, extend `AIChatAgent` instead. Messages are persisted automatically, streams resume on disconnect, and the React hook handles the UI.

* [  JavaScript ](#tab-panel-2114)
* [  TypeScript ](#tab-panel-2115)

JavaScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";

import { createWorkersAI } from "workers-ai-provider";

import { streamText, convertToModelMessages } from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

    });

    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";

import { createWorkersAI } from "workers-ai-provider";

import { streamText, convertToModelMessages } from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

    });

    return result.toUIMessageStreamResponse();

  }

}


```

Refer to the [quick start](https://developers.cloudflare.com/agents/getting-started/quick-start/) for a full walkthrough, the [chat agents guide](https://developers.cloudflare.com/agents/api-reference/chat-agents/) for the full chat API, or the [Agents API reference](https://developers.cloudflare.com/agents/api-reference/agents-api/) for the complete SDK.

---

### Build on the Cloudflare Platform

**[Workers AI](https://developers.cloudflare.com/workers-ai/)** 

Run machine learning models, powered by serverless GPUs, on Cloudflare's global network. No API keys required.

**[Workers](https://developers.cloudflare.com/workers/)** 

Build serverless applications and deploy instantly across the globe for exceptional performance, reliability, and scale.

**[AI Gateway](https://developers.cloudflare.com/ai-gateway/)** 

Observe and control your AI applications with caching, rate limiting, request retries, model fallback, and more.

**[Vectorize](https://developers.cloudflare.com/vectorize/)** 

Build full-stack AI applications with Vectorize, Cloudflare's vector database for semantic search, recommendations, and providing context to LLMs.

**[Workflows](https://developers.cloudflare.com/workflows/)** 

Build stateful agents that guarantee executions, including automatic retries, persistent state that runs for minutes, hours, days, or weeks.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}}]}
```

---

---
title: Getting started
description: Start building agents that can remember context and make decisions. This guide walks you through creating your first agent and understanding how they work.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/getting-started/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Getting started

Start building agents that can remember context and make decisions. This guide walks you through creating your first agent and understanding how they work.

Agents maintain state across conversations and can execute workflows. Use them for customer support automation, personal assistants, or interactive experiences.

## What you will learn

Building with agents involves understanding a few core concepts:

* **State management**: How agents remember information across interactions.
* **Decision making**: How agents analyze requests and choose actions.
* **Tool integration**: How agents access external APIs and data sources.
* **Conversation flow**: How agents maintain context and personality.
* [ Quick start ](https://developers.cloudflare.com/agents/getting-started/quick-start/)
* [ Add to existing project ](https://developers.cloudflare.com/agents/getting-started/add-to-existing-project/)
* [ Testing your Agents ](https://developers.cloudflare.com/agents/getting-started/testing-your-agent/)
* [ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/)
* [ Prompt an AI model ](https://developers.cloudflare.com/workers/get-started/prompting/)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/getting-started/","name":"Getting started"}}]}
```

---

---
title: Add to existing project
description: This guide shows how to add agents to an existing Cloudflare Workers project. If you are starting fresh, refer to Building a chat agent instead.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/getting-started/add-to-existing-project.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Add to existing project

This guide shows how to add agents to an existing Cloudflare Workers project. If you are starting fresh, refer to [Building a chat agent](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) instead.

## Prerequisites

* An existing Cloudflare Workers project with a Wrangler configuration file
* Node.js 18 or newer

## 1\. Install the package

 npm  yarn  pnpm  bun 

```
npm i agents
```

```
yarn add agents
```

```
pnpm add agents
```

```
bun add agents
```

For React applications, no additional packages are needed — React bindings are included.

For Hono applications:

 npm  yarn  pnpm  bun 

```
npm i agents hono-agents
```

```
yarn add agents hono-agents
```

```
pnpm add agents hono-agents
```

```
bun add agents hono-agents
```

## 2\. Create an Agent

Create a new file for your agent (for example, `src/agents/counter.ts`):

* [  JavaScript ](#tab-panel-2872)
* [  TypeScript ](#tab-panel-2873)

JavaScript

```

import { Agent, callable } from "agents";


export class CounterAgent extends Agent {

  initialState = { count: 0 };


  @callable()

  increment() {

    this.setState({ count: this.state.count + 1 });

    return this.state.count;

  }


  @callable()

  decrement() {

    this.setState({ count: this.state.count - 1 });

    return this.state.count;

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


export type CounterState = {

  count: number;

};


export class CounterAgent extends Agent<Env, CounterState> {

  initialState: CounterState = { count: 0 };


  @callable()

  increment() {

    this.setState({ count: this.state.count + 1 });

    return this.state.count;

  }


  @callable()

  decrement() {

    this.setState({ count: this.state.count - 1 });

    return this.state.count;

  }

}


```

## 3\. Update Wrangler configuration

Add the Durable Object binding and migration:

* [  wrangler.jsonc ](#tab-panel-2860)
* [  wrangler.toml ](#tab-panel-2861)

```

{

  "name": "my-existing-project",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],


  "durable_objects": {

    "bindings": [

      {

        "name": "CounterAgent",

        "class_name": "CounterAgent",

      },

    ],

  },


  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["CounterAgent"],

    },

  ],

}


```

```

name = "my-existing-project"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[[durable_objects.bindings]]

name = "CounterAgent"

class_name = "CounterAgent"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "CounterAgent" ]


```

**Key points:**

* `name` in bindings becomes the property on `env` (for example, `env.CounterAgent`)
* `class_name` must exactly match your exported class name
* `new_sqlite_classes` enables SQLite storage for state persistence
* `nodejs_compat` flag is required for the agents package

## 4\. Export the Agent class

Your agent class must be exported from your main entry point. Update your `src/index.ts`:

* [  JavaScript ](#tab-panel-2864)
* [  TypeScript ](#tab-panel-2865)

JavaScript

```

// Export the agent class (required for Durable Objects)

export { CounterAgent } from "./agents/counter";


// Your existing exports...

export default {

  // ...

};


```

TypeScript

```

// Export the agent class (required for Durable Objects)

export { CounterAgent } from "./agents/counter";


// Your existing exports...

export default {

  // ...

} satisfies ExportedHandler<Env>;


```

## 5\. Wire up routing

Choose the approach that matches your project structure:

### Plain Workers (fetch handler)

* [  JavaScript ](#tab-panel-2874)
* [  TypeScript ](#tab-panel-2875)

JavaScript

```

import { routeAgentRequest } from "agents";

export { CounterAgent } from "./agents/counter";


export default {

  async fetch(request, env, ctx) {

    // Try agent routing first

    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    // Your existing routing logic

    const url = new URL(request.url);

    if (url.pathname === "/api/hello") {

      return Response.json({ message: "Hello!" });

    }


    return new Response("Not found", { status: 404 });

  },

};


```

TypeScript

```

import { routeAgentRequest } from "agents";

export { CounterAgent } from "./agents/counter";


export default {

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    // Try agent routing first

    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    // Your existing routing logic

    const url = new URL(request.url);

    if (url.pathname === "/api/hello") {

      return Response.json({ message: "Hello!" });

    }


    return new Response("Not found", { status: 404 });

  },

} satisfies ExportedHandler<Env>;


```

### Hono

* [  JavaScript ](#tab-panel-2868)
* [  TypeScript ](#tab-panel-2869)

JavaScript

```

import { Hono } from "hono";

import { agentsMiddleware } from "hono-agents";

export { CounterAgent } from "./agents/counter";


const app = new Hono();


// Add agents middleware - handles WebSocket upgrades and agent HTTP requests

app.use("*", agentsMiddleware());


// Your existing routes continue to work

app.get("/api/hello", (c) => c.json({ message: "Hello!" }));


export default app;


```

TypeScript

```

import { Hono } from "hono";

import { agentsMiddleware } from "hono-agents";

export { CounterAgent } from "./agents/counter";


const app = new Hono<{ Bindings: Env }>();


// Add agents middleware - handles WebSocket upgrades and agent HTTP requests

app.use("*", agentsMiddleware());


// Your existing routes continue to work

app.get("/api/hello", (c) => c.json({ message: "Hello!" }));


export default app;


```

### With static assets

If you are serving static assets alongside agents, static assets are served first by default. Your Worker code only runs for paths that do not match a static asset:

* [  JavaScript ](#tab-panel-2876)
* [  TypeScript ](#tab-panel-2877)

JavaScript

```

import { routeAgentRequest } from "agents";

export { CounterAgent } from "./agents/counter";


export default {

  async fetch(request, env, ctx) {

    // Static assets are served automatically before this runs

    // This only handles non-asset requests


    // Route to agents

    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    return new Response("Not found", { status: 404 });

  },

};


```

TypeScript

```

import { routeAgentRequest } from "agents";

export { CounterAgent } from "./agents/counter";


export default {

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    // Static assets are served automatically before this runs

    // This only handles non-asset requests


    // Route to agents

    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    return new Response("Not found", { status: 404 });

  },

} satisfies ExportedHandler<Env>;


```

Configure assets in the Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-2858)
* [  wrangler.toml ](#tab-panel-2859)

```

{

  "assets": {

    "directory": "./public",

  },

}


```

```

[assets]

directory = "./public"


```

## 6\. Generate TypeScript types

Do not hand-write your `Env` interface. Run [wrangler types](https://developers.cloudflare.com/workers/wrangler/commands/general/#types) to generate a type definition file that matches your Wrangler configuration. This catches mismatches between your config and code at compile time instead of at deploy time.

Re-run `wrangler types` whenever you add or rename a binding.

Terminal window

```

npx wrangler types


```

This creates a type definition file with all your bindings typed, including your agent Durable Object namespaces. The `Agent` class defaults to using the generated `Env` type, so you do not need to pass it as a type parameter — `extends Agent` is sufficient unless you need to pass a second type parameter for state (for example, `Agent<Env, CounterState>`).

Refer to [Configuration](https://developers.cloudflare.com/agents/api-reference/configuration/#generating-types) for more details on type generation.

## 7\. Connect from the frontend

### React

* [  JavaScript ](#tab-panel-2884)
* [  TypeScript ](#tab-panel-2885)

JavaScript

```

import { useState } from "react";

import { useAgent } from "agents/react";

function CounterWidget() {

  const [count, setCount] = useState(0);


  const agent = useAgent({

    agent: "CounterAgent",

    onStateUpdate: (state) => setCount(state.count),

  });


  return (

    <>

      {count}

      <button onClick={() => agent.stub.increment()}>+</button>

      <button onClick={() => agent.stub.decrement()}>-</button>

    </>

  );

}


```

TypeScript

```

import { useState } from "react";

import { useAgent } from "agents/react";

import type { CounterAgent, CounterState } from "./agents/counter";


function CounterWidget() {

  const [count, setCount] = useState(0);


  const agent = useAgent<CounterAgent, CounterState>({

    agent: "CounterAgent",

    onStateUpdate: (state) => setCount(state.count),

  });


  return (

    <>

      {count}

      <button onClick={() => agent.stub.increment()}>+</button>

      <button onClick={() => agent.stub.decrement()}>-</button>

    </>

  );

}


```

### Vanilla JavaScript

* [  JavaScript ](#tab-panel-2880)
* [  TypeScript ](#tab-panel-2881)

JavaScript

```

import { AgentClient } from "agents/client";


const agent = new AgentClient({

  agent: "CounterAgent",

  name: "user-123", // Optional: unique instance name

  onStateUpdate: (state) => {

    document.getElementById("count").textContent = state.count;

  },

});


// Call methods

document.getElementById("increment").onclick = () => agent.call("increment");


```

TypeScript

```

import { AgentClient } from "agents/client";


const agent = new AgentClient({

  agent: "CounterAgent",

  name: "user-123", // Optional: unique instance name

  onStateUpdate: (state) => {

    document.getElementById("count").textContent = state.count;

  },

});


// Call methods

document.getElementById("increment").onclick = () => agent.call("increment");


```

## Adding multiple agents

Add more agents by extending the configuration:

* [  JavaScript ](#tab-panel-2878)
* [  TypeScript ](#tab-panel-2879)

JavaScript

```

// src/agents/chat.ts

export class Chat extends Agent {

  // ...

}


// src/agents/scheduler.ts

export class Scheduler extends Agent {

  // ...

}


```

TypeScript

```

// src/agents/chat.ts

export class Chat extends Agent {

  // ...

}


// src/agents/scheduler.ts

export class Scheduler extends Agent {

  // ...

}


```

Update the Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-2866)
* [  wrangler.toml ](#tab-panel-2867)

```

{

  "durable_objects": {

    "bindings": [

      { "name": "CounterAgent", "class_name": "CounterAgent" },

      { "name": "Chat", "class_name": "Chat" },

      { "name": "Scheduler", "class_name": "Scheduler" },

    ],

  },

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["CounterAgent", "Chat", "Scheduler"],

    },

  ],

}


```

```

[[durable_objects.bindings]]

name = "CounterAgent"

class_name = "CounterAgent"


[[durable_objects.bindings]]

name = "Chat"

class_name = "Chat"


[[durable_objects.bindings]]

name = "Scheduler"

class_name = "Scheduler"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "CounterAgent", "Chat", "Scheduler" ]


```

Export all agents from your entry point:

* [  JavaScript ](#tab-panel-2870)
* [  TypeScript ](#tab-panel-2871)

JavaScript

```

export { CounterAgent } from "./agents/counter";

export { Chat } from "./agents/chat";

export { Scheduler } from "./agents/scheduler";


```

TypeScript

```

export { CounterAgent } from "./agents/counter";

export { Chat } from "./agents/chat";

export { Scheduler } from "./agents/scheduler";


```

## Common integration patterns

### Agents behind authentication

Check auth before routing to agents:

* [  JavaScript ](#tab-panel-2888)
* [  TypeScript ](#tab-panel-2889)

JavaScript

```

export default {

  async fetch(request, env) {

    // Check auth for agent routes

    if (request.url.includes("/agents/")) {

      const authResult = await checkAuth(request, env);

      if (!authResult.valid) {

        return new Response("Unauthorized", { status: 401 });

      }

    }


    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    // ... rest of routing

  },

};


```

TypeScript

```

export default {

  async fetch(request: Request, env: Env) {

    // Check auth for agent routes

    if (request.url.includes("/agents/")) {

      const authResult = await checkAuth(request, env);

      if (!authResult.valid) {

        return new Response("Unauthorized", { status: 401 });

      }

    }


    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    // ... rest of routing

  },

} satisfies ExportedHandler<Env>;


```

### Custom agent path prefix

By default, agents are routed at `/agents/{agent-name}/{instance-name}`. You can customize this:

* [  JavaScript ](#tab-panel-2882)
* [  TypeScript ](#tab-panel-2883)

JavaScript

```

import { routeAgentRequest } from "agents";


const agentResponse = await routeAgentRequest(request, env, {

  prefix: "/api/agents", // Now routes at /api/agents/{agent-name}/{instance-name}

});


```

TypeScript

```

import { routeAgentRequest } from "agents";


const agentResponse = await routeAgentRequest(request, env, {

  prefix: "/api/agents", // Now routes at /api/agents/{agent-name}/{instance-name}

});


```

Refer to [Routing](https://developers.cloudflare.com/agents/api-reference/routing/) for more options including CORS, custom instance naming, and location hints.

### Accessing agents from server code

You can interact with agents directly from your Worker code:

* [  JavaScript ](#tab-panel-2890)
* [  TypeScript ](#tab-panel-2891)

JavaScript

```

import { getAgentByName } from "agents";


export default {

  async fetch(request, env) {

    if (request.url.endsWith("/api/increment")) {

      // Get a specific agent instance

      const counter = await getAgentByName(env.CounterAgent, "shared-counter");

      const newCount = await counter.increment();

      return Response.json({ count: newCount });

    }

    // ...

  },

};


```

TypeScript

```

import { getAgentByName } from "agents";


export default {

  async fetch(request: Request, env: Env) {

    if (request.url.endsWith("/api/increment")) {

      // Get a specific agent instance

      const counter = await getAgentByName(env.CounterAgent, "shared-counter");

      const newCount = await counter.increment();

      return Response.json({ count: newCount });

    }

    // ...

  },

} satisfies ExportedHandler<Env>;


```

## Troubleshooting

### Agent not found, or 404 errors

1. **Check the export** \- Agent class must be exported from your main entry point.
2. **Check the binding** \- `class_name` in the Wrangler configuration file must exactly match the exported class name.
3. **Check the route** \- Default route is `/agents/{agent-name}/{instance-name}`.

### No such Durable Object class error

Add the migration to the Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-2862)
* [  wrangler.toml ](#tab-panel-2863)

```

{

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["YourAgentClass"],

    },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "YourAgentClass" ]


```

### WebSocket connection fails

Ensure your routing passes the response unchanged:

* [  JavaScript ](#tab-panel-2886)
* [  TypeScript ](#tab-panel-2887)

JavaScript

```

// Correct - return the response directly

const agentResponse = await routeAgentRequest(request, env);

if (agentResponse) return agentResponse;


// Wrong - this breaks WebSocket connections

if (agentResponse) return new Response(agentResponse.body);


```

TypeScript

```

// Correct - return the response directly

const agentResponse = await routeAgentRequest(request, env);

if (agentResponse) return agentResponse;


// Wrong - this breaks WebSocket connections

if (agentResponse) return new Response(agentResponse.body);


```

### State not persisting

Check that:

1. You are using `this.setState()`, not mutating `this.state` directly.
2. The agent class is in `new_sqlite_classes` in migrations.
3. You are connecting to the same agent instance name.

## Next steps

[ State management ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Manage and synchronize agent state. 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Background tasks and cron jobs. 

[ Agent class internals ](https://developers.cloudflare.com/agents/concepts/agent-class/) Full lifecycle and methods reference. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/getting-started/","name":"Getting started"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/getting-started/add-to-existing-project/","name":"Add to existing project"}}]}
```

---

---
title: Build a chat agent
description: Build a streaming AI chat agent with tools using Workers AI — no API keys required.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/getting-started/build-a-chat-agent.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build a chat agent

Build a chat agent that streams AI responses, calls server-side tools, executes client-side tools in the browser, and asks for user approval before sensitive actions.

**What you will build:** A chat agent powered by Workers AI with three tool types — automatic, client-side, and approval-gated.

**Time:** \~15 minutes

**Prerequisites:**

* Node.js 18+
* A Cloudflare account (free tier works)

## 1\. Create the project

Terminal window

```

npm create cloudflare@latest chat-agent


```

Select **"Hello World" Worker** when prompted. Then install the dependencies:

Terminal window

```

cd chat-agent

npm install agents @cloudflare/ai-chat ai workers-ai-provider zod


```

## 2\. Configure Wrangler

Replace your `wrangler.jsonc` with:

* [  wrangler.jsonc ](#tab-panel-2892)
* [  wrangler.toml ](#tab-panel-2893)

```

{

  "name": "chat-agent",

  "main": "src/server.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],

  "ai": { "binding": "AI" },

  "durable_objects": {

    "bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],

  },

  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],

}


```

```

name = "chat-agent"

main = "src/server.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[ai]

binding = "AI"


[[durable_objects.bindings]]

name = "ChatAgent"

class_name = "ChatAgent"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "ChatAgent" ]


```

Key settings:

* `ai` binds Workers AI — no API key needed
* `durable_objects` registers your chat agent class
* `new_sqlite_classes` enables SQLite storage for message persistence

## 3\. Write the server

Create `src/server.ts`. This is where your agent lives:

* [  JavaScript ](#tab-panel-2894)
* [  TypeScript ](#tab-panel-2895)

JavaScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";

import { routeAgentRequest } from "agents";

import { createWorkersAI } from "workers-ai-provider";

import {

  streamText,

  convertToModelMessages,

  pruneMessages,

  tool,

  stepCountIs,

} from "ai";

import { z } from "zod";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),

      system:

        "You are a helpful assistant. You can check the weather, " +

        "get the user's timezone, and run calculations.",

      messages: pruneMessages({

        messages: await convertToModelMessages(this.messages),

        toolCalls: "before-last-2-messages",

      }),

      tools: {

        // Server-side tool: runs automatically on the server

        getWeather: tool({

          description: "Get the current weather for a city",

          inputSchema: z.object({

            city: z.string().describe("City name"),

          }),

          execute: async ({ city }) => {

            // Replace with a real weather API in production

            const conditions = ["sunny", "cloudy", "rainy"];

            const temp = Math.floor(Math.random() * 30) + 5;

            return {

              city,

              temperature: temp,

              condition:

                conditions[Math.floor(Math.random() * conditions.length)],

            };

          },

        }),


        // Client-side tool: no execute function — the browser handles it

        getUserTimezone: tool({

          description: "Get the user's timezone from their browser",

          inputSchema: z.object({}),

        }),


        // Approval tool: requires user confirmation before executing

        calculate: tool({

          description:

            "Perform a math calculation with two numbers. " +

            "Requires user approval for large numbers.",

          inputSchema: z.object({

            a: z.number().describe("First number"),

            b: z.number().describe("Second number"),

            operator: z

              .enum(["+", "-", "*", "/", "%"])

              .describe("Arithmetic operator"),

          }),

          needsApproval: async ({ a, b }) =>

            Math.abs(a) > 1000 || Math.abs(b) > 1000,

          execute: async ({ a, b, operator }) => {

            const ops = {

              "+": (x, y) => x + y,

              "-": (x, y) => x - y,

              "*": (x, y) => x * y,

              "/": (x, y) => x / y,

              "%": (x, y) => x % y,

            };

            if (operator === "/" && b === 0) {

              return { error: "Division by zero" };

            }

            return {

              expression: `${a} ${operator} ${b}`,

              result: ops[operator](a, b),

            };

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


export default {

  async fetch(request, env) {

    return (

      (await routeAgentRequest(request, env)) ||

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";

import { routeAgentRequest } from "agents";

import { createWorkersAI } from "workers-ai-provider";

import {

  streamText,

  convertToModelMessages,

  pruneMessages,

  tool,

  stepCountIs,

} from "ai";

import { z } from "zod";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),

      system:

        "You are a helpful assistant. You can check the weather, " +

        "get the user's timezone, and run calculations.",

      messages: pruneMessages({

        messages: await convertToModelMessages(this.messages),

        toolCalls: "before-last-2-messages",

      }),

      tools: {

        // Server-side tool: runs automatically on the server

        getWeather: tool({

          description: "Get the current weather for a city",

          inputSchema: z.object({

            city: z.string().describe("City name"),

          }),

          execute: async ({ city }) => {

            // Replace with a real weather API in production

            const conditions = ["sunny", "cloudy", "rainy"];

            const temp = Math.floor(Math.random() * 30) + 5;

            return {

              city,

              temperature: temp,

              condition:

                conditions[Math.floor(Math.random() * conditions.length)],

            };

          },

        }),


        // Client-side tool: no execute function — the browser handles it

        getUserTimezone: tool({

          description: "Get the user's timezone from their browser",

          inputSchema: z.object({}),

        }),


        // Approval tool: requires user confirmation before executing

        calculate: tool({

          description:

            "Perform a math calculation with two numbers. " +

            "Requires user approval for large numbers.",

          inputSchema: z.object({

            a: z.number().describe("First number"),

            b: z.number().describe("Second number"),

            operator: z

              .enum(["+", "-", "*", "/", "%"])

              .describe("Arithmetic operator"),

          }),

          needsApproval: async ({ a, b }) =>

            Math.abs(a) > 1000 || Math.abs(b) > 1000,

          execute: async ({ a, b, operator }) => {

            const ops: Record<string, (x: number, y: number) => number> = {

              "+": (x, y) => x + y,

              "-": (x, y) => x - y,

              "*": (x, y) => x * y,

              "/": (x, y) => x / y,

              "%": (x, y) => x % y,

            };

            if (operator === "/" && b === 0) {

              return { error: "Division by zero" };

            }

            return {

              expression: `${a} ${operator} ${b}`,

              result: ops[operator](a, b),

            };

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


export default {

  async fetch(request: Request, env: Env) {

    return (

      (await routeAgentRequest(request, env)) ||

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

### What each tool type does

| Tool            | execute? | needsApproval?      | Behavior                                        |
| --------------- | -------- | ------------------- | ----------------------------------------------- |
| getWeather      | Yes      | No                  | Runs on the server automatically                |
| getUserTimezone | No       | No                  | Sent to the client; browser provides the result |
| calculate       | Yes      | Yes (large numbers) | Pauses for user approval, then runs on server   |

## 4\. Write the client

Create `src/client.tsx`:

* [  JavaScript ](#tab-panel-2896)
* [  TypeScript ](#tab-panel-2897)

JavaScript

```

import { useAgent } from "agents/react";

import { useAgentChat } from "@cloudflare/ai-chat/react";


function Chat() {

  const agent = useAgent({ agent: "ChatAgent" });


  const {

    messages,

    sendMessage,

    clearHistory,

    addToolApprovalResponse,

    status,

  } = useAgentChat({

    agent,

    // Handle client-side tools (tools with no server execute function)

    onToolCall: async ({ toolCall, addToolOutput }) => {

      if (toolCall.toolName === "getUserTimezone") {

        addToolOutput({

          toolCallId: toolCall.toolCallId,

          output: {

            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,

            localTime: new Date().toLocaleTimeString(),

          },

        });

      }

    },

  });


  return (

    <div>

      <div>

        {messages.map((msg) => (

          <div key={msg.id}>

            <strong>{msg.role}:</strong>

            {msg.parts.map((part, i) => {

              if (part.type === "text") {

                return <span key={i}>{part.text}</span>;

              }


              // Render approval UI for tools that need confirmation

              if (part.type === "tool" && part.state === "approval-required") {

                return (

                  <div key={part.toolCallId}>

                    <p>

                      Approve <strong>{part.toolName}</strong>?

                    </p>

                    <pre>{JSON.stringify(part.input, null, 2)}</pre>

                    <button

                      onClick={() =>

                        addToolApprovalResponse({

                          id: part.toolCallId,

                          approved: true,

                        })

                      }

                    >

                      Approve

                    </button>

                    <button

                      onClick={() =>

                        addToolApprovalResponse({

                          id: part.toolCallId,

                          approved: false,

                        })

                      }

                    >

                      Reject

                    </button>

                  </div>

                );

              }


              // Show completed tool results

              if (part.type === "tool" && part.state === "output-available") {

                return (

                  <details key={part.toolCallId}>

                    <summary>{part.toolName} result</summary>

                    <pre>{JSON.stringify(part.output, null, 2)}</pre>

                  </details>

                );

              }


              return null;

            })}

          </div>

        ))}

      </div>


      <form

        onSubmit={(e) => {

          e.preventDefault();

          const input = e.currentTarget.elements.namedItem("message");

          sendMessage({ text: input.value });

          input.value = "";

        }}

      >

        <input name="message" placeholder="Try: What's the weather in Paris?" />

        <button type="submit" disabled={status === "streaming"}>

          Send

        </button>

      </form>


      <button onClick={clearHistory}>Clear history</button>

    </div>

  );

}


export default function App() {

  return <Chat />;

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useAgentChat } from "@cloudflare/ai-chat/react";


function Chat() {

  const agent = useAgent({ agent: "ChatAgent" });


  const { messages, sendMessage, clearHistory, addToolApprovalResponse, status } =

    useAgentChat({

      agent,

      // Handle client-side tools (tools with no server execute function)

      onToolCall: async ({ toolCall, addToolOutput }) => {

        if (toolCall.toolName === "getUserTimezone") {

          addToolOutput({

            toolCallId: toolCall.toolCallId,

            output: {

              timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,

              localTime: new Date().toLocaleTimeString(),

            },

          });

        }

      },

    });


  return (

    <div>

      <div>

        {messages.map((msg) => (

          <div key={msg.id}>

            <strong>{msg.role}:</strong>

            {msg.parts.map((part, i) => {

              if (part.type === "text") {

                return <span key={i}>{part.text}</span>;

              }


              // Render approval UI for tools that need confirmation

              if (

                part.type === "tool" &&

                part.state === "approval-required"

              ) {

                return (

                  <div key={part.toolCallId}>

                    <p>

                      Approve <strong>{part.toolName}</strong>?

                    </p>

                    <pre>{JSON.stringify(part.input, null, 2)}</pre>

                    <button

                      onClick={() =>

                        addToolApprovalResponse({

                          id: part.toolCallId,

                          approved: true,

                        })

                      }

                    >

                      Approve

                    </button>

                    <button

                      onClick={() =>

                        addToolApprovalResponse({

                          id: part.toolCallId,

                          approved: false,

                        })

                      }

                    >

                      Reject

                    </button>

                  </div>

                );

              }


              // Show completed tool results

              if (

                part.type === "tool" &&

                part.state === "output-available"

              ) {

                return (

                  <details key={part.toolCallId}>

                    <summary>{part.toolName} result</summary>

                    <pre>{JSON.stringify(part.output, null, 2)}</pre>

                  </details>

                );

              }


              return null;

            })}

          </div>

        ))}

      </div>


      <form

        onSubmit={(e) => {

          e.preventDefault();

          const input = e.currentTarget.elements.namedItem(

            "message",

          ) as HTMLInputElement;

          sendMessage({ text: input.value });

          input.value = "";

        }}

      >

        <input name="message" placeholder="Try: What's the weather in Paris?" />

        <button type="submit" disabled={status === "streaming"}>

          Send

        </button>

      </form>


      <button onClick={clearHistory}>Clear history</button>

    </div>

  );

}


export default function App() {

  return <Chat />;

}


```

### Key client concepts

* **`useAgent`** connects to your `ChatAgent` over WebSocket
* **`useAgentChat`** manages the chat lifecycle (messages, streaming, tools)
* **`onToolCall`** handles client-side tools — when the LLM calls `getUserTimezone`, the browser provides the result and the conversation auto-continues
* **`addToolApprovalResponse`** approves or rejects tools that have `needsApproval`
* Messages, streaming, and resumption are all handled automatically

## 5\. Run locally

Generate types and start the dev server:

Terminal window

```

npx wrangler types

npm run dev


```

Try these prompts:

* **"What is the weather in Tokyo?"** — calls the server-side `getWeather` tool
* **"What timezone am I in?"** — calls the client-side `getUserTimezone` tool (the browser provides the answer)
* **"What is 5000 times 3?"** — triggers the approval UI before executing (numbers over 1000)

## 6\. Deploy

Terminal window

```

npx wrangler deploy


```

Your agent is now live on Cloudflare's global network. Messages persist in SQLite, streams resume on disconnect, and the agent hibernates when idle to save resources.

## What you built

Your chat agent has:

* **Streaming AI responses** via Workers AI (no API keys)
* **Message persistence** in SQLite — conversations survive restarts
* **Server-side tools** that execute automatically
* **Client-side tools** that run in the browser and feed results back to the LLM
* **Human-in-the-loop approval** for sensitive operations
* **Resumable streaming** — if a client disconnects mid-stream, it picks up where it left off

## Next steps

[ Chat agents API reference ](https://developers.cloudflare.com/agents/api-reference/chat-agents/) Full reference for AIChatAgent and useAgentChat — providers, storage, advanced patterns. 

[ Store and sync state ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Add real-time state beyond chat messages. 

[ Callable methods ](https://developers.cloudflare.com/agents/api-reference/callable-methods/) Expose agent methods as typed RPC for your client. 

[ Human-in-the-loop ](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) Deeper patterns for approval flows and manual intervention. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/getting-started/","name":"Getting started"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/getting-started/build-a-chat-agent/","name":"Build a chat agent"}}]}
```

---

---
title: Prompt an AI model
description: Use the Workers &#34;mega prompt&#34; to build a Agents using your preferred AI tools and/or IDEs. The prompt understands the Agents SDK APIs, best practices and guidelines, and makes it easier to build valid Agents and Workers.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/getting-started/prompting.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Prompt an AI model

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/getting-started/","name":"Getting started"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/getting-started/prompting/","name":"Prompt an AI model"}}]}
```

---

---
title: Quick start
description: Build your first agent in 10 minutes — a counter with persistent state that syncs to a React frontend in real-time.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/getting-started/quick-start.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Quick start

Build AI agents that persist, think, and act. Agents run on Cloudflare's global network, maintain state across requests, and connect to clients in real-time via WebSockets.

**What you will build:** A counter agent with persistent state that syncs to a React frontend in real-time.

**Time:** \~10 minutes

## Create a new project

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- --template cloudflare/agents-starter
```

```
yarn create cloudflare --template cloudflare/agents-starter
```

```
pnpm create cloudflare@latest --template cloudflare/agents-starter
```

Then install dependencies and start the dev server:

Terminal window

```

cd my-agent

npm install

npm run dev


```

This creates a project with:

* `src/server.ts` — Your agent code
* `src/client.tsx` — React frontend
* `wrangler.jsonc` — Cloudflare configuration

Open [http://localhost:5173 ↗](http://localhost:5173) to see your agent in action.

## Your first agent

Build a simple counter agent from scratch. Replace `src/server.ts`:

* [  JavaScript ](#tab-panel-2906)
* [  TypeScript ](#tab-panel-2907)

JavaScript

```

import { Agent, routeAgentRequest, callable } from "agents";


// Define the state shape

// Create the agent

export class CounterAgent extends Agent {

  // Initial state for new instances

  initialState = { count: 0 };


  // Methods marked with @callable can be called from the client

  @callable()

  increment() {

    this.setState({ count: this.state.count + 1 });

    return this.state.count;

  }


  @callable()

  decrement() {

    this.setState({ count: this.state.count - 1 });

    return this.state.count;

  }


  @callable()

  reset() {

    this.setState({ count: 0 });

  }

}


// Route requests to agents

export default {

  async fetch(request, env, ctx) {

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { Agent, routeAgentRequest, callable } from "agents";


// Define the state shape

export type CounterState = {

  count: number;

};


// Create the agent

export class CounterAgent extends Agent<Env, CounterState> {

  // Initial state for new instances

  initialState: CounterState = { count: 0 };


  // Methods marked with @callable can be called from the client

  @callable()

  increment() {

    this.setState({ count: this.state.count + 1 });

    return this.state.count;

  }


  @callable()

  decrement() {

    this.setState({ count: this.state.count - 1 });

    return this.state.count;

  }


  @callable()

  reset() {

    this.setState({ count: 0 });

  }

}


// Route requests to agents

export default {

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

Update `wrangler.jsonc` to register the agent:

* [  wrangler.jsonc ](#tab-panel-2898)
* [  wrangler.toml ](#tab-panel-2899)

```

{

  "name": "my-agent",

  "main": "src/server.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {

    "bindings": [

      {

        "name": "CounterAgent",

        "class_name": "CounterAgent",

      },

    ],

  },

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["CounterAgent"],

    },

  ],

}


```

```

name = "my-agent"

main = "src/server.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[[durable_objects.bindings]]

name = "CounterAgent"

class_name = "CounterAgent"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "CounterAgent" ]


```

## Connect from React

Replace `src/client.tsx`:

src/client.tsx

```

import { useState } from "react";

import { useAgent } from "agents/react";

import type { CounterAgent, CounterState } from "./server";


export default function App() {

  const [count, setCount] = useState(0);


  // Connect to the Counter agent

  const agent = useAgent<CounterAgent, CounterState>({

    agent: "CounterAgent",

    onStateUpdate: (state) => setCount(state.count),

  });


  return (

    <div style={{ padding: "2rem", fontFamily: "system-ui" }}>

      <h1>Counter Agent</h1>

      <p style={{ fontSize: "3rem" }}>{count}</p>

      <div style={{ display: "flex", gap: "1rem" }}>

        <button onClick={() => agent.stub.decrement()}>-</button>

        <button onClick={() => agent.stub.reset()}>Reset</button>

        <button onClick={() => agent.stub.increment()}>+</button>

      </div>

    </div>

  );

}


```

Key points:

* `useAgent` connects to your agent via WebSocket
* `onStateUpdate` fires whenever the agent's state changes
* `agent.stub.methodName()` calls methods marked with `@callable()` on your agent

## What just happened?

When you clicked the button:

1. **Client** called `agent.stub.increment()` over WebSocket
2. **Agent** ran `increment()`, updated state with `setState()`
3. **State** persisted to SQLite automatically
4. **Broadcast** sent to all connected clients
5. **React** updated via `onStateUpdate`

flowchart LR
    A["Browser<br/>(React)"] <-->|WebSocket| B["Agent<br/>(Counter)"]
    B --> C["SQLite<br/>(State)"]

### Key concepts

| Concept              | What it means                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| **Agent instance**   | Each unique name gets its own agent. CounterAgent:user-123 is separate from CounterAgent:user-456 |
| **Persistent state** | State survives restarts, deploys, and hibernation. It is stored in SQLite                         |
| **Real-time sync**   | All clients connected to the same agent receive state updates instantly                           |
| **Hibernation**      | When no clients are connected, the agent hibernates (no cost). It wakes on the next request       |

## Connect from vanilla JavaScript

If you are not using React:

* [  JavaScript ](#tab-panel-2902)
* [  TypeScript ](#tab-panel-2903)

JavaScript

```

import { AgentClient } from "agents/client";


const agent = new AgentClient({

  agent: "CounterAgent",

  name: "my-counter", // optional, defaults to "default"

  onStateUpdate: (state) => {

    console.log("New count:", state.count);

  },

});


// Call methods

await agent.call("increment");

await agent.call("reset");


```

TypeScript

```

import { AgentClient } from "agents/client";


const agent = new AgentClient({

  agent: "CounterAgent",

  name: "my-counter", // optional, defaults to "default"

  onStateUpdate: (state) => {

    console.log("New count:", state.count);

  },

});


// Call methods

await agent.call("increment");

await agent.call("reset");


```

## Deploy to Cloudflare

Terminal window

```

npm run deploy


```

Your agent is now live on Cloudflare's global network, running close to your users.

## Troubleshooting

### "Agent not found" or 404 errors

Make sure:

1. Agent class is exported from your server file
2. `wrangler.jsonc` has the binding and migration
3. Agent name in client matches the class name (case-insensitive)

### State not syncing

Check that:

1. You are calling `this.setState()`, not mutating `this.state` directly
2. The `onStateUpdate` callback is wired up in your client
3. WebSocket connection is established (check browser dev tools)

### "Method X is not callable" errors

Make sure your methods are decorated with `@callable()`:

* [  JavaScript ](#tab-panel-2900)
* [  TypeScript ](#tab-panel-2901)

JavaScript

```

import { Agent, callable } from "agents";


export class MyAgent extends Agent {

  @callable()

  increment() {

    // ...

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


export class MyAgent extends Agent {

  @callable()

  increment() {

    // ...

  }

}


```

### Type errors with `agent.stub`

Add the agent and state type parameters:

* [  JavaScript ](#tab-panel-2904)
* [  TypeScript ](#tab-panel-2905)

JavaScript

```

import { useAgent } from "agents/react";

// Pass the agent and state types to useAgent

const agent = useAgent({

  agent: "CounterAgent",

  onStateUpdate: (state) => setCount(state.count),

});


// Now agent.stub is fully typed

agent.stub.increment();


```

TypeScript

```

import { useAgent } from "agents/react";

import type { CounterAgent, CounterState } from "./server";


// Pass the agent and state types to useAgent

const agent = useAgent<CounterAgent, CounterState>({

  agent: "CounterAgent",

  onStateUpdate: (state) => setCount(state.count),

});


// Now agent.stub is fully typed

agent.stub.increment();


```

### `SyntaxError: Invalid or unexpected token` with `@callable()`

If your dev server fails with `SyntaxError: Invalid or unexpected token`, set `"target": "ES2021"` in your `tsconfig.json`. This ensures that Vite's esbuild transpiler downlevels TC39 decorators instead of passing them through as native syntax.

```

{

  "compilerOptions": {

    "target": "ES2021"

  }

}


```

Warning

Do not set `"experimentalDecorators": true` in your `tsconfig.json`. The Agents SDK uses [TC39 standard decorators ↗](https://github.com/tc39/proposal-decorators), not TypeScript legacy decorators. Enabling `experimentalDecorators` applies an incompatible transform that silently breaks `@callable()` at runtime.

## Next steps

Now that you have a working agent, explore these topics:

### Common patterns

| Learn how to             | Refer to                                                                                   |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| Add AI/LLM capabilities  | [Using AI models](https://developers.cloudflare.com/agents/api-reference/using-ai-models/) |
| Expose tools via MCP     | [MCP servers](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/)       |
| Run background tasks     | [Schedule tasks](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/)   |
| Handle emails            | [Email routing](https://developers.cloudflare.com/agents/api-reference/email/)             |
| Use Cloudflare Workflows | [Run Workflows](https://developers.cloudflare.com/agents/api-reference/run-workflows/)     |

### Explore more

[ State management ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Deep dive into setState(), initialState, and onStateChanged(). 

[ Client SDK ](https://developers.cloudflare.com/agents/api-reference/client-sdk/) Full useAgent and AgentClient API reference. 

[ Callable methods ](https://developers.cloudflare.com/agents/api-reference/callable-methods/) Expose methods to clients with @callable(). 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Run tasks on a delay, schedule, or cron. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/getting-started/","name":"Getting started"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/getting-started/quick-start/","name":"Quick start"}}]}
```

---

---
title: Testing your Agents
description: Because Agents run on Cloudflare Workers and Durable Objects, they can be tested using the same tools and techniques as Workers and Durable Objects.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/getting-started/testing-your-agent.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Testing your Agents

Because Agents run on Cloudflare Workers and Durable Objects, they can be tested using the same tools and techniques as Workers and Durable Objects.

## Writing and running tests

### Setup

Note

The `agents-starter` template and new Cloudflare Workers projects already include the relevant `vitest` and `@cloudflare/vitest-pool-workers` packages, as well as a valid `vitest.config.js` file.

Before you write your first test, install the necessary packages:

Terminal window

```

npm install vitest@~3.0.0 --save-dev --save-exact

npm install @cloudflare/vitest-pool-workers --save-dev


```

Ensure that your `vitest.config.js` file is identical to the following:

JavaScript

```

import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";


export default defineWorkersConfig({

  test: {

    poolOptions: {

      workers: {

        wrangler: { configPath: "./wrangler.jsonc" },

      },

    },

  },

});


```

### Add the Agent configuration

Add a `durableObjects` configuration to `vitest.config.js` with the name of your Agent class:

JavaScript

```

import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";


export default defineWorkersConfig({

  test: {

    poolOptions: {

      workers: {

        main: "./src/index.ts",

        miniflare: {

          durableObjects: {

            NAME: "MyAgent",

          },

        },

      },

    },

  },

});


```

### Write a test

Note

Review the [Vitest documentation ↗](https://vitest.dev/) for more information on testing, including the test API reference and advanced testing techniques.

Tests use the `vitest` framework. A basic test suite for your Agent can validate how your Agent responds to requests, but can also unit test your Agent's methods and state.

TypeScript

```

import {

  env,

  createExecutionContext,

  waitOnExecutionContext,

  SELF,

} from "cloudflare:test";

import { describe, it, expect } from "vitest";

import worker from "../src";

import { Env } from "../src";


interface ProvidedEnv extends Env {}


describe("make a request to my Agent", () => {

  // Unit testing approach

  it("responds with state", async () => {

    // Provide a valid URL that your Worker can use to route to your Agent

    // If you are using routeAgentRequest, this will be /agent/:agent/:name

    const request = new Request<unknown, IncomingRequestCfProperties>(

      "http://example.com/agent/my-agent/agent-123",

    );

    const ctx = createExecutionContext();

    const response = await worker.fetch(request, env, ctx);

    await waitOnExecutionContext(ctx);

    expect(await response.text()).toMatchObject({ hello: "from your agent" });

  });


  it("also responds with state", async () => {

    const request = new Request("http://example.com/agent/my-agent/agent-123");

    const response = await SELF.fetch(request);

    expect(await response.text()).toMatchObject({ hello: "from your agent" });

  });

});


```

### Run tests

Running tests is done using the `vitest` CLI:

Terminal window

```

$ npm run test

# or run vitest directly

$ npx vitest


```

```

  MyAgent

    ✓ should return a greeting (1 ms)


Test Files  1 passed (1)


```

Review the [documentation on testing](https://developers.cloudflare.com/workers/testing/vitest-integration/write-your-first-test/) for additional examples and test configuration.

## Running Agents locally

You can also run an Agent locally using the `wrangler` CLI:

Terminal window

```

$ npx wrangler dev


```

```

Your Worker and resources are simulated locally via Miniflare. For more information, see: https://developers.cloudflare.com/workers/testing/local-development.


Your worker has access to the following bindings:

- Durable Objects:

  - MyAgent: MyAgent

  Starting local server...

[wrangler:inf] Ready on http://localhost:53645


```

This spins up a local development server that runs the same runtime as Cloudflare Workers, and allows you to iterate on your Agent's code and test it locally without deploying it.

Visit the [wrangler dev ↗](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) docs to review the CLI flags and configuration options.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/getting-started/","name":"Getting started"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/getting-started/testing-your-agent/","name":"Testing your Agents"}}]}
```

---

---
title: Patterns
description: This page lists and defines common patterns for implementing AI agents, based on Anthropic's patterns for building effective agents.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/patterns.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Patterns

This page lists and defines common patterns for implementing AI agents, based on [Anthropic's patterns for building effective agents ↗](https://www.anthropic.com/research/building-effective-agents).

Code samples use the [AI SDK ↗](https://sdk.vercel.ai/docs/foundations/agents), running in [Durable Objects](https://developers.cloudflare.com/durable-objects).

## Prompt Chaining

Decomposes tasks into a sequence of steps, where each LLM call processes the output of the previous one.

![Figure 1: Prompt Chaining](https://developers.cloudflare.com/_astro/01-prompt-chaining.BLijYLLo_Z4mjQb.webp) 

TypeScript

```

import { openai } from "@ai-sdk/openai";

import { generateText, generateObject } from "ai";

import { z } from "zod";


export default async function generateMarketingCopy(input: string) {

  const model = openai("gpt-4o");


  // First step: Generate marketing copy

  const { text: copy } = await generateText({

    model,

    prompt: `Write persuasive marketing copy for: ${input}. Focus on benefits and emotional appeal.`,

  });


  // Perform quality check on copy

  const { object: qualityMetrics } = await generateObject({

    model,

    schema: z.object({

      hasCallToAction: z.boolean(),

      emotionalAppeal: z.number().min(1).max(10),

      clarity: z.number().min(1).max(10),

    }),

    prompt: `Evaluate this marketing copy for:

    1. Presence of call to action (true/false)

    2. Emotional appeal (1-10)

    3. Clarity (1-10)


    Copy to evaluate: ${copy}`,

  });


  // If quality check fails, regenerate with more specific instructions

  if (

    !qualityMetrics.hasCallToAction ||

    qualityMetrics.emotionalAppeal < 7 ||

    qualityMetrics.clarity < 7

  ) {

    const { text: improvedCopy } = await generateText({

      model,

      prompt: `Rewrite this marketing copy with:

      ${!qualityMetrics.hasCallToAction ? "- A clear call to action" : ""}

      ${qualityMetrics.emotionalAppeal < 7 ? "- Stronger emotional appeal" : ""}

      ${qualityMetrics.clarity < 7 ? "- Improved clarity and directness" : ""}


      Original copy: ${copy}`,

    });

    return { copy: improvedCopy, qualityMetrics };

  }


  return { copy, qualityMetrics };

}


```

## Routing

Classifies input and directs it to specialized followup tasks, allowing for separation of concerns.

![Figure 2: Routing](https://developers.cloudflare.com/_astro/2_Routing.CT-Tgwab_1YYXmR.webp) 

TypeScript

```

import { openai } from '@ai-sdk/openai';

import { generateObject, generateText } from 'ai';

import { z } from 'zod';


async function handleCustomerQuery(query: string) {

  const model = openai('gpt-4o');


  // First step: Classify the query type

  const { object: classification } = await generateObject({

    model,

    schema: z.object({

      reasoning: z.string(),

      type: z.enum(['general', 'refund', 'technical']),

      complexity: z.enum(['simple', 'complex']),

    }),

    prompt: `Classify this customer query:

    ${query}


    Determine:

    1. Query type (general, refund, or technical)

    2. Complexity (simple or complex)

    3. Brief reasoning for classification`,

  });


  // Route based on classification

  // Set model and system prompt based on query type and complexity

  const { text: response } = await generateText({

    model:

      classification.complexity === 'simple'

        ? openai('gpt-4o-mini')

        : openai('o1-mini'),

    system: {

      general:

        'You are an expert customer service agent handling general inquiries.',

      refund:

        'You are a customer service agent specializing in refund requests. Follow company policy and collect necessary information.',

      technical:

        'You are a technical support specialist with deep product knowledge. Focus on clear step-by-step troubleshooting.',

    }[classification.type],

    prompt: query,

  });


  return { response, classification };

}


```

## Parallelization

Enables simultaneous task processing through sectioning or voting mechanisms.

![Figure 3: Parallelization](https://developers.cloudflare.com/_astro/3_Parallelization.gkwf-xnL_1psyLV.webp) 

TypeScript

```

import { openai } from '@ai-sdk/openai';

import { generateText, generateObject } from 'ai';

import { z } from 'zod';


// Example: Parallel code review with multiple specialized reviewers

async function parallelCodeReview(code: string) {

  const model = openai('gpt-4o');


  // Run parallel reviews

  const [securityReview, performanceReview, maintainabilityReview] =

    await Promise.all([

      generateObject({

        model,

        system:

          'You are an expert in code security. Focus on identifying security vulnerabilities, injection risks, and authentication issues.',

        schema: z.object({

          vulnerabilities: z.array(z.string()),

          riskLevel: z.enum(['low', 'medium', 'high']),

          suggestions: z.array(z.string()),

        }),

        prompt: `Review this code:

      ${code}`,

      }),


      generateObject({

        model,

        system:

          'You are an expert in code performance. Focus on identifying performance bottlenecks, memory leaks, and optimization opportunities.',

        schema: z.object({

          issues: z.array(z.string()),

          impact: z.enum(['low', 'medium', 'high']),

          optimizations: z.array(z.string()),

        }),

        prompt: `Review this code:

      ${code}`,

      }),


      generateObject({

        model,

        system:

          'You are an expert in code quality. Focus on code structure, readability, and adherence to best practices.',

        schema: z.object({

          concerns: z.array(z.string()),

          qualityScore: z.number().min(1).max(10),

          recommendations: z.array(z.string()),

        }),

        prompt: `Review this code:

      ${code}`,

      }),

    ]);


  const reviews = [

    { ...securityReview.object, type: 'security' },

    { ...performanceReview.object, type: 'performance' },

    { ...maintainabilityReview.object, type: 'maintainability' },

  ];


  // Aggregate results using another model instance

  const { text: summary } = await generateText({

    model,

    system: 'You are a technical lead summarizing multiple code reviews.',

    prompt: `Synthesize these code review results into a concise summary with key actions:

    ${JSON.stringify(reviews, null, 2)}`,

  });


  return { reviews, summary };

}


```

## Orchestrator-Workers

A central LLM dynamically breaks down tasks, delegates to Worker LLMs, and synthesizes results.

![Figure 4: Orchestrator Workers](https://developers.cloudflare.com/_astro/4_Orchestrator-Workers.jVghtZEj_Z6FePI.webp) 

TypeScript

```

import { openai } from '@ai-sdk/openai';

import { generateObject } from 'ai';

import { z } from 'zod';


async function implementFeature(featureRequest: string) {

  // Orchestrator: Plan the implementation

  const { object: implementationPlan } = await generateObject({

    model: openai('o1'),

    schema: z.object({

      files: z.array(

        z.object({

          purpose: z.string(),

          filePath: z.string(),

          changeType: z.enum(['create', 'modify', 'delete']),

        }),

      ),

      estimatedComplexity: z.enum(['low', 'medium', 'high']),

    }),

    system:

      'You are a senior software architect planning feature implementations.',

    prompt: `Analyze this feature request and create an implementation plan:

    ${featureRequest}`,

  });


  // Workers: Execute the planned changes

  const fileChanges = await Promise.all(

    implementationPlan.files.map(async file => {

      // Each worker is specialized for the type of change

      const workerSystemPrompt = {

        create:

          'You are an expert at implementing new files following best practices and project patterns.',

        modify:

          'You are an expert at modifying existing code while maintaining consistency and avoiding regressions.',

        delete:

          'You are an expert at safely removing code while ensuring no breaking changes.',

      }[file.changeType];


      const { object: change } = await generateObject({

        model: openai('gpt-4o'),

        schema: z.object({

          explanation: z.string(),

          code: z.string(),

        }),

        system: workerSystemPrompt,

        prompt: `Implement the changes for ${file.filePath} to support:

        ${file.purpose}


        Consider the overall feature context:

        ${featureRequest}`,

      });


      return {

        file,

        implementation: change,

      };

    }),

  );


  return {

    plan: implementationPlan,

    changes: fileChanges,

  };

}


```

## Evaluator-Optimizer

One LLM generates responses while another provides evaluation and feedback in a loop.

![Figure 5: Evaluator-Optimizer](https://developers.cloudflare.com/_astro/5_Evaluator-Optimizer.uXTWfJxj_Z8n6xm.webp) 

TypeScript

```

import { openai } from '@ai-sdk/openai';

import { generateText, generateObject } from 'ai';

import { z } from 'zod';


async function translateWithFeedback(text: string, targetLanguage: string) {

  let currentTranslation = '';

  let iterations = 0;

  const MAX_ITERATIONS = 3;


  // Initial translation

  const { text: translation } = await generateText({

    model: openai('gpt-4o-mini'), // use small model for first attempt

    system: 'You are an expert literary translator.',

    prompt: `Translate this text to ${targetLanguage}, preserving tone and cultural nuances:

    ${text}`,

  });


  currentTranslation = translation;


  // Evaluation-optimization loop

  while (iterations < MAX_ITERATIONS) {

    // Evaluate current translation

    const { object: evaluation } = await generateObject({

      model: openai('gpt-4o'), // use a larger model to evaluate

      schema: z.object({

        qualityScore: z.number().min(1).max(10),

        preservesTone: z.boolean(),

        preservesNuance: z.boolean(),

        culturallyAccurate: z.boolean(),

        specificIssues: z.array(z.string()),

        improvementSuggestions: z.array(z.string()),

      }),

      system: 'You are an expert in evaluating literary translations.',

      prompt: `Evaluate this translation:


      Original: ${text}

      Translation: ${currentTranslation}


      Consider:

      1. Overall quality

      2. Preservation of tone

      3. Preservation of nuance

      4. Cultural accuracy`,

    });


    // Check if quality meets threshold

    if (

      evaluation.qualityScore >= 8 &&

      evaluation.preservesTone &&

      evaluation.preservesNuance &&

      evaluation.culturallyAccurate

    ) {

      break;

    }


    // Generate improved translation based on feedback

    const { text: improvedTranslation } = await generateText({

      model: openai('gpt-4o'), // use a larger model

      system: 'You are an expert literary translator.',

      prompt: `Improve this translation based on the following feedback:

      ${evaluation.specificIssues.join('\n')}

      ${evaluation.improvementSuggestions.join('\n')}


      Original: ${text}

      Current Translation: ${currentTranslation}`,

    });


    currentTranslation = improvedTranslation;

    iterations++;

  }


  return {

    finalTranslation: currentTranslation,

    iterationsRequired: iterations,

  };

}


```

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/patterns/","name":"Patterns"}}]}
```

---

---
title: Model Context Protocol (MCP)
description: You can build and deploy Model Context Protocol (MCP) servers on Cloudflare.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Model Context Protocol (MCP)

You can build and deploy [Model Context Protocol (MCP) ↗](https://modelcontextprotocol.io/) servers on Cloudflare.

## What is the Model Context Protocol (MCP)?

[Model Context Protocol (MCP) ↗](https://modelcontextprotocol.io) is an open standard that connects AI systems with external applications. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various accessories, MCP provides a standardized way to connect AI agents to different services.

### MCP Terminology

* **MCP Hosts**: AI assistants (like [Claude ↗](https://claude.ai) or [Cursor ↗](https://cursor.com)), AI agents, or applications that need to access external capabilities.
* **MCP Clients**: Clients embedded within the MCP hosts that connect to MCP servers and invoke tools. Each MCP client instance has a single connection to an MCP server.
* **MCP Servers**: Applications that expose [tools](https://developers.cloudflare.com/agents/model-context-protocol/tools/), [prompts ↗](https://modelcontextprotocol.io/docs/concepts/prompts), and [resources ↗](https://modelcontextprotocol.io/docs/concepts/resources) that MCP clients can use.

### Remote vs. local MCP connections

The MCP standard supports two modes of operation:

* **Remote MCP connections**: MCP clients connect to MCP servers over the Internet, establishing a connection using [Streamable HTTP](https://developers.cloudflare.com/agents/model-context-protocol/transport/), and authorizing the MCP client access to resources on the user's account using [OAuth](https://developers.cloudflare.com/agents/model-context-protocol/authorization/).
* **Local MCP connections**: MCP clients connect to MCP servers on the same machine, using [stdio ↗](https://spec.modelcontextprotocol.io/specification/draft/basic/transports/#stdio) as a local transport method.

### Best Practices

* **Tool design**: Do not treat your MCP server as a wrapper around your full API schema. Instead, build tools that are optimized for specific user goals and reliable outcomes. Fewer, well-designed tools often outperform many granular ones, especially for agents with small context windows or tight latency budgets.
* **Scoped permissions**: Deploying several focused MCP servers, each with narrowly scoped permissions, reduces the risk of over-privileged access and makes it easier to manage and audit what each server is allowed to do.
* **Tool descriptions**: Detailed parameter descriptions help agents understand how to use your tools correctly — including what values are expected, how they affect behavior, and any important constraints. This reduces errors and improves reliability.
* **Evaluation tests**: Use evaluation tests ('evals') to measure the agent’s ability to use your tools correctly. Run these after any updates to your server or tool descriptions to catch regressions early and track improvements over time.

### Get Started

Go to the [Getting Started](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) guide to learn how to build and deploy your first remote MCP server to Cloudflare.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}}]}
```

---

---
title: Authorization
description: When building a Model Context Protocol (MCP) server, you need both a way to allow users to login (authentication) and allow them to grant the MCP client access to resources on their account (authorization).
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/authorization.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Authorization

When building a [Model Context Protocol (MCP) ↗](https://modelcontextprotocol.io) server, you need both a way to allow users to login (authentication) and allow them to grant the MCP client access to resources on their account (authorization).

The Model Context Protocol uses [a subset of OAuth 2.1 for authorization ↗](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/). OAuth allows your users to grant limited access to resources, without them having to share API keys or other credentials.

Cloudflare provides an [OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider) that implements the provider side of the OAuth 2.1 protocol, allowing you to easily add authorization to your MCP server.

You can use the OAuth Provider Library in four ways:

1. Use Cloudflare Access as an OAuth provider.
2. Integrate directly with a third-party OAuth provider, such as GitHub or Google.
3. Integrate with your own OAuth provider, including authorization-as-a-service providers you might already rely on, such as Stytch, Auth0, or WorkOS.
4. Your Worker handles authorization and authentication itself. Your MCP server, running on Cloudflare, handles the complete OAuth flow.

The following sections describe each of these options and link to runnable code examples for each.

## Authorization options

### (1) Cloudflare Access OAuth provider

Cloudflare Access allows you to add Single Sign-On (SSO) functionality to your MCP server. Users authenticate to your MCP server using a [configured identity provider](https://developers.cloudflare.com/cloudflare-one/integrations/identity-providers/) or a [one-time PIN](https://developers.cloudflare.com/cloudflare-one/integrations/identity-providers/one-time-pin/), and they are only granted access if their identity matches your [Access policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/).

To deploy an [example MCP server ↗](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-cf-access) with Cloudflare Access as the OAuth provider, refer to [Secure MCP servers with Access for SaaS](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/saas-mcp/).

### (2) Third-party OAuth Provider

The [OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider) can be configured to use a third-party OAuth provider, such as GitHub or Google. You can see a complete example of this in the [GitHub example](https://developers.cloudflare.com/agents/guides/remote-mcp-server/#add-authentication).

When you use a third-party OAuth provider, you must provide a handler to the `OAuthProvider` that implements the OAuth flow for the third-party provider.

TypeScript

```

import MyAuthHandler from "./auth-handler";


export default new OAuthProvider({

  apiRoute: "/mcp",

  // Your MCP server:

  apiHandler: MyMCPServer.serve("/mcp"),

  // Replace this handler with your own handler for authentication and authorization with the third-party provider:

  defaultHandler: MyAuthHandler,

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

});


```

Note that as [defined in the Model Context Protocol specification ↗](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/#292-flow-description) when you use a third-party OAuth provider, the MCP Server (your Worker) generates and issues its own token to the MCP client:

sequenceDiagram
    participant B as User-Agent (Browser)
    participant C as MCP Client
    participant M as MCP Server (your Worker)
    participant T as Third-Party Auth Server

    C->>M: Initial OAuth Request
    M->>B: Redirect to Third-Party /authorize
    B->>T: Authorization Request
    Note over T: User authorizes
    T->>B: Redirect to MCP Server callback
    B->>M: Authorization code
    M->>T: Exchange code for token
    T->>M: Third-party access token
    Note over M: Generate bound MCP token
    M->>B: Redirect to MCP Client callback
    B->>C: MCP authorization code
    C->>M: Exchange code for token
    M->>C: MCP access token

Read the docs for the [Workers OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider) for more details.

### (3) Bring your own OAuth Provider

If your application already implements an OAuth Provider itself, or you use an authorization-as-a-service provider, you can use this in the same way that you would use a third-party OAuth provider, described above in [(2) Third-party OAuth Provider](#2-third-party-oauth-provider).

You can use the auth provider to:

* Allow users to authenticate to your MCP server through email, social logins, SSO (single sign-on), and MFA (multi-factor authentication).
* Define scopes and permissions that directly map to your MCP tools.
* Present users with a consent page corresponding with the requested permissions.
* Enforce the permissions so that agents can only invoke permitted tools.

#### Stytch

Get started with a [remote MCP server that uses Stytch ↗](https://stytch.com/docs/guides/connected-apps/mcp-servers) to allow users to sign in with email, Google login or enterprise SSO and authorize their AI agent to view and manage their company's OKRs on their behalf. Stytch will handle restricting the scopes granted to the AI agent based on the user's role and permissions within their organization. When authorizing the MCP Client, each user will see a consent page that outlines the permissions that the agent is requesting that they are able to grant based on their role.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/mcp-stytch-b2b-okr-manager)

For more consumer use cases, deploy a remote MCP server for a To Do app that uses Stytch for authentication and MCP client authorization. Users can sign in with email and immediately access the To Do lists associated with their account, and grant access to any AI assistant to help them manage their tasks.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/mcp-stytch-consumer-todo-list)

#### Auth0

Get started with a remote MCP server that uses Auth0 to authenticate users through email, social logins, or enterprise SSO to interact with their todos and personal data through AI agents. The MCP server securely connects to API endpoints on behalf of users, showing exactly which resources the agent will be able to access once it gets consent from the user. In this implementation, access tokens are automatically refreshed during long running interactions.

To set it up, first deploy the protected API endpoint:

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-auth0/todos-api)

Then, deploy the MCP server that handles authentication through Auth0 and securely connects AI agents to your API endpoint.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-auth0/mcp-auth0-oidc)

#### WorkOS

Get started with a remote MCP server that uses WorkOS's AuthKit to authenticate users and manage the permissions granted to AI agents. In this example, the MCP server dynamically exposes tools based on the user's role and access rights. All authenticated users get access to the `add` tool, but only users who have been assigned the `image_generation` permission in WorkOS can grant the AI agent access to the image generation tool. This showcases how MCP servers can conditionally expose capabilities to AI agents based on the authenticated user's role and permission.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authkit)

#### Descope

Get started with a remote MCP server that uses [Descope ↗](https://www.descope.com/) Inbound Apps to authenticate and authorize users (for example, email, social login, SSO) to interact with their data through AI agents. Leverage Descope custom scopes to define and manage permissions for more fine-grained control.

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-server-descope-auth)

### (4) Your MCP Server handles authorization and authentication itself

Your MCP Server, using the [OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider), can handle the complete OAuth authorization flow, without any third-party involvement.

The [Workers OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider) is a Cloudflare Worker that implements a [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/), and handles incoming requests to your MCP server.

You provide your own handlers for your MCP Server's API, and authentication and authorization logic, and URI paths for the OAuth endpoints, as shown below:

TypeScript

```

export default new OAuthProvider({

  apiRoute: "/mcp",

  // Your MCP server:

  apiHandler: MyMCPServer.serve("/mcp"),

  // Your handler for authentication and authorization:

  defaultHandler: MyAuthHandler,

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

});


```

Refer to the [getting started example](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) for a complete example of the `OAuthProvider` in use, with a mock authentication flow.

The authorization flow in this case works like this:

sequenceDiagram
    participant B as User-Agent (Browser)
    participant C as MCP Client
    participant M as MCP Server (your Worker)

    C->>M: MCP Request
    M->>C: HTTP 401 Unauthorized
    Note over C: Generate code_verifier and code_challenge
    C->>B: Open browser with authorization URL + code_challenge
    B->>M: GET /authorize
    Note over M: User logs in and authorizes
    M->>B: Redirect to callback URL with auth code
    B->>C: Callback with authorization code
    C->>M: Token Request with code + code_verifier
    M->>C: Access Token (+ Refresh Token)
    C->>M: MCP Request with Access Token
    Note over C,M: Begin standard MCP message exchange

Remember — [authentication is different from authorization ↗](https://www.cloudflare.com/learning/access-management/authn-vs-authz/). Your MCP Server can handle authorization itself, while still relying on an external authentication service to first authenticate users. The [example](https://developers.cloudflare.com/agents/guides/remote-mcp-server) in getting started provides a mock authentication flow. You will need to implement your own authentication handler — either handling authentication yourself, or using an external authentication services.

## Using authentication context in tools

When a user authenticates through the OAuth Provider, their identity information is available inside your tools. How you access it depends on whether you use `McpAgent` or `createMcpHandler`.

### With McpAgent

The third type parameter on `McpAgent` defines the shape of the authentication context. Access it via `this.props` inside `init()` and tool handlers.

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


type AuthContext = {

  claims: { sub: string; name: string; email: string };

  permissions: string[];

};


export class MyMCP extends McpAgent<Env, unknown, AuthContext> {

  server = new McpServer({ name: "Auth Demo", version: "1.0.0" });


  async init() {

    this.server.tool("whoami", "Get the current user", {}, async () => ({

      content: [{ type: "text", text: `Hello, ${this.props.claims.name}!` }],

    }));

  }

}


```

### With createMcpHandler

Use `getMcpAuthContext()` to access the same information from within a tool handler. This uses `AsyncLocalStorage` under the hood.

TypeScript

```

import { createMcpHandler, getMcpAuthContext } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


function createServer() {

  const server = new McpServer({ name: "Auth Demo", version: "1.0.0" });


  server.tool("whoami", "Get the current user", {}, async () => {

    const auth = getMcpAuthContext();

    const name = (auth?.props?.name as string) ?? "anonymous";

    return {

      content: [{ type: "text", text: `Hello, ${name}!` }],

    };

  });


  return server;

}


```

## Permission-based tool access

You can control which tools are available based on user permissions. There are two approaches: check permissions inside the tool handler, or conditionally register tools.

TypeScript

```

export class MyMCP extends McpAgent<Env, unknown, AuthContext> {

  server = new McpServer({ name: "Permissions Demo", version: "1.0.0" });


  async init() {

    this.server.tool("publicTool", "Available to all users", {}, async () => ({

      content: [{ type: "text", text: "Public result" }],

    }));


    this.server.tool(

      "adminAction",

      "Requires admin permission",

      {},

      async () => {

        if (!this.props.permissions?.includes("admin")) {

          return {

            content: [

              { type: "text", text: "Permission denied: requires admin" },

            ],

          };

        }

        return {

          content: [{ type: "text", text: "Admin action completed" }],

        };

      },

    );


    if (this.props.permissions?.includes("special_feature")) {

      this.server.tool("specialTool", "Special feature", {}, async () => ({

        content: [{ type: "text", text: "Special feature result" }],

      }));

    }

  }

}


```

Checking inside the handler returns an error message to the LLM, which can explain the denial to the user. Conditionally registering tools means the LLM never sees tools the user cannot access — it cannot attempt to call them at all.

## Next steps

[ Workers OAuth Provider ](https://github.com/cloudflare/workers-oauth-provider) OAuth provider library for Workers. 

[ MCP portals ](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/mcp-portals/) Set up MCP portals to provide governance and security. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/model-context-protocol/authorization/","name":"Authorization"}}]}
```

---

---
title: MCP governance
description: Model Context Protocol (MCP) allows Large Language Models (LLMs) to interact with proprietary data and internal tools. However, as MCP adoption grows, organizations face security risks from &#34;Shadow MCP&#34;, where employees run unmanaged local MCP servers against sensitive internal resources. MCP governance means that administrators have control over which MCP servers are used in the organization, who can use them, and under what conditions.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/governance.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# MCP governance

Model Context Protocol (MCP) allows Large Language Models (LLMs) to interact with proprietary data and internal tools. However, as MCP adoption grows, organizations face security risks from "Shadow MCP", where employees run unmanaged local MCP servers against sensitive internal resources. MCP governance means that administrators have control over which MCP servers are used in the organization, who can use them, and under what conditions.

## MCP server portals

Cloudflare Access provides a centralized governance layer for MCP, allowing you to vet, authorize, and audit every interaction between users and MCP servers.

The [MCP server portal](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/mcp-portals/) serves as the administrative hub for governance. From this portal, administrators can manage both third-party and internal MCP servers and define policies for:

* **Identity**: Which users or groups are authorized to access specific MCP servers.
* **Conditions**: The security posture (for example, device health or location) required for access.
* **Scope**: Which specific tools within an MCP server are authorized for use.

Cloudflare Access logs MCP server requests and tool executions made through the portal, providing administrators with visibility into MCP usage across the organization.

## Remote MCP servers

To maintain a modern security posture, Cloudflare recommends the use of [remote MCP servers](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) over local installations. Running MCP servers locally introduces risks similar to unmanaged [shadow IT ↗](https://www.cloudflare.com/learning/access-management/what-is-shadow-it/), making it difficult to audit data flow or verify the integrity of the server code. Remote MCP servers give administrators visibility into what servers are being used, along with the ability to control who access them and what tools are authorized for employee use.

You can [build your remote MCP servers](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) directly on Cloudflare Workers. When both your [MCP server portal](#mcp-server-portals) and remote MCP servers run on Cloudflare's network, requests stay on the same infrastructure, minimizing latency and maximizing performance.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/model-context-protocol/governance/","name":"MCP governance"}}]}
```

---

---
title: MCP server portals
description: Centralize multiple MCP servers onto a single endpoint and customize the tools, prompts, and resources available to users.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/mcp-portal.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# MCP server portals

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/model-context-protocol/mcp-portal/","name":"MCP server portals"}}]}
```

---

---
title: Cloudflare's own MCP servers
description: Cloudflare runs a catalog of managed remote MCP servers which you can connect to using OAuth on clients like Claude, Windsurf, our own AI Playground or any SDK that supports MCP.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/mcp-servers-for-cloudflare.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Cloudflare's own MCP servers

Cloudflare runs a catalog of managed remote MCP servers which you can connect to using OAuth on clients like [Claude ↗](https://modelcontextprotocol.io/quickstart/user), [Windsurf ↗](https://docs.windsurf.com/windsurf/cascade/mcp), our own [AI Playground ↗](https://playground.ai.cloudflare.com/) or any [SDK that supports MCP ↗](https://github.com/cloudflare/agents/tree/main/packages/agents/src/mcp).

These MCP servers allow your MCP client to read configurations from your account, process information, make suggestions based on data, and even make those suggested changes for you. All of these actions can happen across Cloudflare's many services including application development, security and performance. They support both the `streamable-http` transport via `/mcp` and the `sse` transport (deprecated) via `/sse`.

## Cloudflare API MCP server

The [Cloudflare API MCP server ↗](https://github.com/cloudflare/mcp) provides access to the entire [Cloudflare API](https://developers.cloudflare.com/api/) — over 2,500 endpoints across DNS, Workers, R2, Zero Trust, and every other product — through just two tools: `search()` and `execute()`.

It uses [Codemode](https://developers.cloudflare.com/agents/api-reference/codemode/), a technique where the model writes JavaScript against a typed representation of the OpenAPI spec and the Cloudflare API client, rather than loading individual tool definitions for each endpoint. The generated code runs inside an isolated [Dynamic Worker](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/) sandbox.

This approach uses approximately 1,000 tokens regardless of how many API endpoints exist. An equivalent MCP server that exposed every endpoint as a native tool would consume over 1 million tokens — more than the entire context window of most foundation models.

| Approach                          | Tools | Token cost  |
| --------------------------------- | ----- | ----------- |
| Native MCP (full schemas)         | 2,594 | \~1,170,000 |
| Native MCP (required params only) | 2,594 | \~244,000   |
| Codemode                          | 2     | \~1,000     |

### Connect to the Cloudflare API MCP server

Add the following configuration to your MCP client:

```

{

  "mcpServers": {

    "cloudflare-api": {

      "url": "https://mcp.cloudflare.com/mcp"

    }

  }

}


```

When you connect, you will be redirected to Cloudflare to authorize via OAuth and select the permissions to grant to your agent.

For CI/CD or automation, you can create a [Cloudflare API token ↗](https://dash.cloudflare.com/profile/api-tokens) with the permissions you need and pass it as a bearer token in the `Authorization` header. Both user tokens and account tokens are supported.

For more information, refer to the [Cloudflare MCP repository ↗](https://github.com/cloudflare/mcp).

### Install via agent and IDE plugins

You can install the [Cloudflare Skills plugin ↗](https://github.com/cloudflare/skills), which bundles the Cloudflare MCP servers alongside contextual skills and slash commands for building on Cloudflare. The plugin works with any agent that supports the Agent Skills standard, including Claude Code, OpenCode, OpenAI Codex, and Pi.

#### Claude Code

Install using the [plugin marketplace ↗](https://code.claude.com/docs/en/discover-plugins#add-from-github):

```

/plugin marketplace add cloudflare/skills


```

#### Cursor

Install from the **Cursor Marketplace**, or add manually via **Settings** \> **Rules** \> **Add Rule** \> **Remote Rule (Github)** with `cloudflare/skills`.

#### npx skills

Install using the [npx skills ↗](https://skills.sh) CLI:

Terminal window

```

npx skills add https://github.com/cloudflare/skills


```

#### Clone or copy

Clone the [cloudflare/skills ↗](https://github.com/cloudflare/skills) repository and copy the skill folders into the appropriate directory for your agent:

| Agent        | Skill directory             | Docs                                                                                                   |
| ------------ | --------------------------- | ------------------------------------------------------------------------------------------------------ |
| Claude Code  | \~/.claude/skills/          | [Claude Code skills ↗](https://code.claude.com/docs/en/skills)                                         |
| Cursor       | \~/.cursor/skills/          | [Cursor skills ↗](https://cursor.com/docs/context/skills)                                              |
| OpenCode     | \~/.config/opencode/skills/ | [OpenCode skills ↗](https://opencode.ai/docs/skills/)                                                  |
| OpenAI Codex | \~/.codex/skills/           | [OpenAI Codex skills ↗](https://developers.openai.com/codex/skills/)                                   |
| Pi           | \~/.pi/agent/skills/        | [Pi coding agent skills ↗](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent#skills) |

## Product-specific MCP servers

In addition to the Cloudflare API MCP server, Cloudflare provides product-specific MCP servers for targeted use cases:

| Server Name                                                                                                               | Description                                                                                     | Server URL                                   |
| ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------- |
| [Documentation server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/docs-vectorize)               | Get up to date reference information on Cloudflare                                              | https://docs.mcp.cloudflare.com/mcp          |
| [Workers Bindings server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/workers-bindings)          | Build Workers applications with storage, AI, and compute primitives                             | https://bindings.mcp.cloudflare.com/mcp      |
| [Workers Builds server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/workers-builds)              | Get insights and manage your Cloudflare Workers Builds                                          | https://builds.mcp.cloudflare.com/mcp        |
| [Observability server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/workers-observability)        | Debug and get insight into your application's logs and analytics                                | https://observability.mcp.cloudflare.com/mcp |
| [Radar server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/radar)                                | Get global Internet traffic insights, trends, URL scans, and other utilities                    | https://radar.mcp.cloudflare.com/mcp         |
| [Container server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/sandbox-container)                | Spin up a sandbox development environment                                                       | https://containers.mcp.cloudflare.com/mcp    |
| [Browser rendering server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/browser-rendering)        | Fetch web pages, convert them to markdown and take screenshots                                  | https://browser.mcp.cloudflare.com/mcp       |
| [Logpush server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/logpush)                            | Get quick summaries for Logpush job health                                                      | https://logs.mcp.cloudflare.com/mcp          |
| [AI Gateway server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/ai-gateway)                      | Search your logs, get details about the prompts and responses                                   | https://ai-gateway.mcp.cloudflare.com/mcp    |
| [AI Search server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/autorag)                          | List and search documents on your AI Searches                                                   | https://autorag.mcp.cloudflare.com/mcp       |
| [Audit Logs server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/auditlogs)                       | Query audit logs and generate reports for review                                                | https://auditlogs.mcp.cloudflare.com/mcp     |
| [DNS Analytics server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/dns-analytics)                | Optimize DNS performance and debug issues based on current set up                               | https://dns-analytics.mcp.cloudflare.com/mcp |
| [Digital Experience Monitoring server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/dex-analysis) | Get quick insight on critical applications for your organization                                | https://dex.mcp.cloudflare.com/mcp           |
| [Cloudflare One CASB server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/cloudflare-one-casb)    | Quickly identify any security misconfigurations for SaaS applications to safeguard users & data | https://casb.mcp.cloudflare.com/mcp          |
| [GraphQL server ↗](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/graphql/)                           | Get analytics data using Cloudflare's GraphQL API                                               | https://graphql.mcp.cloudflare.com/mcp       |
| [Agents SDK Documentation server ↗](https://github.com/cloudflare/agents/tree/main/site/agents)                           | Token-efficient search of the Cloudflare Agents SDK documentation                               | https://agents.cloudflare.com/mcp            |

Check the [GitHub page ↗](https://github.com/cloudflare/mcp-server-cloudflare) to learn how to use Cloudflare's remote MCP servers with different MCP clients.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/model-context-protocol/mcp-servers-for-cloudflare/","name":"Cloudflare's own MCP servers"}}]}
```

---

---
title: Tools
description: MCP tools are functions that an MCP server exposes for clients to call. When an LLM decides it needs to take an action — look up data, run a calculation, call an API — it invokes a tool. The MCP server executes the tool and returns the result.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/tools.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Tools

MCP tools are functions that an [MCP server](https://developers.cloudflare.com/agents/model-context-protocol/) exposes for clients to call. When an LLM decides it needs to take an action — look up data, run a calculation, call an API — it invokes a tool. The MCP server executes the tool and returns the result.

Tools are defined using the `@modelcontextprotocol/sdk` package. The Agents SDK handles transport and lifecycle; the tool definitions are the same regardless of whether you use [createMcpHandler](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/) or [McpAgent](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/).

## Defining tools

Use `server.tool()` to register a tool on an `McpServer` instance. Each tool has a name, a description (used by the LLM to decide when to call it), an input schema defined with [Zod ↗](https://zod.dev), and a handler function.

* [  JavaScript ](#tab-panel-3002)
* [  TypeScript ](#tab-panel-3003)

JavaScript

```

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({ name: "Math", version: "1.0.0" });


  server.tool(

    "add",

    "Add two numbers together",

    { a: z.number(), b: z.number() },

    async ({ a, b }) => ({

      content: [{ type: "text", text: String(a + b) }],

    }),

  );


  return server;

}


```

TypeScript

```

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({ name: "Math", version: "1.0.0" });


  server.tool(

    "add",

    "Add two numbers together",

    { a: z.number(), b: z.number() },

    async ({ a, b }) => ({

      content: [{ type: "text", text: String(a + b) }],

    }),

  );


  return server;

}


```

The tool handler receives the validated input and must return an object with a `content` array. Each content item has a `type` (typically `"text"`) and the corresponding data.

## Tool results

Tool results are returned as an array of content parts. The most common type is `text`, but you can also return images and embedded resources.

* [  JavaScript ](#tab-panel-3004)
* [  TypeScript ](#tab-panel-3005)

JavaScript

```

server.tool(

  "lookup",

  "Look up a user by ID",

  { userId: z.string() },

  async ({ userId }) => {

    const user = await db.getUser(userId);


    if (!user) {

      return {

        isError: true,

        content: [{ type: "text", text: `User ${userId} not found` }],

      };

    }


    return {

      content: [{ type: "text", text: JSON.stringify(user, null, 2) }],

    };

  },

);


```

TypeScript

```

server.tool(

  "lookup",

  "Look up a user by ID",

  { userId: z.string() },

  async ({ userId }) => {

    const user = await db.getUser(userId);


    if (!user) {

      return {

        isError: true,

        content: [{ type: "text", text: `User ${userId} not found` }],

      };

    }


    return {

      content: [{ type: "text", text: JSON.stringify(user, null, 2) }],

    };

  },

);


```

Set `isError: true` to signal that the tool call failed. The LLM receives the error message and can decide how to proceed.

## Tool descriptions

The `description` parameter is critical — it is what the LLM reads to decide whether and when to call your tool. Write descriptions that are:

* **Specific** about what the tool does: "Get the current weather for a city" is better than "Weather tool"
* **Clear about inputs**: "Requires a city name as a string" helps the LLM format the call correctly
* **Honest about limitations**: "Only supports US cities" prevents the LLM from calling it with unsupported inputs

## Input validation with Zod

Tool inputs are defined as Zod schemas and validated automatically before the handler runs. Use Zod's `.describe()` method to give the LLM context about each parameter.

* [  JavaScript ](#tab-panel-3008)
* [  TypeScript ](#tab-panel-3009)

JavaScript

```

server.tool(

  "search",

  "Search for documents by query",

  {

    query: z.string().describe("The search query"),

    limit: z

      .number()

      .min(1)

      .max(100)

      .default(10)

      .describe("Maximum number of results to return"),

    category: z

      .enum(["docs", "blog", "api"])

      .optional()

      .describe("Filter by content category"),

  },

  async ({ query, limit, category }) => {

    const results = await searchIndex(query, { limit, category });

    return {

      content: [{ type: "text", text: JSON.stringify(results) }],

    };

  },

);


```

TypeScript

```

server.tool(

  "search",

  "Search for documents by query",

  {

    query: z.string().describe("The search query"),

    limit: z

      .number()

      .min(1)

      .max(100)

      .default(10)

      .describe("Maximum number of results to return"),

    category: z

      .enum(["docs", "blog", "api"])

      .optional()

      .describe("Filter by content category"),

  },

  async ({ query, limit, category }) => {

    const results = await searchIndex(query, { limit, category });

    return {

      content: [{ type: "text", text: JSON.stringify(results) }],

    };

  },

);


```

## Using tools with `createMcpHandler`

For stateless MCP servers, define tools inside a factory function and pass the server to [createMcpHandler](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/):

* [  JavaScript ](#tab-panel-3006)
* [  TypeScript ](#tab-panel-3007)

JavaScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({ name: "My Tools", version: "1.0.0" });


  server.tool("ping", "Check if the server is alive", {}, async () => ({

    content: [{ type: "text", text: "pong" }],

  }));


  return server;

}


export default {

  fetch: (request, env, ctx) => {

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

};


```

TypeScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({ name: "My Tools", version: "1.0.0" });


  server.tool("ping", "Check if the server is alive", {}, async () => ({

    content: [{ type: "text", text: "pong" }],

  }));


  return server;

}


export default {

  fetch: (request: Request, env: Env, ctx: ExecutionContext) => {

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

} satisfies ExportedHandler<Env>;


```

## Using tools with `McpAgent`

For stateful MCP servers, define tools in the `init()` method of an [McpAgent](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/). Tools have access to the agent instance via `this`, which means they can read and write state.

* [  JavaScript ](#tab-panel-3010)
* [  TypeScript ](#tab-panel-3011)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "Stateful Tools", version: "1.0.0" });


  async init() {

    this.server.tool(

      "incrementCounter",

      "Increment and return a counter",

      {},

      async () => {

        const count = (this.state?.count ?? 0) + 1;

        this.setState({ count });

        return {

          content: [{ type: "text", text: `Counter: ${count}` }],

        };

      },

    );

  }

}


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "Stateful Tools", version: "1.0.0" });


  async init() {

    this.server.tool(

      "incrementCounter",

      "Increment and return a counter",

      {},

      async () => {

        const count = (this.state?.count ?? 0) + 1;

        this.setState({ count });

        return {

          content: [{ type: "text", text: `Counter: ${count}` }],

        };

      },

    );

  }

}


```

## Next steps

[ Build a remote MCP server ](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) Step-by-step guide to deploying an MCP server on Cloudflare. 

[ createMcpHandler API ](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/) Reference for stateless MCP servers. 

[ McpAgent API ](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) Reference for stateful MCP servers. 

[ MCP authorization ](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) Add OAuth authentication to your MCP server. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/model-context-protocol/tools/","name":"Tools"}}]}
```

---

---
title: Transport
description: The Model Context Protocol (MCP) specification defines two standard transport mechanisms for communication between clients and servers:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/model-context-protocol/transport.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Transport

The Model Context Protocol (MCP) specification defines two standard [transport mechanisms ↗](https://spec.modelcontextprotocol.io/specification/draft/basic/transports/) for communication between clients and servers:

1. **stdio** — Communication over standard in and standard out, designed for local MCP connections.
2. **Streamable HTTP** — The standard transport method for remote MCP connections, [introduced ↗](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) in March 2025\. It uses a single HTTP endpoint for bidirectional messaging.

Note

Server-Sent Events (SSE) was previously used for remote MCP connections but has been deprecated in favor of Streamable HTTP. If you need SSE support for legacy clients, use the [McpAgent](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) class.

MCP servers built with the [Agents SDK](https://developers.cloudflare.com/agents) use [createMcpHandler](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/) to handle Streamable HTTP transport.

## Implementing remote MCP transport

Use [createMcpHandler](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/) to create an MCP server that handles Streamable HTTP transport. This is the recommended approach for new MCP servers.

#### Get started quickly

You can use the "Deploy to Cloudflare" button to create a remote MCP server.

[![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agents/tree/main/examples/mcp-worker)

#### Remote MCP server (without authentication)

Create an MCP server using `createMcpHandler`. View the [complete example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker).

* [  JavaScript ](#tab-panel-3022)
* [  TypeScript ](#tab-panel-3023)

JavaScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({

    name: "My MCP Server",

    version: "1.0.0",

  });


  server.registerTool(

    "hello",

    {

      description: "Returns a greeting message",

      inputSchema: { name: z.string().optional() },

    },

    async ({ name }) => {

      return {

        content: [{ text: `Hello, ${name ?? "World"}!`, type: "text" }],

      };

    },

  );


  return server;

}


export default {

  fetch: (request, env, ctx) => {

    // Create a new server instance per request

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

};


```

TypeScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({

    name: "My MCP Server",

    version: "1.0.0",

  });


  server.registerTool(

    "hello",

    {

      description: "Returns a greeting message",

      inputSchema: { name: z.string().optional() },

    },

    async ({ name }) => {

      return {

        content: [{ text: `Hello, ${name ?? "World"}!`, type: "text" }],

      };

    },

  );


  return server;

}


export default {

  fetch: (request: Request, env: Env, ctx: ExecutionContext) => {

    // Create a new server instance per request

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

} satisfies ExportedHandler<Env>;


```

#### MCP server with authentication

If your MCP server implements authentication & authorization using the [Workers OAuth Provider ↗](https://github.com/cloudflare/workers-oauth-provider) library, use `createMcpHandler` with the `apiRoute` and `apiHandler` properties. View the [complete example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated).

* [  JavaScript ](#tab-panel-3012)
* [  TypeScript ](#tab-panel-3013)

JavaScript

```

export default new OAuthProvider({

  apiRoute: "/mcp",

  apiHandler: {

    fetch: (request, env, ctx) => {

      // Create a new server instance per request

      const server = createServer();

      return createMcpHandler(server)(request, env, ctx);

    },

  },

  // ... other OAuth configuration

});


```

TypeScript

```

export default new OAuthProvider({

  apiRoute: "/mcp",

  apiHandler: {

    fetch: (request: Request, env: Env, ctx: ExecutionContext) => {

      // Create a new server instance per request

      const server = createServer();

      return createMcpHandler(server)(request, env, ctx);

    },

  },

  // ... other OAuth configuration

});


```

### Stateful MCP servers

If your MCP server needs to maintain state across requests, use `createMcpHandler` with a `WorkerTransport` inside an [Agent](https://developers.cloudflare.com/agents/) class. This allows you to persist session state in Durable Object storage and use advanced MCP features like [elicitation ↗](https://modelcontextprotocol.io/specification/draft/client/elicitation) and [sampling ↗](https://modelcontextprotocol.io/specification/draft/client/sampling).

See [Stateful MCP Servers](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api#stateful-mcp-servers) for implementation details.

## RPC transport

The **RPC transport** is designed for internal applications where your MCP server and agent are both running on Cloudflare — they can even run in the same Worker. It sends JSON-RPC messages directly over Cloudflare's [RPC bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) without going over the public internet.

* **Faster** — no network overhead, direct function calls between Durable Objects
* **Simpler** — no HTTP endpoints, no connection management
* **Internal only** — perfect for agents calling MCP servers within the same Worker

RPC transport does not support authentication. Use Streamable HTTP for external connections that require OAuth.

### Connecting an Agent to an McpAgent via RPC

#### 1\. Define your MCP server

Create your `McpAgent` with the tools you want to expose:

* [  JavaScript ](#tab-panel-3024)
* [  TypeScript ](#tab-panel-3025)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "MyMCP", version: "1.0.0" });

  initialState = { counter: 0 };


  async init() {

    this.server.tool(

      "add",

      "Add to the counter",

      { amount: z.number() },

      async ({ amount }) => {

        this.setState({ counter: this.state.counter + amount });

        return {

          content: [

            {

              type: "text",

              text: `Added ${amount}, total is now ${this.state.counter}`,

            },

          ],

        };

      },

    );

  }

}


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


type State = { counter: number };


export class MyMCP extends McpAgent<Env, State> {

  server = new McpServer({ name: "MyMCP", version: "1.0.0" });

  initialState: State = { counter: 0 };


  async init() {

    this.server.tool(

      "add",

      "Add to the counter",

      { amount: z.number() },

      async ({ amount }) => {

        this.setState({ counter: this.state.counter + amount });

        return {

          content: [

            {

              type: "text",

              text: `Added ${amount}, total is now ${this.state.counter}`,

            },

          ],

        };

      },

    );

  }

}


```

#### 2\. Connect your Agent to the MCP server

In your `Agent`, call `addMcpServer()` with the Durable Object binding in `onStart()`:

* [  JavaScript ](#tab-panel-3018)
* [  TypeScript ](#tab-panel-3019)

JavaScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";


export class Chat extends AIChatAgent {

  async onStart() {

    // Pass the DO namespace binding directly

    await this.addMcpServer("my-mcp", this.env.MyMCP);

  }


  async onChatMessage(onFinish) {

    const allTools = this.mcp.getAITools();


    const result = streamText({

      model,

      tools: allTools,

      // ...

    });


    return createUIMessageStreamResponse({ stream: result });

  }

}


```

TypeScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";


export class Chat extends AIChatAgent<Env> {

  async onStart(): Promise<void> {

    // Pass the DO namespace binding directly

    await this.addMcpServer("my-mcp", this.env.MyMCP);

  }


  async onChatMessage(onFinish) {

    const allTools = this.mcp.getAITools();


    const result = streamText({

      model,

      tools: allTools,

      // ...

    });


    return createUIMessageStreamResponse({ stream: result });

  }

}


```

RPC connections are automatically restored after Durable Object hibernation, just like HTTP connections. The binding name and props are persisted to storage so the connection can be re-established without any extra code.

For RPC transport, if `addMcpServer` is called with a name that already has an active connection, the existing connection is returned instead of creating a duplicate. For HTTP transport, deduplication matches on both server name and URL (refer to [MCP Client API](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/) for details). This makes it safe to call in `onStart()`.

#### 3\. Configure Durable Object bindings

In your `wrangler.jsonc`, define bindings for both Durable Objects:

```

{

  "durable_objects": {

    "bindings": [

      { "name": "Chat", "class_name": "Chat" },

      { "name": "MyMCP", "class_name": "MyMCP" }

    ]

  },

  "migrations": [

    {

      "new_sqlite_classes": ["MyMCP", "Chat"],

      "tag": "v1"

    }

  ]

}


```

#### 4\. Set up your Worker fetch handler

Route requests to your Chat agent:

* [  JavaScript ](#tab-panel-3016)
* [  TypeScript ](#tab-panel-3017)

JavaScript

```

import { routeAgentRequest } from "agents";


export default {

  async fetch(request, env, ctx) {

    const url = new URL(request.url);


    // Optionally expose the MCP server via HTTP as well

    if (url.pathname.startsWith("/mcp")) {

      return MyMCP.serve("/mcp").fetch(request, env, ctx);

    }


    const response = await routeAgentRequest(request, env);

    if (response) return response;


    return new Response("Not found", { status: 404 });

  },

};


```

TypeScript

```

import { routeAgentRequest } from "agents";


export default {

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    const url = new URL(request.url);


    // Optionally expose the MCP server via HTTP as well

    if (url.pathname.startsWith("/mcp")) {

      return MyMCP.serve("/mcp").fetch(request, env, ctx);

    }


    const response = await routeAgentRequest(request, env);

    if (response) return response;


    return new Response("Not found", { status: 404 });

  },

} satisfies ExportedHandler<Env>;


```

### Passing props to the MCP server

Since RPC transport does not have an OAuth flow, you can pass user context directly as props:

* [  JavaScript ](#tab-panel-3014)
* [  TypeScript ](#tab-panel-3015)

JavaScript

```

await this.addMcpServer("my-mcp", this.env.MyMCP, {

  props: { userId: "user-123", role: "admin" },

});


```

TypeScript

```

await this.addMcpServer("my-mcp", this.env.MyMCP, {

  props: { userId: "user-123", role: "admin" },

});


```

Your `McpAgent` can then access these props:

* [  JavaScript ](#tab-panel-3020)
* [  TypeScript ](#tab-panel-3021)

JavaScript

```

export class MyMCP extends McpAgent {

  async init() {

    this.server.tool("whoami", "Get current user info", {}, async () => {

      const userId = this.props?.userId || "anonymous";

      const role = this.props?.role || "guest";


      return {

        content: [{ type: "text", text: `User ID: ${userId}, Role: ${role}` }],

      };

    });

  }

}


```

TypeScript

```

export class MyMCP extends McpAgent<

  Env,

  State,

  { userId?: string; role?: string }

> {

  async init() {

    this.server.tool("whoami", "Get current user info", {}, async () => {

      const userId = this.props?.userId || "anonymous";

      const role = this.props?.role || "guest";


      return {

        content: [

          { type: "text", text: `User ID: ${userId}, Role: ${role}` },

        ],

      };

    });

  }

}


```

Props are type-safe (TypeScript extracts the Props type from your `McpAgent` generic), persistent (stored in Durable Object storage), and available immediately before any tool calls are made.

### Configuring RPC transport server timeout

The RPC transport has a configurable timeout for waiting for tool responses. By default, the server waits **60 seconds** for a tool handler to respond. You can customize this by overriding `getRpcTransportOptions()` in your `McpAgent`:

* [  JavaScript ](#tab-panel-3026)
* [  TypeScript ](#tab-panel-3027)

JavaScript

```

export class MyMCP extends McpAgent {

  server = new McpServer({ name: "MyMCP", version: "1.0.0" });


  getRpcTransportOptions() {

    return { timeout: 120000 }; // 2 minutes

  }


  async init() {

    this.server.tool(

      "long-running-task",

      "A tool that takes a while",

      { input: z.string() },

      async ({ input }) => {

        await longRunningOperation(input);

        return {

          content: [{ type: "text", text: "Task completed" }],

        };

      },

    );

  }

}


```

TypeScript

```

export class MyMCP extends McpAgent<Env, State> {

  server = new McpServer({ name: "MyMCP", version: "1.0.0" });


  protected getRpcTransportOptions() {

    return { timeout: 120000 }; // 2 minutes

  }


  async init() {

    this.server.tool(

      "long-running-task",

      "A tool that takes a while",

      { input: z.string() },

      async ({ input }) => {

        await longRunningOperation(input);

        return {

          content: [{ type: "text", text: "Task completed" }],

        };

      },

    );

  }

}


```

## Choosing a transport

| Transport           | Use when                              | Pros                                     | Cons                                  |
| ------------------- | ------------------------------------- | ---------------------------------------- | ------------------------------------- |
| **Streamable HTTP** | External MCP servers, production apps | Standard protocol, secure, supports auth | Slight network overhead               |
| **RPC**             | Internal agents on Cloudflare         | Fastest, simplest setup                  | No auth, Durable Object bindings only |
| **SSE**             | Legacy compatibility                  | Backwards compatible                     | Deprecated, use Streamable HTTP       |

### Migrating from McpAgent

If you have an existing MCP server using the `McpAgent` class:

* **Not using state?** Replace your `McpAgent` class with `McpServer` from `@modelcontextprotocol/sdk` and use `createMcpHandler(server)` in a Worker `fetch` handler.
* **Using state?** Use `createMcpHandler` with a `WorkerTransport` inside an [Agent](https://developers.cloudflare.com/agents/) class. See [Stateful MCP Servers](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api#stateful-mcp-servers) for details.
* **Need SSE support?** Continue using `McpAgent` with `serveSSE()` for legacy client compatibility. See the [McpAgent API reference](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/).

### Testing with MCP clients

You can test your MCP server using an MCP client that supports remote connections, or use [mcp-remote ↗](https://www.npmjs.com/package/mcp-remote), an adapter that lets MCP clients that only support local connections work with remote MCP servers.

Follow [this guide](https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/) for instructions on how to connect to your remote MCP server to Claude Desktop, Cursor, Windsurf, and other MCP clients.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/model-context-protocol/","name":"Model Context Protocol (MCP)"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/model-context-protocol/transport/","name":"Transport"}}]}
```

---

---
title: Agentic Payments
description: Let AI agents pay for services programmatically using payment protocols like MPP and x402 with Cloudflare's Agents SDK.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Agentic Payments

AI agents need to discover, pay for, and consume resources and services programmatically. Traditional onboarding requires account creation, a payment method, and an API key before an agent can pay for a service. Agentic payments let AI agents purchase resources and services directly through the HTTP `402 Payment Required` response code.

Cloudflare's [Agents SDK](https://developers.cloudflare.com/agents/) supports agentic payments through two protocols built on the HTTP `402 Payment Required` status code: **x402** and **Machine Payments Protocol (MPP)**. Both follow the same core flow:

1. A client requests a resource or calls a tool.
2. The server responds with `402` and a payment challenge describing what to pay, how much, and where.
3. The client fulfills the payment and retries the request with a payment credential.
4. The server verifies the payment (optionally through a facilitator service) and returns the resource along with a receipt.

No accounts, sessions, or pre-shared API keys are required. Agents handle the entire exchange programmatically.

## x402 and Machine Payments Protocol

### x402

[x402 ↗](https://www.x402.org/) is a payment standard created by Coinbase. It uses on-chain stablecoin payments (USDC on Base, Ethereum, Solana, and other networks) and defines three HTTP headers — `PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, and `PAYMENT-RESPONSE` — to carry challenges, credentials, and receipts. Servers can offload verification and settlement to a **facilitator** service so they do not need direct blockchain connectivity. It is governed by Coinbase and Cloudflare, two of the founding members of the x402 Foundation.

The Agents SDK provides first-class x402 integration:

* **Server-side**: `withX402` and `paidTool` for MCP servers, plus `x402-hono` middleware for HTTP Workers.
* **Client-side**: `withX402Client` wraps MCP client connections with automatic 402 handling and optional human-in-the-loop confirmation.

### Machine Payments Protocol

[Machine Payments Protocol (MPP) ↗](https://mpp.dev) is a protocol co-authored by Tempo Labs and Stripe. It extends the HTTP `402` pattern with a formal `WWW-Authenticate: Payment` / `Authorization: Payment` header scheme and is on the IETF standards track.

MPP supports multiple payment methods beyond blockchain — including cards (via Stripe), Bitcoin Lightning, and stablecoins — and introduces **sessions** for streaming and pay-as-you-go use cases with sub-millisecond latency and sub-cent costs. MPP is backwards-compatible with x402: MPP clients can consume existing x402 services without modification.

## Charge for resources

[ HTTP content (x402) ](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-http-content/) Gate APIs, web pages, and files with a Worker proxy 

[ HTTP content (MPP) ](https://developers.cloudflare.com/agents/agentic-payments/mpp/charge-for-http-content/) Gate APIs, web pages, and files with a Worker proxy 

## Related

* [x402.org ↗](https://x402.org) — x402 protocol specification
* [mpp.dev ↗](https://mpp.dev) — MPP protocol specification
* [Pay Per Crawl](https://developers.cloudflare.com/ai-crawl-control/features/pay-per-crawl/) — Cloudflare-native monetization for web content
* [x402 examples ↗](https://github.com/cloudflare/agents/tree/main/examples) — Complete working code

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}}]}
```

---

---
title: MPP (Machine Payments Protocol)
description: Accept and make payments using the Machine Payments Protocol (MPP) on Cloudflare Workers.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/mpp/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# MPP (Machine Payments Protocol)

[Machine Payments Protocol (MPP) ↗](https://mpp.dev) is a protocol for machine-to-machine payments, co-authored by [Tempo Labs ↗](https://tempo.xyz) and [Stripe ↗](https://stripe.com). It standardizes the HTTP `402 Payment Required` status code with a formal authentication scheme proposed to the [IETF ↗](https://paymentauth.org). MPP gives agents, apps, and humans a single interface to pay for any service in the same HTTP request.

MPP is payment-method agnostic. A single endpoint can accept stablecoins (Tempo), credit cards (Stripe), or Bitcoin (Lightning).

## How it works

1. A client requests a resource — `GET /resource`.
2. The server returns `402 Payment Required` with a `WWW-Authenticate: Payment` header containing a payment challenge.
3. The client fulfills the payment — signs a transaction, pays an invoice, or completes a card payment.
4. The client retries the request with an `Authorization: Payment` header containing a payment credential.
5. The server verifies the payment and returns the resource with a `Payment-Receipt` header.

## Payment methods

MPP supports multiple payment methods through a single protocol:

| Method                                                   | Description                                                                  | Status     |
| -------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------- |
| [Tempo ↗](https://mpp.dev/payment-methods/tempo)         | Stablecoin payments on the Tempo blockchain with sub-second settlement       | Production |
| [Stripe ↗](https://mpp.dev/payment-methods/stripe)       | Cards, wallets, and other Stripe-supported methods via Shared Payment Tokens | Production |
| [Lightning ↗](https://mpp.dev/payment-methods/lightning) | Bitcoin payments over the Lightning Network                                  | Available  |
| [Card ↗](https://mpp.dev/payment-methods/card)           | Card payments via encrypted network tokens                                   | Available  |
| [Custom ↗](https://mpp.dev/payment-methods/custom)       | Build your own payment method using the MPP SDK                              | Available  |

Servers can offer multiple methods simultaneously. Clients choose the method that works for them.

## Payment intents

MPP defines two payment intents:

* **`charge`** — A one-time payment that settles immediately. Use for per-request billing.
* **`session`** — A streaming payment over a payment channel. Use for pay-as-you-go or per-token billing with sub-cent costs and sub-millisecond latency.

## Compatibility with x402

MPP is backwards-compatible with [x402](https://developers.cloudflare.com/agents/agentic-payments/x402/). The core x402 `exact` payment flows map directly onto MPP's `charge` intent, so MPP clients can consume existing x402 services without modification.

## Charge for resources

[ HTTP content ](https://developers.cloudflare.com/agents/agentic-payments/mpp/charge-for-http-content/) Gate APIs, web pages, and files with MPP middleware 

## SDKs

MPP provides official SDKs in three languages:

| SDK        | Package | Install           |
| ---------- | ------- | ----------------- |
| TypeScript | mppx    | npm install mppx  |
| Python     | pympp   | pip install pympp |
| Rust       | mpp-rs  | cargo add mpp     |

The TypeScript SDK includes framework middleware for [Hono ↗](https://mpp.dev/sdk/typescript/middlewares/hono), [Express ↗](https://mpp.dev/sdk/typescript/middlewares/express), [Next.js ↗](https://mpp.dev/sdk/typescript/middlewares/nextjs), and [Elysia ↗](https://mpp.dev/sdk/typescript/middlewares/elysia), as well as a [CLI ↗](https://mpp.dev/sdk/typescript/cli) for testing paid endpoints.

## Related

* [mpp.dev ↗](https://mpp.dev) — Protocol documentation and quickstart guides
* [IETF specification ↗](https://paymentauth.org) — Full Payment HTTP Authentication Scheme specification
* [Pay Per Crawl](https://developers.cloudflare.com/ai-crawl-control/features/pay-per-crawl/) — Cloudflare-native monetization for web content

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/mpp/","name":"MPP (Machine Payments Protocol)"}}]}
```

---

---
title: Charge for HTTP content
description: Gate HTTP endpoints with MPP payments using the mpp-proxy template on Cloudflare Workers.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/mpp/charge-for-http-content.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Charge for HTTP content

The [mpp-proxy ↗](https://github.com/cloudflare/mpp-proxy) template is a Cloudflare Worker that sits in front of any HTTP backend. When a request hits a protected route, the proxy returns a `402` response with an MPP payment challenge. After the client pays, the proxy verifies the payment, forwards the request to your origin, and issues a 1-hour session cookie.

Deploy the mpp-proxy template to your Cloudflare account:

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/mpp-proxy)

## Prerequisites

* A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up)
* An HTTP backend to gate
* A wallet address to receive payments

## Configuration

Define protected routes in `wrangler.jsonc`:

```

{

  "vars": {

    "PAY_TO": "0xYourWalletAddress",

    "TEMPO_TESTNET": false,

    "PAYMENT_CURRENCY": "0x20c000000000000000000000b9537d11c60e8b50",

    "PROTECTED_PATTERNS": [

      {

        "pattern": "/premium/*",

        "amount": "0.01",

        "description": "Access to premium content for 1 hour"

      }

    ]

  }

}


```

Note

Set `TEMPO_TESTNET` to `true` and `PAYMENT_CURRENCY` to `0x20c0000000000000000000000000000000000000` for testnet development.

## Selective gating with Bot Management

With [Bot Management](https://developers.cloudflare.com/bots/), the proxy can charge crawlers while keeping the site free for humans:

```

{

  "pattern": "/content/*",

  "amount": "0.25",

  "description": "Content access for 1 hour",

  "bot_score_threshold": 30,

  "except_detection_ids": [120623194, 117479730]

}


```

Requests with a bot score at or below `bot_score_threshold` are directed to the paywall. Use `except_detection_ids` to allowlist specific crawlers by [detection ID](https://developers.cloudflare.com/ai-crawl-control/reference/bots/).

## Deploy

Clone the template, edit `wrangler.jsonc`, and deploy:

Terminal window

```

git clone https://github.com/cloudflare/mpp-proxy

cd mpp-proxy

npm install

npx wrangler secret put JWT_SECRET

npx wrangler secret put MPP_SECRET_KEY

npx wrangler deploy


```

For full configuration options, proxy modes, and Bot Management examples, refer to the [mpp-proxy README ↗](https://github.com/cloudflare/mpp-proxy).

## Custom Worker endpoints

For more control, add MPP middleware directly to your Worker using Hono:

TypeScript

```

import { Hono } from "hono";

import { Mppx, tempo } from "mppx/hono";


const app = new Hono();


const mppx = Mppx.create({

  methods: [

    tempo({

      currency: "0x20c0000000000000000000000000000000000000",

      recipient: "0xYourWalletAddress",

    }),

  ],

});


app.get("/premium", mppx.charge({ amount: "0.10" }), (c) =>

  c.json({ data: "Thanks for paying!" }),

);


export default app;


```

Refer to the [Hono middleware reference ↗](https://mpp.dev/sdk/typescript/middlewares/hono) for the full API, including session payments and payer identification.

## Related

* [mpp.dev ↗](https://mpp.dev) — Protocol specification
* [Pay Per Crawl](https://developers.cloudflare.com/ai-crawl-control/features/pay-per-crawl/) — Cloudflare-native monetization without custom code

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/mpp/","name":"MPP (Machine Payments Protocol)"}},{"@type":"ListItem","position":5,"item":{"@id":"/agents/agentic-payments/mpp/charge-for-http-content/","name":"Charge for HTTP content"}}]}
```

---

---
title: x402
description: Accept and make machine-to-machine payments using the x402 HTTP payment protocol on Cloudflare Workers and the Agents SDK.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/x402/index.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# x402

[x402 ↗](https://www.x402.org/) is a payment standard built around HTTP 402 (Payment Required). Services return a 402 response with payment instructions, and clients pay programmatically without accounts, sessions, or API keys.

## How it works

1. A client requests a resource — `GET /resource`.
2. The server returns `402 Payment Required` with a `PAYMENT-REQUIRED` header containing Base64-encoded payment details: the price, accepted token, network, and merchant address.
3. The client constructs a signed payment payload and retries the request with a `PAYMENT-SIGNATURE` header.
4. The server verifies the payment payload — directly or by calling a [facilitator](#the-facilitator) — and settles the transaction on-chain.
5. The server returns the resource with a `PAYMENT-RESPONSE` header containing settlement confirmation.

## Key components

### Client

The client is any entity that requests a paid resource: a human-operated app, an AI agent, or a programmatic service. Clients need only a crypto wallet — no accounts, credentials, or session tokens to manage.

### Server

The server defines payment requirements in the `402` response, verifies incoming payment payloads, settles the transaction, and serves the resource. The x402 SDKs and a facilitator handle most of this automatically.

### The facilitator

The facilitator is an optional but recommended third-party service that abstracts blockchain interaction. Rather than connecting to a node directly, the server delegates two operations:

* **`POST /verify`** — Confirms the client's payment payload is valid before the server fulfills the request.
* **`POST /settle`** — Submits the verified payment transaction to the blockchain.

The facilitator does not hold funds. It verifies and broadcasts the client's pre-signed transaction on behalf of the server. `https://x402.org/facilitator` is the public facilitator operated by Coinbase and is used in all Cloudflare examples. [Multiple facilitators ↗](https://www.x402.org/ecosystem?filter=facilitators) are available across different networks.

## Payment schemes and networks

x402 uses payment **schemes** to define how a payment is constructed and settled on a given network.

| Scheme                                                                                      | Networks                                 | Description                                                                                                                         |
| ------------------------------------------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [exact ↗](https://github.com/coinbase/x402/blob/main/specs/schemes/exact/scheme%5Fexact.md) | EVM, Solana, Aptos, Stellar, Hedera, Sui | Transfers a fixed token amount — typically [ERC-20 ↗](https://eips.ethereum.org/EIPS/eip-20) USDC on EVM — to the merchant address. |
| [upto ↗](https://github.com/coinbase/x402/blob/main/specs/schemes/upto/scheme%5Fupto.md)    | EVM                                      | Authorizes a maximum amount; the actual charge is determined at settlement time based on resource consumption.                      |

Supported networks include Base, Ethereum, Polygon, Optimism, Arbitrum, Avalanche, Solana, Aptos, Stellar, and Sui. Use `base-sepolia` for testing with free test USDC from the [Circle Faucet ↗](https://faucet.circle.com/).

## Charge for resources

[ HTTP content ](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-http-content/) Gate APIs, web pages, and files with a Worker proxy 

[ MCP tools ](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-mcp-tools/) Charge per tool call using paidTool 

## Pay for resources

[ Agents SDK ](https://developers.cloudflare.com/agents/agentic-payments/x402/pay-from-agents-sdk/) Wrap MCP clients with withX402Client 

[ Coding tools ](https://developers.cloudflare.com/agents/agentic-payments/x402/pay-with-tool-plugins/) OpenCode plugin and Claude Code hook 

## SDKs

| Package     | Install                 | Use                                           |
| ----------- | ----------------------- | --------------------------------------------- |
| x402-hono   | npm install x402-hono   | Hono middleware for Worker servers            |
| @x402/fetch | npm install @x402/fetch | Fetch wrapper with automatic payment handling |
| @x402/evm   | npm install @x402/evm   | EVM payment scheme support                    |
| agents/x402 | Included in agents      | MCP client with x402 payment support          |

## Related

* [x402.org ↗](https://x402.org) — Protocol specification
* [x402 GitHub ↗](https://github.com/coinbase/x402) — Open source SDK
* [x402 examples ↗](https://github.com/cloudflare/agents/tree/main/examples) — Complete working code
* [Pay Per Crawl](https://developers.cloudflare.com/ai-crawl-control/features/pay-per-crawl/) — Cloudflare-native monetization

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/x402/","name":"x402"}}]}
```

---

---
title: Charge for HTTP content
description: Gate HTTP endpoints with x402 payments using a Cloudflare Worker proxy.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/x402/charge-for-http-content.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Charge for HTTP content

The x402-proxy template is a Cloudflare Worker that sits in front of any HTTP backend. When a request hits a protected route, the proxy returns a 402 response with payment instructions. After the client pays, the proxy verifies the payment and forwards the request to your origin.

Deploy the x402-proxy template to your Cloudflare account:

[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/x402-proxy-template)

## Prerequisites

* A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up)
* An HTTP backend to gate
* A wallet address to receive payments

## Configuration

Define protected routes in `wrangler.jsonc`:

```

{

  "vars": {

    "PAY_TO": "0xYourWalletAddress",

    "NETWORK": "base-sepolia",

    "PROTECTED_PATTERNS": [

      {

        "pattern": "/api/premium/*",

        "price": "$0.10",

        "description": "Premium API access"

      }

    ]

  }

}


```

Note

`base-sepolia` is a test network. Change to `base` for production.

## Selective gating with Bot Management

With [Bot Management](https://developers.cloudflare.com/bots/), the proxy can charge crawlers while keeping the site free for humans:

```

{

  "pattern": "/content/*",

  "price": "$0.10",

  "description": "Content access",

  "bot_score_threshold": 30,

  "except_detection_ids": [117479730]

}


```

Requests with a bot score below `bot_score_threshold` are directed to the paywall. Use `except_detection_ids` to allowlist specific crawlers by [detection ID](https://developers.cloudflare.com/ai-crawl-control/reference/bots/).

## Deploy

Clone the template, edit `wrangler.jsonc`, and deploy:

Terminal window

```

git clone https://github.com/cloudflare/templates

cd templates/x402-proxy-template

npm install

npx wrangler deploy


```

For full configuration options and Bot Management examples, refer to the [template README ↗](https://github.com/cloudflare/templates/tree/main/x402-proxy-template).

## Custom Worker endpoints

For more control, add x402 middleware directly to your Worker using Hono:

TypeScript

```

import { Hono } from "hono";

import { paymentMiddleware } from "x402-hono";


const app = new Hono<{ Bindings: Env }>();


app.use(

  paymentMiddleware(

    "0xYourWalletAddress" as `0x${string}`,

    {

      "/premium": {

        price: "$0.10",

        network: "base-sepolia",

        config: { description: "Premium content" },

      },

    },

    { url: "https://x402.org/facilitator" },

  ),

);


app.get("/premium", (c) => c.json({ message: "Thanks for paying!" }));


export default app;


```

Refer to the [x402 Workers example ↗](https://github.com/cloudflare/agents/tree/main/examples/x402) for a complete implementation.

## Related

* [Pay Per Crawl](https://developers.cloudflare.com/ai-crawl-control/features/pay-per-crawl/) — Native Cloudflare monetization without custom code
* [Charge for MCP tools](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-mcp-tools/) — Charge per tool call instead of per request
* [x402.org ↗](https://x402.org) — Protocol specification

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/x402/","name":"x402"}},{"@type":"ListItem","position":5,"item":{"@id":"/agents/agentic-payments/x402/charge-for-http-content/","name":"Charge for HTTP content"}}]}
```

---

---
title: Charge for MCP tools
description: Charge per tool call in an MCP server using paidTool.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/x402/charge-for-mcp-tools.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Charge for MCP tools

The Agents SDK provides `paidTool`, a drop-in replacement for `tool` that adds x402 payment requirements. Clients pay per tool call, and you can mix free and paid tools in the same server.

## Setup

Wrap your `McpServer` with `withX402` and use `paidTool` for tools you want to charge for:

TypeScript

```

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { McpAgent } from "agents/mcp";

import { withX402, type X402Config } from "agents/x402";

import { z } from "zod";


const X402_CONFIG: X402Config = {

  network: "base",

  recipient: "0xYourWalletAddress",

  facilitator: { url: "https://x402.org/facilitator" }, // Payment facilitator URL

  // To learn more about facilitators: https://docs.x402.org/core-concepts/facilitator

};


export class PaidMCP extends McpAgent<Env> {

  server = withX402(

    new McpServer({ name: "PaidMCP", version: "1.0.0" }),

    X402_CONFIG,

  );


  async init() {

    // Paid tool — $0.01 per call

    this.server.paidTool(

      "square",

      "Squares a number",

      0.01, // USD

      { number: z.number() },

      {},

      async ({ number }) => {

        return { content: [{ type: "text", text: String(number ** 2) }] };

      },

    );


    // Free tool

    this.server.tool(

      "echo",

      "Echo a message",

      { message: z.string() },

      async ({ message }) => {

        return { content: [{ type: "text", text: message }] };

      },

    );

  }

}


```

## Configuration

| Field       | Description                                                |
| ----------- | ---------------------------------------------------------- |
| network     | base for production, base-sepolia for testing              |
| recipient   | Wallet address to receive payments                         |
| facilitator | Payment facilitator URL (use https://x402.org/facilitator) |

## paidTool signature

TypeScript

```

this.server.paidTool(

  name, // Tool name

  description, // Tool description

  price, // Price in USD (e.g., 0.01)

  inputSchema, // Zod schema for inputs

  annotations, // MCP annotations

  handler, // Async function that executes the tool

);


```

When a client calls a paid tool without payment, the server returns 402 with payment requirements. The client pays via x402, retries with payment proof, and receives the result.

## Testing

Use `base-sepolia` and get test USDC from the [Circle faucet ↗](https://faucet.circle.com/).

For a complete working example, refer to [x402-mcp on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/x402-mcp).

## Related

* [Pay from Agents SDK](https://developers.cloudflare.com/agents/agentic-payments/x402/pay-from-agents-sdk/) — Build clients that pay for tools
* [Charge for HTTP content](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-http-content/) — Gate HTTP endpoints
* [MCP server guide](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) — Build your first MCP server

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/x402/","name":"x402"}},{"@type":"ListItem","position":5,"item":{"@id":"/agents/agentic-payments/x402/charge-for-mcp-tools/","name":"Charge for MCP tools"}}]}
```

---

---
title: Pay from Agents SDK
description: Use withX402Client to pay for resources from a Cloudflare Agent.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/x402/pay-from-agents-sdk.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Pay from Agents SDK

The Agents SDK includes an MCP client that can pay for x402-protected tools. Use it from your Agents or any MCP client connection.

TypeScript

```

import { Agent } from "agents";

import { withX402Client } from "agents/x402";

import { privateKeyToAccount } from "viem/accounts";


export class MyAgent extends Agent {

  // Your Agent definitions...


  async onStart() {

    const { id } = await this.mcp.connect(`${this.env.WORKER_URL}/mcp`);

    const account = privateKeyToAccount(this.env.MY_PRIVATE_KEY);


    this.x402Client = withX402Client(this.mcp.mcpConnections[id].client, {

      network: "base-sepolia",

      account,

    });

  }


  onPaymentRequired(paymentRequirements): Promise<boolean> {

    // Your human-in-the-loop confirmation flow...

  }


  async onToolCall(toolName: string, toolArgs: unknown) {

    // The first parameter is the confirmation callback.

    // Set to `null` for the agent to pay automatically.

    return await this.x402Client.callTool(this.onPaymentRequired, {

      name: toolName,

      arguments: toolArgs,

    });

  }

}


```

For a complete working example, see [x402-mcp on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/x402-mcp).

## Environment setup

Store your private key securely:

Terminal window

```

# Local development (.dev.vars)

MY_PRIVATE_KEY="0x..."


# Production

npx wrangler secret put MY_PRIVATE_KEY


```

Use `base-sepolia` for testing. Get test USDC from the [Circle faucet ↗](https://faucet.circle.com/).

## Related

* [Charge for MCP tools](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-mcp-tools/) — Build servers that charge for tools
* [Pay from coding tools](https://developers.cloudflare.com/agents/agentic-payments/x402/pay-with-tool-plugins/) — Add payments to OpenCode or Claude Code
* [Human-in-the-loop guide](https://developers.cloudflare.com/agents/guides/human-in-the-loop/) — Implement approval workflows

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/x402/","name":"x402"}},{"@type":"ListItem","position":5,"item":{"@id":"/agents/agentic-payments/x402/pay-from-agents-sdk/","name":"Pay from Agents SDK"}}]}
```

---

---
title: Pay from coding tools
description: Add x402 payment handling to OpenCode and Claude Code.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/agentic-payments/x402/pay-with-tool-plugins.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Pay from coding tools

The following examples show how to add x402 payment handling to AI coding tools. When the tool encounters a 402 response, it pays automatically and retries.

Both examples require:

* A wallet private key (set as `X402_PRIVATE_KEY` environment variable)
* The x402 packages: `@x402/fetch`, `@x402/evm`, and `viem`

## OpenCode plugin

OpenCode plugins expose tools to the agent. To create an `x402-fetch` tool that handles 402 responses, create `.opencode/plugins/x402-payment.ts`:

TypeScript

```

// Use base-sepolia for testing. Get test USDC from https://faucet.circle.com/

import type { Plugin } from "@opencode-ai/plugin";

import { tool } from "@opencode-ai/plugin";

import { x402Client, wrapFetchWithPayment } from "@x402/fetch";

import { registerExactEvmScheme } from "@x402/evm/exact/client";

import { privateKeyToAccount } from "viem/accounts";


export const X402PaymentPlugin: Plugin = async () => ({

  tool: {

    "x402-fetch": tool({

      description:

        "Fetch a URL with x402 payment. Use when webfetch returns 402.",

      args: {

        url: tool.schema.string().describe("The URL to fetch"),

        timeout: tool.schema.number().optional().describe("Timeout in seconds"),

      },

      async execute(args) {

        const privateKey = process.env.X402_PRIVATE_KEY;

        if (!privateKey) {

          throw new Error("X402_PRIVATE_KEY environment variable is not set.");

        }


        // Your human-in-the-loop confirmation flow...

        // const approved = await confirmPayment(args.url, estimatedCost);

        // if (!approved) throw new Error("Payment declined by user");


        const account = privateKeyToAccount(privateKey as `0x${string}`);

        const client = new x402Client();

        registerExactEvmScheme(client, { signer: account });

        const paidFetch = wrapFetchWithPayment(fetch, client);


        const response = await paidFetch(args.url, {

          method: "GET",

          signal: args.timeout

            ? AbortSignal.timeout(args.timeout * 1000)

            : undefined,

        });


        if (!response.ok) {

          throw new Error(`${response.status} ${response.statusText}`);

        }


        return await response.text();

      },

    }),

  },

});


```

When the built-in `webfetch` returns a 402, the agent calls `x402-fetch` to retry with payment.

## Claude Code hook

Claude Code hooks intercept tool results. To handle 402s transparently, create a script at `.claude/scripts/handle-x402.mjs`:

JavaScript

```

// Use base-sepolia for testing. Get test USDC from https://faucet.circle.com/

import { x402Client, wrapFetchWithPayment } from "@x402/fetch";

import { registerExactEvmScheme } from "@x402/evm/exact/client";

import { privateKeyToAccount } from "viem/accounts";


const input = JSON.parse(await readStdin());


const haystack = JSON.stringify(input.tool_response ?? input.error ?? "");

if (!haystack.includes("402")) process.exit(0);


const url = input.tool_input?.url;

if (!url) process.exit(0);


const privateKey = process.env.X402_PRIVATE_KEY;

if (!privateKey) {

  console.error("X402_PRIVATE_KEY not set.");

  process.exit(2);

}


try {

  // Your human-in-the-loop confirmation flow...

  // const approved = await confirmPayment(url);

  // if (!approved) process.exit(0);


  const account = privateKeyToAccount(privateKey);

  const client = new x402Client();

  registerExactEvmScheme(client, { signer: account });

  const paidFetch = wrapFetchWithPayment(fetch, client);


  const res = await paidFetch(url, { method: "GET" });

  const text = await res.text();


  if (!res.ok) {

    console.error(`Paid fetch failed: ${res.status}`);

    process.exit(2);

  }


  console.log(

    JSON.stringify({

      hookSpecificOutput: {

        hookEventName: "PostToolUse",

        additionalContext: `Paid for "${url}" via x402:\n${text}`,

      },

    }),

  );

} catch (err) {

  console.error(`x402 payment failed: ${err.message}`);

  process.exit(2);

}


function readStdin() {

  return new Promise((resolve) => {

    let data = "";

    process.stdin.on("data", (chunk) => (data += chunk));

    process.stdin.on("end", () => resolve(data));

  });

}


```

Register the hook in `.claude/settings.json`:

```

{

  "hooks": {

    "PostToolUse": [

      {

        "matcher": "WebFetch",

        "hooks": [

          {

            "type": "command",

            "command": "node .claude/scripts/handle-x402.mjs",

            "timeout": 30

          }

        ]

      }

    ]

  }

}


```

## Related

* [Pay from Agents SDK](https://developers.cloudflare.com/agents/agentic-payments/x402/pay-from-agents-sdk/) — Use the Agents SDK for more control
* [Charge for HTTP content](https://developers.cloudflare.com/agents/agentic-payments/x402/charge-for-http-content/) — Build the server side
* [Human-in-the-loop guide](https://developers.cloudflare.com/agents/guides/human-in-the-loop/) — Implement approval workflows
* [x402.org ↗](https://x402.org) — Protocol specification

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/agentic-payments/","name":"Agentic Payments"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/agentic-payments/x402/","name":"x402"}},{"@type":"ListItem","position":5,"item":{"@id":"/agents/agentic-payments/x402/pay-with-tool-plugins/","name":"Pay from coding tools"}}]}
```

---

---
title: Agents API
description: This page provides an overview of the Agents SDK. For detailed documentation on each feature, refer to the linked reference pages.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/agents-api.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Agents API

This page provides an overview of the Agents SDK. For detailed documentation on each feature, refer to the linked reference pages.

## Overview

The Agents SDK provides two main APIs:

| API                         | Description                                                                      |
| --------------------------- | -------------------------------------------------------------------------------- |
| **Server-side** Agent class | Encapsulates agent logic: connections, state, methods, AI models, error handling |
| **Client-side** SDK         | AgentClient, useAgent, and useAgentChat for connecting from browsers             |

Note

Agents require [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/). Refer to [Configuration](https://developers.cloudflare.com/agents/api-reference/configuration/) to learn how to add the required bindings.

## Agent class

An Agent is a class that extends the base `Agent` class:

TypeScript

```

import { Agent } from "agents";


class MyAgent extends Agent<Env, State> {

  // Your agent logic

}


export default MyAgent;


```

Each Agent can have millions of instances. Each instance is a separate micro-server that runs independently, allowing horizontal scaling. Instances are addressed by a unique identifier (user ID, email, ticket number, etc.).

Note

An instance of an Agent is globally unique: given the same name (or ID), you will always get the same instance of an agent.

This allows you to avoid synchronizing state across requests: if an Agent instance represents a specific user, team, channel or other entity, you can use the Agent instance to store state for that entity. There is no need to set up a centralized session store.

If the client disconnects, you can always route the client back to the exact same Agent and pick up where they left off.

## Lifecycle

flowchart TD
    A["onStart<br/>(instance wakes up)"] --> B["onRequest<br/>(HTTP)"]
    A --> C["onConnect<br/>(WebSocket)"]
    A --> D["onEmail"]
    C --> E["onMessage ↔ send()<br/>onError (on failure)"]
    E --> F["onClose"]

| Method                                      | When it runs                                                                                                                                                                                                         |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| onStart(props?)                             | When the instance starts, or wakes from hibernation. Receives optional [initialization props](https://developers.cloudflare.com/agents/api-reference/routing/#props) passed via getAgentByName or routeAgentRequest. |
| onRequest(request)                          | For each HTTP request to the instance                                                                                                                                                                                |
| onConnect(connection, ctx)                  | When a WebSocket connection is established                                                                                                                                                                           |
| onMessage(connection, message)              | For each WebSocket message received                                                                                                                                                                                  |
| onError(connection, error)                  | When a WebSocket error occurs                                                                                                                                                                                        |
| onClose(connection, code, reason, wasClean) | When a WebSocket connection closes                                                                                                                                                                                   |
| onEmail(email)                              | When an email is routed to the instance                                                                                                                                                                              |
| onStateChanged(state, source)               | When state changes (from server or client)                                                                                                                                                                           |

## Core properties

| Property   | Type             | Description                            |
| ---------- | ---------------- | -------------------------------------- |
| this.env   | Env              | Environment variables and bindings     |
| this.ctx   | ExecutionContext | Execution context for the request      |
| this.state | State            | Current persisted state                |
| this.sql   | Function         | Execute SQL queries on embedded SQLite |

## Server-side API reference

| Feature               | Methods                                                                    | Documentation                                                                                        |
| --------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| **State**             | setState(), onStateChanged(), initialState                                 | [Store and sync state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) |
| **Callable methods**  | @callable() decorator                                                      | [Callable methods](https://developers.cloudflare.com/agents/api-reference/callable-methods/)         |
| **Scheduling**        | schedule(), scheduleEvery(), getSchedules(), cancelSchedule(), keepAlive() | [Schedule tasks](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/)             |
| **Queue**             | queue(), dequeue(), dequeueAll(), getQueue()                               | [Queue tasks](https://developers.cloudflare.com/agents/api-reference/queue-tasks/)                   |
| **WebSockets**        | onConnect(), onMessage(), onClose(), broadcast()                           | [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/)                     |
| **HTTP/SSE**          | onRequest()                                                                | [HTTP and SSE](https://developers.cloudflare.com/agents/api-reference/http-sse/)                     |
| **Email**             | onEmail(), replyToEmail()                                                  | [Email routing](https://developers.cloudflare.com/agents/api-reference/email/)                       |
| **Workflows**         | runWorkflow(), waitForApproval()                                           | [Run Workflows](https://developers.cloudflare.com/agents/api-reference/run-workflows/)               |
| **MCP Client**        | addMcpServer(), removeMcpServer(), getMcpServers()                         | [MCP Client API](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/)             |
| **AI Models**         | Workers AI, OpenAI, Anthropic bindings                                     | [Using AI models](https://developers.cloudflare.com/agents/api-reference/using-ai-models/)           |
| **Protocol messages** | shouldSendProtocolMessages(), isConnectionProtocolEnabled()                | [Protocol messages](https://developers.cloudflare.com/agents/api-reference/protocol-messages/)       |
| **Context**           | getCurrentAgent()                                                          | [getCurrentAgent()](https://developers.cloudflare.com/agents/api-reference/get-current-agent/)       |
| **Observability**     | subscribe(), diagnostics channels, Tail Workers                            | [Observability](https://developers.cloudflare.com/agents/api-reference/observability/)               |

## SQL API

Each Agent instance has an embedded SQLite database accessed via `this.sql`:

TypeScript

```

// Create tables

this.sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT)`;


// Insert data

this.sql`INSERT INTO users (id, name) VALUES (${id}, ${name})`;


// Query data

const users = this.sql<User>`SELECT * FROM users WHERE id = ${id}`;


```

For state that needs to sync with clients, use the [State API](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) instead.

## Client-side API reference

| Feature              | Methods        | Documentation                                                                                                  |
| -------------------- | -------------- | -------------------------------------------------------------------------------------------------------------- |
| **WebSocket client** | AgentClient    | [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/)                               |
| **HTTP client**      | agentFetch()   | [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/#http-requests-with-agentfetch) |
| **React hook**       | useAgent()     | [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/#react)                         |
| **Chat hook**        | useAgentChat() | [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/)                               |

### Quick example

TypeScript

```

import { useAgent } from "agents/react";

import type { MyAgent } from "./server";


function App() {

  const agent = useAgent<MyAgent, State>({

    agent: "my-agent",

    name: "user-123",

  });


  // Call methods on the agent

  agent.stub.someMethod();


  // Update state (syncs to server and all clients)

  agent.setState({ count: 1 });

}


```

## Chat agents

For AI chat applications, extend `AIChatAgent` instead of `Agent`:

TypeScript

```

import { AIChatAgent } from "agents/ai-chat-agent";


class ChatAgent extends AIChatAgent {

  async onChatMessage(onFinish) {

    // this.messages contains the conversation history

    // Return a streaming response

  }

}


```

Features include:

* Built-in message persistence
* Automatic resumable streaming (reconnect mid-stream)
* Works with `useAgentChat` React hook

Refer to [Build a chat agent](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) for a complete tutorial.

## Routing

Agents are accessed via URL patterns:

```

https://your-worker.workers.dev/agents/:agent-name/:instance-name


```

Use `routeAgentRequest()` in your Worker to route requests:

TypeScript

```

import { routeAgentRequest } from "agents";


export default {

  async fetch(request: Request, env: Env) {

    return (

      routeAgentRequest(request, env) ||

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

Refer to [Routing](https://developers.cloudflare.com/agents/api-reference/routing/) for custom paths, CORS, and instance naming patterns.

## Next steps

[ Quick start ](https://developers.cloudflare.com/agents/getting-started/quick-start/) Build your first agent in about 10 minutes. 

[ Configuration ](https://developers.cloudflare.com/agents/api-reference/configuration/) Learn about wrangler.jsonc setup and deployment. 

[ WebSockets ](https://developers.cloudflare.com/agents/api-reference/websockets/) Real-time bidirectional communication with clients. 

[ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) Build AI applications with AIChatAgent. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/agents-api/","name":"Agents API"}}]}
```

---

---
title: Browse the web
description: Agents can browse the web using the Browser Rendering API or your preferred headless browser service.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/browse-the-web.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Browse the web

Agents can browse the web using the [Browser Rendering](https://developers.cloudflare.com/browser-rendering/) API or your preferred headless browser service.

### Browser Rendering API

The [Browser Rendering](https://developers.cloudflare.com/browser-rendering/) allows you to spin up headless browser instances, render web pages, and interact with websites through your Agent.

You can define a method that uses Puppeteer to pull the content of a web page, parse the DOM, and extract relevant information by calling a model via [Workers AI](https://developers.cloudflare.com/workers-ai/):

* [  JavaScript ](#tab-panel-2120)
* [  TypeScript ](#tab-panel-2121)

JavaScript

```

export class MyAgent extends Agent {

  async browse(browserInstance, urls) {

    let responses = [];

    for (const url of urls) {

      const browser = await puppeteer.launch(browserInstance);

      const page = await browser.newPage();

      await page.goto(url);


      await page.waitForSelector("body");

      const bodyContent = await page.$eval(

        "body",

        (element) => element.innerHTML,

      );


      let resp = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {

        messages: [

          {

            role: "user",

            content: `Return a JSON object with the product names, prices and URLs with the following format: { "name": "Product Name", "price": "Price", "url": "URL" } from the website content below. <content>${bodyContent}</content>`,

          },

        ],

      });


      responses.push(resp);

      await browser.close();

    }


    return responses;

  }

}


```

TypeScript

```

interface Env {

  BROWSER: Fetcher;

  AI: Ai;

}


export class MyAgent extends Agent<Env> {

  async browse(browserInstance: Fetcher, urls: string[]) {

    let responses = [];

    for (const url of urls) {

      const browser = await puppeteer.launch(browserInstance);

      const page = await browser.newPage();

      await page.goto(url);


      await page.waitForSelector("body");

      const bodyContent = await page.$eval(

        "body",

        (element) => element.innerHTML,

      );


      let resp = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {

        messages: [

          {

            role: "user",

            content: `Return a JSON object with the product names, prices and URLs with the following format: { "name": "Product Name", "price": "Price", "url": "URL" } from the website content below. <content>${bodyContent}</content>`,

          },

        ],

      });


      responses.push(resp);

      await browser.close();

    }


    return responses;

  }

}


```

You'll also need to add install the `@cloudflare/puppeteer` package and add the following to the wrangler configuration of your Agent:

 npm  yarn  pnpm  bun 

```
npm i -D @cloudflare/puppeteer
```

```
yarn add -D @cloudflare/puppeteer
```

```
pnpm add -D @cloudflare/puppeteer
```

```
bun add -d @cloudflare/puppeteer
```

* [  wrangler.jsonc ](#tab-panel-2116)
* [  wrangler.toml ](#tab-panel-2117)

```

{

  // ...

  "ai": {

    "binding": "AI",

  },

  "browser": {

    "binding": "MYBROWSER",

  },

  // ...

}


```

```

[ai]

binding = "AI"


[browser]

binding = "MYBROWSER"


```

### Browserbase

You can also use [Browserbase ↗](https://docs.browserbase.com/integrations/cloudflare/typescript) by using the Browserbase API directly from within your Agent.

Once you have your [Browserbase API key ↗](https://docs.browserbase.com/integrations/cloudflare/typescript), you can add it to your Agent by creating a [secret](https://developers.cloudflare.com/workers/configuration/secrets/):

Terminal window

```

cd your-agent-project-folder

npx wrangler@latest secret put BROWSERBASE_API_KEY


```

```

Enter a secret value: ******

Creating the secret for the Worker "agents-example"

Success! Uploaded secret BROWSERBASE_API_KEY


```

Install the `@cloudflare/puppeteer` package and use it from within your Agent to call the Browserbase API:

 npm  yarn  pnpm  bun 

```
npm i @cloudflare/puppeteer
```

```
yarn add @cloudflare/puppeteer
```

```
pnpm add @cloudflare/puppeteer
```

```
bun add @cloudflare/puppeteer
```

* [  JavaScript ](#tab-panel-2118)
* [  TypeScript ](#tab-panel-2119)

JavaScript

```

export class MyAgent extends Agent {

  constructor(env) {

    super(env);

  }

}


```

TypeScript

```

interface Env {

  BROWSERBASE_API_KEY: string;

}


export class MyAgent extends Agent {

  constructor(env: Env) {

    super(env);

  }

}


```

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/browse-the-web/","name":"Browse the web"}}]}
```

---

---
title: Callable methods
description: Callable methods let clients invoke agent methods over WebSocket using RPC (Remote Procedure Call). Mark methods with @callable() to expose them to external clients like browsers, mobile apps, or other services.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/callable-methods.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Callable methods

Callable methods let clients invoke agent methods over WebSocket using RPC (Remote Procedure Call). Mark methods with `@callable()` to expose them to external clients like browsers, mobile apps, or other services.

## Overview

* [  JavaScript ](#tab-panel-2124)
* [  TypeScript ](#tab-panel-2125)

JavaScript

```

import { Agent, callable } from "agents";


export class MyAgent extends Agent {

  @callable()

  async greet(name) {

    return `Hello, ${name}!`;

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


export class MyAgent extends Agent {

  @callable()

  async greet(name: string): Promise<string> {

    return `Hello, ${name}!`;

  }

}


```

* [  JavaScript ](#tab-panel-2122)
* [  TypeScript ](#tab-panel-2123)

JavaScript

```

// Client

const result = await agent.stub.greet("World");

console.log(result); // "Hello, World!"


```

TypeScript

```

// Client

const result = await agent.stub.greet("World");

console.log(result); // "Hello, World!"


```

### How it works

sequenceDiagram
    participant Client
    participant Agent
    Client->>Agent: agent.stub.greet("World")
    Note right of Agent: Check @callable<br/>Execute method
    Agent-->>Client: "Hello, World!"

### When to use `@callable()`

| Scenario                             | Use                                      |
| ------------------------------------ | ---------------------------------------- |
| Browser/mobile calling agent         | @callable()                              |
| External service calling agent       | @callable()                              |
| Worker calling agent (same codebase) | Durable Object RPC (no decorator needed) |
| Agent calling another agent          | Durable Object RPC via getAgentByName()  |

The `@callable()` decorator is specifically for WebSocket-based RPC from external clients. When calling from within the same Worker or another agent, use standard [Durable Object RPC](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) directly.

## Basic usage

### Defining callable methods

Add the `@callable()` decorator to any method you want to expose:

* [  JavaScript ](#tab-panel-2142)
* [  TypeScript ](#tab-panel-2143)

JavaScript

```

import { Agent, callable } from "agents";


export class CounterAgent extends Agent {

  initialState = { count: 0, items: [] };


  @callable()

  increment() {

    this.setState({ ...this.state, count: this.state.count + 1 });

    return this.state.count;

  }


  @callable()

  decrement() {

    this.setState({ ...this.state, count: this.state.count - 1 });

    return this.state.count;

  }


  @callable()

  async addItem(item) {

    this.setState({ ...this.state, items: [...this.state.items, item] });

    return this.state.items;

  }


  @callable()

  getStats() {

    return {

      count: this.state.count,

      itemCount: this.state.items.length,

    };

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


export type CounterState = {

  count: number;

  items: string[];

};


export class CounterAgent extends Agent<Env, CounterState> {

  initialState: CounterState = { count: 0, items: [] };


  @callable()

  increment(): number {

    this.setState({ ...this.state, count: this.state.count + 1 });

    return this.state.count;

  }


  @callable()

  decrement(): number {

    this.setState({ ...this.state, count: this.state.count - 1 });

    return this.state.count;

  }


  @callable()

  async addItem(item: string): Promise<string[]> {

    this.setState({ ...this.state, items: [...this.state.items, item] });

    return this.state.items;

  }


  @callable()

  getStats(): { count: number; itemCount: number } {

    return {

      count: this.state.count,

      itemCount: this.state.items.length,

    };

  }

}


```

### Calling from the client

There are two ways to call methods from the client:

#### Using `agent.stub` (recommended):

* [  JavaScript ](#tab-panel-2126)
* [  TypeScript ](#tab-panel-2127)

JavaScript

```

// Clean, typed syntax

const count = await agent.stub.increment();

const items = await agent.stub.addItem("new item");

const stats = await agent.stub.getStats();


```

TypeScript

```

// Clean, typed syntax

const count = await agent.stub.increment();

const items = await agent.stub.addItem("new item");

const stats = await agent.stub.getStats();


```

#### Using `agent.call()`:

* [  JavaScript ](#tab-panel-2128)
* [  TypeScript ](#tab-panel-2129)

JavaScript

```

// Explicit method name as string

const count = await agent.call("increment");

const items = await agent.call("addItem", ["new item"]);

const stats = await agent.call("getStats");


```

TypeScript

```

// Explicit method name as string

const count = await agent.call("increment");

const items = await agent.call("addItem", ["new item"]);

const stats = await agent.call("getStats");


```

The `stub` proxy provides better ergonomics and TypeScript support.

## Method signatures

### Serializable types

Arguments and return values must be JSON-serializable:

* [  JavaScript ](#tab-panel-2134)
* [  TypeScript ](#tab-panel-2135)

JavaScript

```

// Valid - primitives and plain objects

class MyAgent extends Agent {

  @callable()

  processData(input) {

    return { result: true };

  }

}


// Valid - arrays

class MyAgent extends Agent {

  @callable()

  processItems(items) {

    return items.map((item) => item.length);

  }

}


// Invalid - non-serializable types

// Functions, Dates, Maps, Sets, etc. cannot be serialized


```

TypeScript

```

// Valid - primitives and plain objects

class MyAgent extends Agent {

  @callable()

  processData(input: { name: string; count: number }): { result: boolean } {

    return { result: true };

  }

}


// Valid - arrays

class MyAgent extends Agent {

  @callable()

  processItems(items: string[]): number[] {

    return items.map((item) => item.length);

  }

}


// Invalid - non-serializable types

// Functions, Dates, Maps, Sets, etc. cannot be serialized


```

### Async methods

Both sync and async methods work:

* [  JavaScript ](#tab-panel-2136)
* [  TypeScript ](#tab-panel-2137)

JavaScript

```

// Sync method

class MyAgent extends Agent {

  @callable()

  add(a, b) {

    return a + b;

  }

}


// Async method

class MyAgent extends Agent {

  @callable()

  async fetchUser(id) {

    const user = await this.sql`SELECT * FROM users WHERE id = ${id}`;

    return user[0];

  }

}


```

TypeScript

```

// Sync method

class MyAgent extends Agent {

  @callable()

  add(a: number, b: number): number {

    return a + b;

  }

}


// Async method

class MyAgent extends Agent {

  @callable()

  async fetchUser(id: string): Promise<User> {

    const user = await this.sql`SELECT * FROM users WHERE id = ${id}`;

    return user[0];

  }

}


```

### Void methods

Methods that do not return a value:

* [  JavaScript ](#tab-panel-2132)
* [  TypeScript ](#tab-panel-2133)

JavaScript

```

class MyAgent extends Agent {

  @callable()

  async logEvent(event) {

    await this.sql`INSERT INTO events (name) VALUES (${event})`;

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  @callable()

  async logEvent(event: string): Promise<void> {

    await this.sql`INSERT INTO events (name) VALUES (${event})`;

  }

}


```

On the client, these still return a Promise that resolves when the method completes:

* [  JavaScript ](#tab-panel-2130)
* [  TypeScript ](#tab-panel-2131)

JavaScript

```

await agent.stub.logEvent("user-clicked");

// Resolves when the server confirms execution


```

TypeScript

```

await agent.stub.logEvent("user-clicked");

// Resolves when the server confirms execution


```

## Streaming responses

For methods that produce data over time (like AI text generation), use streaming:

### Defining a streaming method

* [  JavaScript ](#tab-panel-2144)
* [  TypeScript ](#tab-panel-2145)

JavaScript

```

import { Agent, callable } from "agents";


export class AIAgent extends Agent {

  @callable({ streaming: true })

  async generateText(stream, prompt) {

    // First parameter is always StreamingResponse for streaming methods


    for await (const chunk of this.llm.stream(prompt)) {

      stream.send(chunk); // Send each chunk to the client

    }


    stream.end(); // Signal completion

  }


  @callable({ streaming: true })

  async streamNumbers(stream, count) {

    for (let i = 0; i < count; i++) {

      stream.send(i);

      await new Promise((resolve) => setTimeout(resolve, 100));

    }

    stream.end(count); // Optional final value

  }

}


```

TypeScript

```

import { Agent, callable, type StreamingResponse } from "agents";


export class AIAgent extends Agent {

  @callable({ streaming: true })

  async generateText(stream: StreamingResponse, prompt: string) {

    // First parameter is always StreamingResponse for streaming methods


    for await (const chunk of this.llm.stream(prompt)) {

      stream.send(chunk); // Send each chunk to the client

    }


    stream.end(); // Signal completion

  }


  @callable({ streaming: true })

  async streamNumbers(stream: StreamingResponse, count: number) {

    for (let i = 0; i < count; i++) {

      stream.send(i);

      await new Promise((resolve) => setTimeout(resolve, 100));

    }

    stream.end(count); // Optional final value

  }

}


```

### Consuming streams on the client

* [  JavaScript ](#tab-panel-2152)
* [  TypeScript ](#tab-panel-2153)

JavaScript

```

// Preferred format (supports timeout and other options)

await agent.call("generateText", [prompt], {

  stream: {

    onChunk: (chunk) => {

      // Called for each chunk

      appendToOutput(chunk);

    },

    onDone: (finalValue) => {

      // Called when stream ends

      console.log("Stream complete", finalValue);

    },

    onError: (error) => {

      // Called if an error occurs

      console.error("Stream error:", error);

    },

  },

});


// Legacy format (still supported for backward compatibility)

await agent.call("generateText", [prompt], {

  onChunk: (chunk) => appendToOutput(chunk),

  onDone: (finalValue) => console.log("Done", finalValue),

  onError: (error) => console.error("Error:", error),

});


```

TypeScript

```

// Preferred format (supports timeout and other options)

await agent.call("generateText", [prompt], {

  stream: {

    onChunk: (chunk) => {

      // Called for each chunk

      appendToOutput(chunk);

    },

    onDone: (finalValue) => {

      // Called when stream ends

      console.log("Stream complete", finalValue);

    },

    onError: (error) => {

      // Called if an error occurs

      console.error("Stream error:", error);

    },

  },

});


// Legacy format (still supported for backward compatibility)

await agent.call("generateText", [prompt], {

  onChunk: (chunk) => appendToOutput(chunk),

  onDone: (finalValue) => console.log("Done", finalValue),

  onError: (error) => console.error("Error:", error),

});


```

### StreamingResponse API

| Method           | Description                                      |
| ---------------- | ------------------------------------------------ |
| send(chunk)      | Send a chunk to the client                       |
| end(finalChunk?) | End the stream, optionally with a final value    |
| error(message)   | Send an error to the client and close the stream |

* [  JavaScript ](#tab-panel-2138)
* [  TypeScript ](#tab-panel-2139)

JavaScript

```

class MyAgent extends Agent {

  @callable({ streaming: true })

  async processWithProgress(stream, items) {

    for (let i = 0; i < items.length; i++) {

      await this.process(items[i]);

      stream.send({ progress: (i + 1) / items.length, item: items[i] });

    }

    stream.end({ completed: true, total: items.length });

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  @callable({ streaming: true })

  async processWithProgress(stream: StreamingResponse, items: string[]) {

    for (let i = 0; i < items.length; i++) {

      await this.process(items[i]);

      stream.send({ progress: (i + 1) / items.length, item: items[i] });

    }

    stream.end({ completed: true, total: items.length });

  }

}


```

## TypeScript integration

### Typed client calls

Pass your agent class as a type parameter for full type safety:

* [  JavaScript ](#tab-panel-2150)
* [  TypeScript ](#tab-panel-2151)

JavaScript

```

import { useAgent } from "agents/react";

function App() {

  const agent = useAgent({

    agent: "MyAgent",

    name: "default",

  });


  async function handleGreet() {

    // TypeScript knows the method signature

    const result = await agent.stub.greet("World");

    // ^? string

  }


  // TypeScript catches errors

  // await agent.stub.greet(123); // Error: Argument of type 'number' is not assignable

  // await agent.stub.nonExistent(); // Error: Property 'nonExistent' does not exist

}


```

TypeScript

```

import { useAgent } from "agents/react";

import type { MyAgent } from "./server";


function App() {

  const agent = useAgent<MyAgent>({

    agent: "MyAgent",

    name: "default",

  });


  async function handleGreet() {

    // TypeScript knows the method signature

    const result = await agent.stub.greet("World");

    // ^? string

  }


  // TypeScript catches errors

  // await agent.stub.greet(123); // Error: Argument of type 'number' is not assignable

  // await agent.stub.nonExistent(); // Error: Property 'nonExistent' does not exist

}


```

### Excluding non-callable methods

If you have methods that are not decorated with `@callable()`, you can exclude them from the type:

* [  JavaScript ](#tab-panel-2156)
* [  TypeScript ](#tab-panel-2157)

JavaScript

```

class MyAgent extends Agent {

  @callable()

  publicMethod() {

    return "public";

  }


  // Not callable from clients

  internalMethod() {

    // internal logic

  }

}


// Exclude internal methods from the client type

const agent = useAgent({

  agent: "MyAgent",

});


agent.stub.publicMethod(); // Works

// agent.stub.internalMethod(); // TypeScript error


```

TypeScript

```

class MyAgent extends Agent {

  @callable()

  publicMethod(): string {

    return "public";

  }


  // Not callable from clients

  internalMethod(): void {

    // internal logic

  }

}


// Exclude internal methods from the client type

const agent = useAgent<Omit<MyAgent, "internalMethod">>({

  agent: "MyAgent",

});


agent.stub.publicMethod(); // Works

// agent.stub.internalMethod(); // TypeScript error


```

## Error handling

### Throwing errors in callable methods

Errors thrown in callable methods are propagated to the client:

* [  JavaScript ](#tab-panel-2146)
* [  TypeScript ](#tab-panel-2147)

JavaScript

```

class MyAgent extends Agent {

  @callable()

  async riskyOperation(data) {

    if (!isValid(data)) {

      throw new Error("Invalid data format");

    }


    try {

      await this.processData(data);

    } catch (e) {

      throw new Error("Processing failed: " + e.message);

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  @callable()

  async riskyOperation(data: unknown): Promise<void> {

    if (!isValid(data)) {

      throw new Error("Invalid data format");

    }


    try {

      await this.processData(data);

    } catch (e) {

      throw new Error("Processing failed: " + e.message);

    }

  }

}


```

### Client-side error handling

* [  JavaScript ](#tab-panel-2140)
* [  TypeScript ](#tab-panel-2141)

JavaScript

```

try {

  const result = await agent.stub.riskyOperation(data);

} catch (error) {

  // Error thrown by the agent method

  console.error("RPC failed:", error.message);

}


```

TypeScript

```

try {

  const result = await agent.stub.riskyOperation(data);

} catch (error) {

  // Error thrown by the agent method

  console.error("RPC failed:", error.message);

}


```

### Streaming error handling

For streaming methods, use the `onError` callback:

* [  JavaScript ](#tab-panel-2148)
* [  TypeScript ](#tab-panel-2149)

JavaScript

```

await agent.call("streamData", [input], {

  stream: {

    onChunk: (chunk) => handleChunk(chunk),

    onError: (errorMessage) => {

      console.error("Stream error:", errorMessage);

      showErrorUI(errorMessage);

    },

    onDone: (result) => handleComplete(result),

  },

});


```

TypeScript

```

await agent.call("streamData", [input], {

  stream: {

    onChunk: (chunk) => handleChunk(chunk),

    onError: (errorMessage) => {

      console.error("Stream error:", errorMessage);

      showErrorUI(errorMessage);

    },

    onDone: (result) => handleComplete(result),

  },

});


```

Server-side, you can use `stream.error()` to gracefully send an error mid-stream:

* [  JavaScript ](#tab-panel-2158)
* [  TypeScript ](#tab-panel-2159)

JavaScript

```

class MyAgent extends Agent {

  @callable({ streaming: true })

  async processItems(stream, items) {

    for (const item of items) {

      try {

        const result = await this.process(item);

        stream.send(result);

      } catch (e) {

        stream.error(`Failed to process ${item}: ${e.message}`);

        return; // Stream is now closed

      }

    }

    stream.end();

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  @callable({ streaming: true })

  async processItems(stream: StreamingResponse, items: string[]) {

    for (const item of items) {

      try {

        const result = await this.process(item);

        stream.send(result);

      } catch (e) {

        stream.error(`Failed to process ${item}: ${e.message}`);

        return; // Stream is now closed

      }

    }

    stream.end();

  }

}


```

### Connection errors

If the WebSocket connection closes while RPC calls are pending, they automatically reject with a "Connection closed" error:

* [  JavaScript ](#tab-panel-2154)
* [  TypeScript ](#tab-panel-2155)

JavaScript

```

try {

  const result = await agent.call("longRunningMethod", []);

} catch (error) {

  if (error.message === "Connection closed") {

    // Handle disconnection

    console.log("Lost connection to agent");

  }

}


```

TypeScript

```

try {

  const result = await agent.call("longRunningMethod", []);

} catch (error) {

  if (error.message === "Connection closed") {

    // Handle disconnection

    console.log("Lost connection to agent");

  }

}


```

#### Retrying after reconnection

The client automatically reconnects after disconnection. To retry a failed call after reconnection, await `agent.ready` before retrying:

* [  JavaScript ](#tab-panel-2164)
* [  TypeScript ](#tab-panel-2165)

JavaScript

```

async function callWithRetry(agent, method, args = []) {

  try {

    return await agent.call(method, args);

  } catch (error) {

    if (error.message === "Connection closed") {

      await agent.ready; // Wait for reconnection

      return await agent.call(method, args); // Retry once

    }

    throw error;

  }

}


// Usage

const result = await callWithRetry(agent, "processData", [data]);


```

TypeScript

```

async function callWithRetry<T>(

  agent: AgentClient,

  method: string,

  args: unknown[] = [],

): Promise<T> {

  try {

    return await agent.call(method, args);

  } catch (error) {

    if (error.message === "Connection closed") {

      await agent.ready; // Wait for reconnection

      return await agent.call(method, args); // Retry once

    }

    throw error;

  }

}


// Usage

const result = await callWithRetry(agent, "processData", [data]);


```

Note

Only retry idempotent operations. If the server received the request but the connection dropped before the response arrived, retrying could cause duplicate execution.

## When NOT to use @callable

### Worker-to-Agent calls

When calling an agent from the same Worker (for example, in your `fetch` handler), use Durable Object RPC directly:

* [  JavaScript ](#tab-panel-2160)
* [  TypeScript ](#tab-panel-2161)

JavaScript

```

import { getAgentByName } from "agents";


export default {

  async fetch(request, env) {

    // Get the agent stub

    const agent = await getAgentByName(env.MyAgent, "instance-name");


    // Call methods directly - no @callable needed

    const result = await agent.processData(data);


    return Response.json(result);

  },

};


```

TypeScript

```

import { getAgentByName } from "agents";


export default {

  async fetch(request: Request, env: Env) {

    // Get the agent stub

    const agent = await getAgentByName(env.MyAgent, "instance-name");


    // Call methods directly - no @callable needed

    const result = await agent.processData(data);


    return Response.json(result);

  },

} satisfies ExportedHandler<Env>;


```

### Agent-to-Agent calls

When one agent needs to call another:

* [  JavaScript ](#tab-panel-2162)
* [  TypeScript ](#tab-panel-2163)

JavaScript

```

class OrchestratorAgent extends Agent {

  async delegateWork(taskId) {

    // Get another agent

    const worker = await getAgentByName(this.env.WorkerAgent, taskId);


    // Call its methods directly

    const result = await worker.doWork();


    return result;

  }

}


```

TypeScript

```

class OrchestratorAgent extends Agent {

  async delegateWork(taskId: string) {

    // Get another agent

    const worker = await getAgentByName(this.env.WorkerAgent, taskId);


    // Call its methods directly

    const result = await worker.doWork();


    return result;

  }

}


```

### Why the distinction?

| RPC Type           | Transport | Use Case                          |
| ------------------ | --------- | --------------------------------- |
| @callable          | WebSocket | External clients (browsers, apps) |
| Durable Object RPC | Internal  | Worker to Agent, Agent to Agent   |

Durable Object RPC is more efficient for internal calls since it does not go through WebSocket serialization. The `@callable` decorator adds the necessary WebSocket RPC handling for external clients.

## API reference

### @callable(metadata?) decorator

Marks a method as callable from external clients.

* [  JavaScript ](#tab-panel-2166)
* [  TypeScript ](#tab-panel-2167)

JavaScript

```

import { callable } from "agents";


class MyAgent extends Agent {

  @callable()

  method() {}


  @callable({ streaming: true })

  streamingMethod(stream) {}


  @callable({ description: "Fetches user data" })

  getUser(id) {}

}


```

TypeScript

```

import { callable } from "agents";


class MyAgent extends Agent {

  @callable()

  method(): void {}


  @callable({ streaming: true })

  streamingMethod(stream: StreamingResponse): void {}


  @callable({ description: "Fetches user data" })

  getUser(id: string): User {}

}


```

### CallableMetadata type

TypeScript

```

type CallableMetadata = {

  /** Optional description of what the method does */

  description?: string;

  /** Whether the method supports streaming responses */

  streaming?: boolean;

};


```

### StreamingResponse class

Used in streaming callable methods to send data to the client.

* [  JavaScript ](#tab-panel-2168)
* [  TypeScript ](#tab-panel-2169)

JavaScript

```

import {} from "agents";


class MyAgent extends Agent {

  @callable({ streaming: true })

  async streamData(stream, input) {

    stream.send("chunk 1");

    stream.send("chunk 2");

    stream.end("final");

  }

}


```

TypeScript

```

import { type StreamingResponse } from "agents";


class MyAgent extends Agent {

  @callable({ streaming: true })

  async streamData(stream: StreamingResponse, input: string) {

    stream.send("chunk 1");

    stream.send("chunk 2");

    stream.end("final");

  }

}


```

| Method | Signature                      | Description                        |
| ------ | ------------------------------ | ---------------------------------- |
| send   | (chunk: unknown) => void       | Send a chunk to the client         |
| end    | (finalChunk?: unknown) => void | End the stream                     |
| error  | (message: string) => void      | Send an error and close the stream |

### Client methods

| Method     | Signature                            | Description           |
| ---------- | ------------------------------------ | --------------------- |
| agent.call | (method, args?, options?) => Promise | Call a method by name |
| agent.stub | Proxy                                | Typed method calls    |

* [  JavaScript ](#tab-panel-2172)
* [  TypeScript ](#tab-panel-2173)

JavaScript

```

// Using call()

await agent.call("methodName", [arg1, arg2]);

await agent.call("streamMethod", [arg], {

  stream: { onChunk, onDone, onError },

});


// With timeout (rejects if call does not complete in time)

await agent.call("slowMethod", [], { timeout: 5000 });


// Using stub

await agent.stub.methodName(arg1, arg2);


```

TypeScript

```

// Using call()

await agent.call("methodName", [arg1, arg2]);

await agent.call("streamMethod", [arg], {

  stream: { onChunk, onDone, onError },

});


// With timeout (rejects if call does not complete in time)

await agent.call("slowMethod", [], { timeout: 5000 });


// Using stub

await agent.stub.methodName(arg1, arg2);


```

### CallOptions type

TypeScript

```

type CallOptions = {

  /** Timeout in milliseconds. Rejects if call does not complete in time. */

  timeout?: number;

  /** Streaming options */

  stream?: {

    onChunk?: (chunk: unknown) => void;

    onDone?: (finalChunk: unknown) => void;

    onError?: (error: string) => void;

  };

};


```

Note

The legacy format `{ onChunk, onDone, onError }` (without nesting under `stream`) is still supported. The client automatically detects which format you are using.

### getCallableMethods() method

Returns a map of all callable methods on the agent with their metadata. Useful for introspection and automatic documentation.

* [  JavaScript ](#tab-panel-2170)
* [  TypeScript ](#tab-panel-2171)

JavaScript

```

const methods = agent.getCallableMethods();

// Map<string, CallableMetadata>


for (const [name, meta] of methods) {

  console.log(`${name}: ${meta.description || "(no description)"}`);

  if (meta.streaming) console.log("  (streaming)");

}


```

TypeScript

```

const methods = agent.getCallableMethods();

// Map<string, CallableMetadata>


for (const [name, meta] of methods) {

  console.log(`${name}: ${meta.description || "(no description)"}`);

  if (meta.streaming) console.log("  (streaming)");

}


```

## Troubleshooting

### `SyntaxError: Invalid or unexpected token`

If your dev server fails with `SyntaxError: Invalid or unexpected token` when using `@callable()`, set `"target": "ES2021"` in your `tsconfig.json`. This ensures that Vite's esbuild transpiler downlevels TC39 decorators instead of passing them through as native syntax.

```

{

  "compilerOptions": {

    "target": "ES2021"

  }

}


```

Warning

Do not set `"experimentalDecorators": true` in your `tsconfig.json`. The Agents SDK uses [TC39 standard decorators ↗](https://github.com/tc39/proposal-decorators), not TypeScript legacy decorators. Enabling `experimentalDecorators` applies an incompatible transform that silently breaks `@callable()` at runtime.

## Next steps

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ WebSockets ](https://developers.cloudflare.com/agents/api-reference/websockets/) Real-time bidirectional communication with clients. 

[ State management ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Sync state between agents and clients. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/callable-methods/","name":"Callable methods"}}]}
```

---

---
title: Chat agents
description: Build AI-powered chat interfaces with AIChatAgent and useAgentChat. Messages are automatically persisted to SQLite, streams resume on disconnect, and tool calls work across server and client.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/chat-agents.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Chat agents

Build AI-powered chat interfaces with `AIChatAgent` and `useAgentChat`. Messages are automatically persisted to SQLite, streams resume on disconnect, and tool calls work across server and client.

## Overview

The `@cloudflare/ai-chat` package provides two main exports:

| Export       | Import                    | Purpose                                                        |
| ------------ | ------------------------- | -------------------------------------------------------------- |
| AIChatAgent  | @cloudflare/ai-chat       | Server-side agent class with message persistence and streaming |
| useAgentChat | @cloudflare/ai-chat/react | React hook for building chat UIs                               |

Built on the [AI SDK ↗](https://ai-sdk.dev) and Cloudflare Durable Objects, you get:

* **Automatic message persistence** — conversations stored in SQLite, survive restarts
* **Resumable streaming** — disconnected clients resume mid-stream without data loss
* **Real-time sync** — messages broadcast to all connected clients via WebSocket
* **Tool support** — server-side, client-side, and human-in-the-loop tool patterns
* **Data parts** — attach typed JSON (citations, progress, usage) to messages alongside text
* **Row size protection** — automatic compaction when messages approach SQLite limits

## Quick start

### Install

Terminal window

```

npm install @cloudflare/ai-chat agents ai


```

### Server

* [  JavaScript ](#tab-panel-2176)
* [  TypeScript ](#tab-panel-2177)

JavaScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";

import { createWorkersAI } from "workers-ai-provider";

import { streamText, convertToModelMessages } from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    // Use any provicer such as workers-ai-provider, openai, anthropic, google, etc.

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";

import { createWorkersAI } from "workers-ai-provider";

import { streamText, convertToModelMessages } from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    // Use any provicer such as workers-ai-provider, openai, anthropic, google, etc.

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

### Client

* [  JavaScript ](#tab-panel-2196)
* [  TypeScript ](#tab-panel-2197)

JavaScript

```

import { useAgent } from "agents/react";

import { useAgentChat } from "@cloudflare/ai-chat/react";


function Chat() {

  const agent = useAgent({ agent: "ChatAgent" });

  const { messages, sendMessage, status } = useAgentChat({ agent });


  return (

    <div>

      {messages.map((msg) => (

        <div key={msg.id}>

          <strong>{msg.role}:</strong>

          {msg.parts.map((part, i) =>

            part.type === "text" ? <span key={i}>{part.text}</span> : null,

          )}

        </div>

      ))}


      <form

        onSubmit={(e) => {

          e.preventDefault();

          const input = e.currentTarget.elements.namedItem("input");

          sendMessage({ text: input.value });

          input.value = "";

        }}

      >

        <input name="input" placeholder="Type a message..." />

        <button type="submit" disabled={status === "streaming"}>

          Send

        </button>

      </form>

    </div>

  );

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useAgentChat } from "@cloudflare/ai-chat/react";


function Chat() {

  const agent = useAgent({ agent: "ChatAgent" });

  const { messages, sendMessage, status } = useAgentChat({ agent });


  return (

    <div>

      {messages.map((msg) => (

        <div key={msg.id}>

          <strong>{msg.role}:</strong>

          {msg.parts.map((part, i) =>

            part.type === "text" ? <span key={i}>{part.text}</span> : null,

          )}

        </div>

      ))}


      <form

        onSubmit={(e) => {

          e.preventDefault();

          const input = e.currentTarget.elements.namedItem(

            "input",

          ) as HTMLInputElement;

          sendMessage({ text: input.value });

          input.value = "";

        }}

      >

        <input name="input" placeholder="Type a message..." />

        <button type="submit" disabled={status === "streaming"}>

          Send

        </button>

      </form>

    </div>

  );

}


```

### Wrangler configuration

```

// wrangler.jsonc

{

  "ai": { "binding": "AI" },

  "durable_objects": {

    "bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],

  },

  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],

}


```

The `new_sqlite_classes` migration is required — `AIChatAgent` uses SQLite for message persistence and stream chunk buffering.

## How it works

sequenceDiagram
    participant Client as Client (useAgentChat)
    participant Agent as AIChatAgent
    participant DB as SQLite

    Client->>Agent: CF_AGENT_USE_CHAT_REQUEST (WebSocket)
    Agent->>DB: Persist messages
    Agent->>Agent: onChatMessage()
    loop Streaming response
        Agent-->>Client: CF_AGENT_USE_CHAT_RESPONSE (chunks)
        Agent->>DB: Buffer chunks
    end
    Agent->>DB: Persist final message
    Agent-->>Client: CF_AGENT_CHAT_MESSAGES (broadcast to all clients)

1. The client sends a message via WebSocket
2. `AIChatAgent` persists messages to SQLite and calls your `onChatMessage` method
3. Your method returns a streaming `Response` (typically from `streamText`)
4. Chunks stream back over WebSocket in real-time
5. When the stream completes, the final message is persisted and broadcast to all connections

## Server API

### `AIChatAgent`

Extends `Agent` from the `agents` package. Manages conversation state, persistence, and streaming.

* [  JavaScript ](#tab-panel-2182)
* [  TypeScript ](#tab-panel-2183)

JavaScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";


export class ChatAgent extends AIChatAgent {

  // Access current messages

  // this.messages: UIMessage[]


  // Limit stored messages (optional)

  maxPersistedMessages = 200;


  async onChatMessage(onFinish, options) {

    // onFinish: optional callback for streamText (cleanup is automatic)

    // options.abortSignal: cancel signal

    // options.body: custom data from client

    // Return a Response (streaming or plain text)

  }

}


```

TypeScript

```

import { AIChatAgent } from "@cloudflare/ai-chat";


export class ChatAgent extends AIChatAgent {

  // Access current messages

  // this.messages: UIMessage[]


  // Limit stored messages (optional)

  maxPersistedMessages = 200;


  async onChatMessage(onFinish?, options?) {

    // onFinish: optional callback for streamText (cleanup is automatic)

    // options.abortSignal: cancel signal

    // options.body: custom data from client

    // Return a Response (streaming or plain text)

  }

}


```

### `onChatMessage`

This is the main method you override. It receives the conversation context and should return a `Response`.

**Streaming response** (most common):

* [  JavaScript ](#tab-panel-2178)
* [  TypeScript ](#tab-panel-2179)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      system: "You are a helpful assistant.",

      messages: await convertToModelMessages(this.messages),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      system: "You are a helpful assistant.",

      messages: await convertToModelMessages(this.messages),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

**Plain text response**:

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    return new Response("Hello! I am a simple agent.", {

      headers: { "Content-Type": "text/plain" },

    });

  }

}


```

**Accessing custom body data and request ID**:

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage(_onFinish, options) {

    const { timezone, userId } = options?.body ?? {};

    // Use these values in your LLM call or business logic


    // options.requestId — unique identifier for this chat request,

    // useful for logging and correlating events

    console.log("Request ID:", options?.requestId);

  }

}


```

### `this.messages`

The current conversation history, loaded from SQLite. This is an array of `UIMessage` objects from the AI SDK. Messages are automatically persisted after each interaction.

### `maxPersistedMessages`

Cap the number of messages stored in SQLite. When the limit is exceeded, the oldest messages are deleted. This controls storage only — it does not affect what is sent to the LLM.

* [  JavaScript ](#tab-panel-2174)
* [  TypeScript ](#tab-panel-2175)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  maxPersistedMessages = 200;

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  maxPersistedMessages = 200;

}


```

To control what is sent to the model, use the AI SDK's `pruneMessages()`:

* [  JavaScript ](#tab-panel-2186)
* [  TypeScript ](#tab-panel-2187)

JavaScript

```

import { streamText, convertToModelMessages, pruneMessages } from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: pruneMessages({

        messages: await convertToModelMessages(this.messages),

        reasoning: "before-last-message",

        toolCalls: "before-last-2-messages",

      }),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import { streamText, convertToModelMessages, pruneMessages } from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: pruneMessages({

        messages: await convertToModelMessages(this.messages),

        reasoning: "before-last-message",

        toolCalls: "before-last-2-messages",

      }),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

### `waitForMcpConnections`

Controls whether `AIChatAgent` waits for MCP server connections to settle before calling `onChatMessage`. This ensures `this.mcp.getAITools()` returns the full set of tools, especially after Durable Object hibernation when connections are being restored in the background.

| Value                | Behavior                                      |
| -------------------- | --------------------------------------------- |
| { timeout: 10\_000 } | Wait up to 10 seconds (default)               |
| { timeout: N }       | Wait up to N milliseconds                     |
| true                 | Wait indefinitely until all connections ready |
| false                | Do not wait (old behavior before 0.2.0)       |

* [  JavaScript ](#tab-panel-2184)
* [  TypeScript ](#tab-panel-2185)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  // Default — waits up to 10 seconds

  // waitForMcpConnections = { timeout: 10_000 };


  // Wait forever

  waitForMcpConnections = true;


  // Disable waiting

  waitForMcpConnections = false;

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  // Default — waits up to 10 seconds

  // waitForMcpConnections = { timeout: 10_000 };


  // Wait forever

  waitForMcpConnections = true;


  // Disable waiting

  waitForMcpConnections = false;

}


```

For lower-level control, call `this.mcp.waitForConnections()` directly inside your `onChatMessage` instead.

### `persistMessages` and `saveMessages`

For advanced cases, you can manually persist messages:

* [  JavaScript ](#tab-panel-2180)
* [  TypeScript ](#tab-panel-2181)

JavaScript

```

// Persist messages without triggering a new response

await this.persistMessages(messages);


// Persist messages AND trigger onChatMessage (e.g., programmatic messages)

await this.saveMessages(messages);


```

TypeScript

```

// Persist messages without triggering a new response

await this.persistMessages(messages);


// Persist messages AND trigger onChatMessage (e.g., programmatic messages)

await this.saveMessages(messages);


```

### Lifecycle hooks

Override `onConnect` and `onClose` to add custom logic. Stream resumption and message sync are handled for you:

* [  JavaScript ](#tab-panel-2188)
* [  TypeScript ](#tab-panel-2189)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  async onConnect(connection, ctx) {

    // Your custom logic (e.g., logging, auth checks)

    console.log("Client connected:", connection.id);

    // Stream resumption and message sync are handled automatically

  }


  async onClose(connection, code, reason, wasClean) {

    console.log("Client disconnected:", connection.id);

    // Connection cleanup is handled automatically

  }

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onConnect(connection, ctx) {

    // Your custom logic (e.g., logging, auth checks)

    console.log("Client connected:", connection.id);

    // Stream resumption and message sync are handled automatically

  }


  async onClose(connection, code, reason, wasClean) {

    console.log("Client disconnected:", connection.id);

    // Connection cleanup is handled automatically

  }

}


```

The `destroy()` method cancels any pending chat requests and cleans up stream state. It is called automatically when the Durable Object is evicted, but you can call it manually if needed.

### Request cancellation

When a user clicks "stop" in the chat UI, the client sends a `CF_AGENT_CHAT_REQUEST_CANCEL` message. The server propagates this to the `abortSignal` in `options`:

* [  JavaScript ](#tab-panel-2190)
* [  TypeScript ](#tab-panel-2191)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage(_onFinish, options) {

    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

      abortSignal: options?.abortSignal, // Pass through for cancellation

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage(_onFinish, options) {

    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

      abortSignal: options?.abortSignal, // Pass through for cancellation

    });


    return result.toUIMessageStreamResponse();

  }

}


```

Warning

If you do not pass `abortSignal` to `streamText`, the LLM call will continue running in the background even after the user cancels. Always forward it when possible.

## Client API

### `useAgentChat`

React hook that connects to an `AIChatAgent` over WebSocket. Wraps the AI SDK's `useChat` with a native WebSocket transport.

* [  JavaScript ](#tab-panel-2194)
* [  TypeScript ](#tab-panel-2195)

JavaScript

```

import { useAgent } from "agents/react";

import { useAgentChat } from "@cloudflare/ai-chat/react";


function Chat() {

  const agent = useAgent({ agent: "ChatAgent" });

  const {

    messages,

    sendMessage,

    clearHistory,

    addToolOutput,

    addToolApprovalResponse,

    setMessages,

    status,

  } = useAgentChat({ agent });


  // ...

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useAgentChat } from "@cloudflare/ai-chat/react";


function Chat() {

  const agent = useAgent({ agent: "ChatAgent" });

  const {

    messages,

    sendMessage,

    clearHistory,

    addToolOutput,

    addToolApprovalResponse,

    setMessages,

    status,

  } = useAgentChat({ agent });


  // ...

}


```

### Options

| Option                      | Type                                        | Default  | Description                                                                                                          |
| --------------------------- | ------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| agent                       | ReturnType<typeof useAgent>                 | Required | Agent connection from useAgent                                                                                       |
| onToolCall                  | ({ toolCall, addToolOutput }) => void       | —        | Handle client-side tool execution                                                                                    |
| autoContinueAfterToolResult | boolean                                     | true     | Auto-continue conversation after client tool results and approvals                                                   |
| resume                      | boolean                                     | true     | Enable automatic stream resumption on reconnect                                                                      |
| body                        | object \| () => object                      | —        | Custom data sent with every request                                                                                  |
| prepareSendMessagesRequest  | (options) => { body?, headers? }            | —        | Advanced per-request customization                                                                                   |
| getInitialMessages          | (options) => Promise<UIMessage\[\]> or null | —        | Custom initial message loader. Set to null to skip the HTTP fetch entirely (useful when providing messages directly) |

### Return values

| Property                | Type                             | Description                                  |
| ----------------------- | -------------------------------- | -------------------------------------------- |
| messages                | UIMessage\[\]                    | Current conversation messages                |
| sendMessage             | (message) => void                | Send a message                               |
| clearHistory            | () => void                       | Clear conversation (client and server)       |
| addToolOutput           | ({ toolCallId, output }) => void | Provide output for a client-side tool        |
| addToolApprovalResponse | ({ id, approved }) => void       | Approve or reject a tool requiring approval  |
| setMessages             | (messages \| updater) => void    | Set messages directly (syncs to server)      |
| status                  | string                           | "idle", "submitted", "streaming", or "error" |

## Tools

`AIChatAgent` supports three tool patterns, all using the AI SDK's `tool()` function:

| Pattern     | Where it runs                | When to use                                   |
| ----------- | ---------------------------- | --------------------------------------------- |
| Server-side | Server (automatic)           | API calls, database queries, computations     |
| Client-side | Browser (via onToolCall)     | Geolocation, clipboard, camera, local storage |
| Approval    | Server (after user approval) | Payments, deletions, external actions         |

### Server-side tools

Tools with an `execute` function run automatically on the server:

* [  JavaScript ](#tab-panel-2210)
* [  TypeScript ](#tab-panel-2211)

JavaScript

```

import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";

import { z } from "zod";

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

      tools: {

        getWeather: tool({

          description: "Get weather for a city",

          inputSchema: z.object({ city: z.string() }),

          execute: async ({ city }) => {

            const data = await fetchWeather(city);

            return { temperature: data.temp, condition: data.condition };

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";

import { z } from "zod";

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: await convertToModelMessages(this.messages),

      tools: {

        getWeather: tool({

          description: "Get weather for a city",

          inputSchema: z.object({ city: z.string() }),

          execute: async ({ city }) => {

            const data = await fetchWeather(city);

            return { temperature: data.temp, condition: data.condition };

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

### Client-side tools

Define a tool on the server without `execute`, then handle it on the client with `onToolCall`. Use this for tools that need browser APIs.

**Server:**

* [  JavaScript ](#tab-panel-2192)
* [  TypeScript ](#tab-panel-2193)

JavaScript

```

tools: {

  getLocation: tool({

    description: "Get the user's location from the browser",

    inputSchema: z.object({}),

    // No execute — the client handles it

  });

}


```

TypeScript

```

tools: {

  getLocation: tool({

    description: "Get the user's location from the browser",

    inputSchema: z.object({}),

    // No execute — the client handles it

  });

}


```

**Client:**

* [  JavaScript ](#tab-panel-2200)
* [  TypeScript ](#tab-panel-2201)

JavaScript

```

const { messages, sendMessage } = useAgentChat({

  agent,

  onToolCall: async ({ toolCall, addToolOutput }) => {

    if (toolCall.toolName === "getLocation") {

      const pos = await new Promise((resolve, reject) =>

        navigator.geolocation.getCurrentPosition(resolve, reject),

      );

      addToolOutput({

        toolCallId: toolCall.toolCallId,

        output: { lat: pos.coords.latitude, lng: pos.coords.longitude },

      });

    }

  },

});


```

TypeScript

```

const { messages, sendMessage } = useAgentChat({

  agent,

  onToolCall: async ({ toolCall, addToolOutput }) => {

    if (toolCall.toolName === "getLocation") {

      const pos = await new Promise((resolve, reject) =>

        navigator.geolocation.getCurrentPosition(resolve, reject),

      );

      addToolOutput({

        toolCallId: toolCall.toolCallId,

        output: { lat: pos.coords.latitude, lng: pos.coords.longitude },

      });

    }

  },

});


```

When the LLM invokes `getLocation`, the stream pauses. The `onToolCall` callback fires, your code provides the output, and the conversation continues.

### Tool approval (human-in-the-loop)

Use `needsApproval` for tools that require user confirmation before executing.

**Server:**

* [  JavaScript ](#tab-panel-2198)
* [  TypeScript ](#tab-panel-2199)

JavaScript

```

tools: {

  processPayment: tool({

    description: "Process a payment",

    inputSchema: z.object({

      amount: z.number(),

      recipient: z.string(),

    }),

    needsApproval: async ({ amount }) => amount > 100,

    execute: async ({ amount, recipient }) => charge(amount, recipient),

  });

}


```

TypeScript

```

tools: {

  processPayment: tool({

    description: "Process a payment",

    inputSchema: z.object({

      amount: z.number(),

      recipient: z.string(),

    }),

    needsApproval: async ({ amount }) => amount > 100,

    execute: async ({ amount, recipient }) => charge(amount, recipient),

  });

}


```

**Client:**

* [  JavaScript ](#tab-panel-2228)
* [  TypeScript ](#tab-panel-2229)

JavaScript

```

const { messages, addToolApprovalResponse } = useAgentChat({ agent });


// Render pending approvals from message parts

{

  messages.map((msg) =>

    msg.parts

      .filter(

        (part) => part.type === "tool" && part.state === "approval-required",

      )

      .map((part) => (

        <div key={part.toolCallId}>

          <p>Approve {part.toolName}?</p>

          <button

            onClick={() =>

              addToolApprovalResponse({

                id: part.toolCallId,

                approved: true,

              })

            }

          >

            Approve

          </button>

          <button

            onClick={() =>

              addToolApprovalResponse({

                id: part.toolCallId,

                approved: false,

              })

            }

          >

            Reject

          </button>

        </div>

      )),

  );

}


```

TypeScript

```

const { messages, addToolApprovalResponse } = useAgentChat({ agent });


// Render pending approvals from message parts

{

  messages.map((msg) =>

    msg.parts

      .filter(

        (part) => part.type === "tool" && part.state === "approval-required",

      )

      .map((part) => (

        <div key={part.toolCallId}>

          <p>Approve {part.toolName}?</p>

          <button

            onClick={() =>

              addToolApprovalResponse({

                id: part.toolCallId,

                approved: true,

              })

            }

          >

            Approve

          </button>

          <button

            onClick={() =>

              addToolApprovalResponse({

                id: part.toolCallId,

                approved: false,

              })

            }

          >

            Reject

          </button>

        </div>

      )),

  );

}


```

#### Custom denial messages with `addToolOutput`

When a user rejects a tool, `addToolApprovalResponse({ id, approved: false })` sets the tool state to `output-denied` with a generic message. To give the LLM a more specific reason for the denial, use `addToolOutput` with `state: "output-error"` instead:

* [  JavaScript ](#tab-panel-2202)
* [  TypeScript ](#tab-panel-2203)

JavaScript

```

const { addToolOutput } = useAgentChat({ agent });


// Reject with a custom error message

addToolOutput({

  toolCallId: part.toolCallId,

  state: "output-error",

  errorText: "User declined: insufficient budget for this quarter",

});


```

TypeScript

```

const { addToolOutput } = useAgentChat({ agent });


// Reject with a custom error message

addToolOutput({

  toolCallId: part.toolCallId,

  state: "output-error",

  errorText: "User declined: insufficient budget for this quarter",

});


```

This sends a `tool_result` to the LLM with your custom error text, so it can respond appropriately (for example, suggest an alternative or ask clarifying questions).

`addToolApprovalResponse` (with `approved: false`) auto-continues the conversation when `autoContinueAfterToolResult` is enabled (the default). `addToolOutput` with `state: "output-error"` does **not** auto-continue — call `sendMessage()` afterward if you want the LLM to respond to the error.

For more patterns, refer to [Human-in-the-loop](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/).

## Custom request data

Include custom data with every chat request using the `body` option:

* [  JavaScript ](#tab-panel-2206)
* [  TypeScript ](#tab-panel-2207)

JavaScript

```

const { messages, sendMessage } = useAgentChat({

  agent,

  body: {

    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,

    userId: currentUser.id,

  },

});


```

TypeScript

```

const { messages, sendMessage } = useAgentChat({

  agent,

  body: {

    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,

    userId: currentUser.id,

  },

});


```

For dynamic values, use a function:

* [  JavaScript ](#tab-panel-2204)
* [  TypeScript ](#tab-panel-2205)

JavaScript

```

body: () => ({

  token: getAuthToken(),

  timestamp: Date.now(),

});


```

TypeScript

```

body: () => ({

  token: getAuthToken(),

  timestamp: Date.now(),

});


```

Access these fields on the server:

* [  JavaScript ](#tab-panel-2208)
* [  TypeScript ](#tab-panel-2209)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage(_onFinish, options) {

    const { timezone, userId } = options?.body ?? {};

    // ...

  }

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage(_onFinish, options) {

    const { timezone, userId } = options?.body ?? {};

    // ...

  }

}


```

For advanced per-request customization (custom headers, different body per request), use `prepareSendMessagesRequest`:

* [  JavaScript ](#tab-panel-2212)
* [  TypeScript ](#tab-panel-2213)

JavaScript

```

const { messages, sendMessage } = useAgentChat({

  agent,

  prepareSendMessagesRequest: async ({ messages, trigger }) => ({

    headers: { Authorization: `Bearer ${await getToken()}` },

    body: { requestedAt: Date.now() },

  }),

});


```

TypeScript

```

const { messages, sendMessage } = useAgentChat({

  agent,

  prepareSendMessagesRequest: async ({ messages, trigger }) => ({

    headers: { Authorization: `Bearer ${await getToken()}` },

    body: { requestedAt: Date.now() },

  }),

});


```

## Data parts

Data parts let you attach typed JSON to messages alongside text — progress indicators, source citations, token usage, or any structured data your UI needs.

### Writing data parts (server)

Use `createUIMessageStream` with `writer.write()` to send data parts from the server:

* [  JavaScript ](#tab-panel-2234)
* [  TypeScript ](#tab-panel-2235)

JavaScript

```

import {

  streamText,

  convertToModelMessages,

  createUIMessageStream,

  createUIMessageStreamResponse,

} from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const stream = createUIMessageStream({

      execute: async ({ writer }) => {

        const result = streamText({

          model: workersai("@cf/zai-org/glm-4.7-flash"),

          messages: await convertToModelMessages(this.messages),

        });


        // Merge the LLM stream

        writer.merge(result.toUIMessageStream());


        // Write a data part — persisted to message.parts

        writer.write({

          type: "data-sources",

          id: "src-1",

          data: { query: "agents", status: "searching", results: [] },

        });


        // Later: update the same part in-place (same type + id)

        writer.write({

          type: "data-sources",

          id: "src-1",

          data: {

            query: "agents",

            status: "found",

            results: ["Agents SDK docs", "Durable Objects guide"],

          },

        });

      },

    });


    return createUIMessageStreamResponse({ stream });

  }

}


```

TypeScript

```

import {

  streamText,

  convertToModelMessages,

  createUIMessageStream,

  createUIMessageStreamResponse,

} from "ai";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const stream = createUIMessageStream({

      execute: async ({ writer }) => {

        const result = streamText({

          model: workersai("@cf/zai-org/glm-4.7-flash"),

          messages: await convertToModelMessages(this.messages),

        });


        // Merge the LLM stream

        writer.merge(result.toUIMessageStream());


        // Write a data part — persisted to message.parts

        writer.write({

          type: "data-sources",

          id: "src-1",

          data: { query: "agents", status: "searching", results: [] },

        });


        // Later: update the same part in-place (same type + id)

        writer.write({

          type: "data-sources",

          id: "src-1",

          data: {

            query: "agents",

            status: "found",

            results: ["Agents SDK docs", "Durable Objects guide"],

          },

        });

      },

    });


    return createUIMessageStreamResponse({ stream });

  }

}


```

### Three patterns

| Pattern            | How                                          | Persisted? | Use case                              |
| ------------------ | -------------------------------------------- | ---------- | ------------------------------------- |
| **Reconciliation** | Same type \+ id → updates in-place           | Yes        | Progressive state (searching → found) |
| **Append**         | No id, or different id → appends             | Yes        | Log entries, multiple citations       |
| **Transient**      | transient: true → not added to message.parts | No         | Ephemeral status (thinking indicator) |

Transient parts are broadcast to connected clients in real time but excluded from SQLite persistence and `message.parts`. Use the `onData` callback to consume them.

### Reading data parts (client)

Non-transient data parts appear in `message.parts`. Use the `UIMessage` generic to type them:

* [  JavaScript ](#tab-panel-2224)
* [  TypeScript ](#tab-panel-2225)

JavaScript

```

import { useAgentChat } from "@cloudflare/ai-chat/react";

const { messages } = useAgentChat({ agent });


// Typed access — no casts needed

for (const msg of messages) {

  for (const part of msg.parts) {

    if (part.type === "data-sources") {

      console.log(part.data.results); // string[]

    }

  }

}


```

TypeScript

```

import { useAgentChat } from "@cloudflare/ai-chat/react";

import type { UIMessage } from "ai";


type ChatMessage = UIMessage<

  unknown,

  {

    sources: { query: string; status: string; results: string[] };

    usage: { model: string; inputTokens: number; outputTokens: number };

  }

>;


const { messages } = useAgentChat<unknown, ChatMessage>({ agent });


// Typed access — no casts needed

for (const msg of messages) {

  for (const part of msg.parts) {

    if (part.type === "data-sources") {

      console.log(part.data.results); // string[]

    }

  }

}


```

### Transient parts with `onData`

Transient data parts are not in `message.parts`. Use the `onData` callback instead:

* [  JavaScript ](#tab-panel-2218)
* [  TypeScript ](#tab-panel-2219)

JavaScript

```

const [thinking, setThinking] = useState(false);


const { messages } = useAgentChat({

  agent,

  onData(part) {

    if (part.type === "data-thinking") {

      setThinking(true);

    }

  },

});


```

TypeScript

```

const [thinking, setThinking] = useState(false);


const { messages } = useAgentChat<unknown, ChatMessage>({

  agent,

  onData(part) {

    if (part.type === "data-thinking") {

      setThinking(true);

    }

  },

});


```

On the server, write transient parts with `transient: true`:

* [  JavaScript ](#tab-panel-2216)
* [  TypeScript ](#tab-panel-2217)

JavaScript

```

writer.write({

  transient: true,

  type: "data-thinking",

  data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() },

});


```

TypeScript

```

writer.write({

  transient: true,

  type: "data-thinking",

  data: { model: "glm-4.7-flash", startedAt: new Date().toISOString() },

});


```

`onData` fires on all code paths — new messages, stream resumption, and cross-tab broadcasts.

## Resumable streaming

Streams automatically resume when a client disconnects and reconnects. No configuration is needed — it works out of the box.

When streaming is active:

1. All chunks are buffered in SQLite as they are generated
2. If the client disconnects, the server continues streaming and buffering
3. When the client reconnects, it receives all buffered chunks and resumes live streaming

Disable with `resume: false`:

* [  JavaScript ](#tab-panel-2214)
* [  TypeScript ](#tab-panel-2215)

JavaScript

```

const { messages } = useAgentChat({ agent, resume: false });


```

TypeScript

```

const { messages } = useAgentChat({ agent, resume: false });


```

## Storage management

### Row size protection

SQLite rows have a maximum size of 2 MB. When a message approaches this limit (for example, a tool returning a very large output), `AIChatAgent` automatically compacts the message:

1. **Tool output compaction** — Large tool outputs are replaced with an LLM-friendly summary that instructs the model to suggest re-running the tool
2. **Text truncation** — If the message is still too large after tool compaction, text parts are truncated with a note

Compacted messages include `metadata.compactedToolOutputs` so clients can detect and display this gracefully.

### Controlling LLM context vs storage

Storage (`maxPersistedMessages`) and LLM context are independent:

| Concern                         | Control              | Scope       |
| ------------------------------- | -------------------- | ----------- |
| How many messages SQLite stores | maxPersistedMessages | Persistence |
| What the model sees             | pruneMessages()      | LLM context |
| Row size limits                 | Automatic compaction | Per-message |

* [  JavaScript ](#tab-panel-2230)
* [  TypeScript ](#tab-panel-2231)

JavaScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: pruneMessages({

        // LLM context limit

        messages: await convertToModelMessages(this.messages),

        reasoning: "before-last-message",

        toolCalls: "before-last-2-messages",

      }),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      messages: pruneMessages({

        // LLM context limit

        messages: await convertToModelMessages(this.messages),

        reasoning: "before-last-message",

        toolCalls: "before-last-2-messages",

      }),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

## Using different AI providers

`AIChatAgent` works with any AI SDK-compatible provider. The server code determines which model to use — the client does not need to change it manually.

### Workers AI (Cloudflare)

* [  JavaScript ](#tab-panel-2220)
* [  TypeScript ](#tab-panel-2221)

JavaScript

```

import { createWorkersAI } from "workers-ai-provider";


const workersai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

  model: workersai("@cf/zai-org/glm-4.7-flash"),

  messages: await convertToModelMessages(this.messages),

});


```

TypeScript

```

import { createWorkersAI } from "workers-ai-provider";


const workersai = createWorkersAI({ binding: this.env.AI });

const result = streamText({

  model: workersai("@cf/zai-org/glm-4.7-flash"),

  messages: await convertToModelMessages(this.messages),

});


```

### OpenAI

* [  JavaScript ](#tab-panel-2222)
* [  TypeScript ](#tab-panel-2223)

JavaScript

```

import { createOpenAI } from "@ai-sdk/openai";


const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY });

const result = streamText({

  model: openai.chat("gpt-4o"),

  messages: await convertToModelMessages(this.messages),

});


```

TypeScript

```

import { createOpenAI } from "@ai-sdk/openai";


const openai = createOpenAI({ apiKey: this.env.OPENAI_API_KEY });

const result = streamText({

  model: openai.chat("gpt-4o"),

  messages: await convertToModelMessages(this.messages),

});


```

### Anthropic

* [  JavaScript ](#tab-panel-2226)
* [  TypeScript ](#tab-panel-2227)

JavaScript

```

import { createAnthropic } from "@ai-sdk/anthropic";


const anthropic = createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY });

const result = streamText({

  model: anthropic("claude-sonnet-4-20250514"),

  messages: await convertToModelMessages(this.messages),

});


```

TypeScript

```

import { createAnthropic } from "@ai-sdk/anthropic";


const anthropic = createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY });

const result = streamText({

  model: anthropic("claude-sonnet-4-20250514"),

  messages: await convertToModelMessages(this.messages),

});


```

## Advanced patterns

Since `onChatMessage` gives you full control over the `streamText` call, you can use any AI SDK feature directly. The patterns below all work out of the box — no special `AIChatAgent` configuration is needed.

### Dynamic model and tool control

Use [prepareStep ↗](https://ai-sdk.dev/docs/agents/loop-control) to change the model, available tools, or system prompt between steps in a multi-step agent loop:

* [  JavaScript ](#tab-panel-2238)
* [  TypeScript ](#tab-panel-2239)

JavaScript

```

import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";

import { z } from "zod";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: cheapModel, // Default model for simple steps

      messages: await convertToModelMessages(this.messages),

      tools: {

        search: searchTool,

        analyze: analyzeTool,

        summarize: summarizeTool,

      },

      stopWhen: stepCountIs(10),

      prepareStep: async ({ stepNumber, messages }) => {

        // Phase 1: Search (steps 0-2)

        if (stepNumber <= 2) {

          return {

            activeTools: ["search"],

            toolChoice: "required", // Force tool use

          };

        }


        // Phase 2: Analyze with a stronger model (steps 3-5)

        if (stepNumber <= 5) {

          return {

            model: expensiveModel,

            activeTools: ["analyze"],

          };

        }


        // Phase 3: Summarize

        return { activeTools: ["summarize"] };

      },

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";

import { z } from "zod";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: cheapModel, // Default model for simple steps

      messages: await convertToModelMessages(this.messages),

      tools: {

        search: searchTool,

        analyze: analyzeTool,

        summarize: summarizeTool,

      },

      stopWhen: stepCountIs(10),

      prepareStep: async ({ stepNumber, messages }) => {

        // Phase 1: Search (steps 0-2)

        if (stepNumber <= 2) {

          return {

            activeTools: ["search"],

            toolChoice: "required", // Force tool use

          };

        }


        // Phase 2: Analyze with a stronger model (steps 3-5)

        if (stepNumber <= 5) {

          return {

            model: expensiveModel,

            activeTools: ["analyze"],

          };

        }


        // Phase 3: Summarize

        return { activeTools: ["summarize"] };

      },

    });


    return result.toUIMessageStreamResponse();

  }

}


```

`prepareStep` runs before each step and can return overrides for `model`, `activeTools`, `toolChoice`, `system`, and `messages`. Use it to:

* **Switch models** — use a cheap model for simple steps, escalate for reasoning
* **Phase tools** — restrict which tools are available at each step
* **Manage context** — prune or transform messages to stay within token limits
* **Force tool calls** — use `toolChoice: { type: "tool", toolName: "search" }` to require a specific tool

### Language model middleware

Use [wrapLanguageModel ↗](https://ai-sdk.dev/docs/ai-sdk-core/middleware) to add guardrails, RAG, caching, or logging without modifying your chat logic:

* [  JavaScript ](#tab-panel-2236)
* [  TypeScript ](#tab-panel-2237)

JavaScript

```

import { streamText, convertToModelMessages, wrapLanguageModel } from "ai";

const guardrailMiddleware = {

  wrapGenerate: async ({ doGenerate }) => {

    const { text, ...rest } = await doGenerate();

    // Filter PII or sensitive content from the response

    const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");

    return { text: cleaned, ...rest };

  },

};


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const model = wrapLanguageModel({

      model: baseModel,

      middleware: [guardrailMiddleware],

    });


    const result = streamText({

      model,

      messages: await convertToModelMessages(this.messages),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import { streamText, convertToModelMessages, wrapLanguageModel } from "ai";

import type { LanguageModelV3Middleware } from "@ai-sdk/provider";


const guardrailMiddleware: LanguageModelV3Middleware = {

  wrapGenerate: async ({ doGenerate }) => {

    const { text, ...rest } = await doGenerate();

    // Filter PII or sensitive content from the response

    const cleaned = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED]");

    return { text: cleaned, ...rest };

  },

};


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const model = wrapLanguageModel({

      model: baseModel,

      middleware: [guardrailMiddleware],

    });


    const result = streamText({

      model,

      messages: await convertToModelMessages(this.messages),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

The AI SDK includes built-in middlewares:

* `extractReasoningMiddleware` — surface chain-of-thought from models like DeepSeek R1
* `defaultSettingsMiddleware` — apply default temperature, max tokens, etc.
* `simulateStreamingMiddleware` — add streaming to non-streaming models

Multiple middlewares compose in order: `middleware: [first, second]` applies as `first(second(model))`.

### Structured output

Use [generateObject ↗](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) inside tools for structured data extraction:

* [  JavaScript ](#tab-panel-2240)
* [  TypeScript ](#tab-panel-2241)

JavaScript

```

import {

  streamText,

  generateObject,

  convertToModelMessages,

  tool,

  stepCountIs,

} from "ai";

import { z } from "zod";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: myModel,

      messages: await convertToModelMessages(this.messages),

      tools: {

        extractContactInfo: tool({

          description:

            "Extract structured contact information from the conversation",

          inputSchema: z.object({

            text: z.string().describe("The text to extract contact info from"),

          }),

          execute: async ({ text }) => {

            const { object } = await generateObject({

              model: myModel,

              schema: z.object({

                name: z.string(),

                email: z.string().email(),

                phone: z.string().optional(),

              }),

              prompt: `Extract contact information from: ${text}`,

            });

            return object;

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import {

  streamText,

  generateObject,

  convertToModelMessages,

  tool,

  stepCountIs,

} from "ai";

import { z } from "zod";


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: myModel,

      messages: await convertToModelMessages(this.messages),

      tools: {

        extractContactInfo: tool({

          description:

            "Extract structured contact information from the conversation",

          inputSchema: z.object({

            text: z.string().describe("The text to extract contact info from"),

          }),

          execute: async ({ text }) => {

            const { object } = await generateObject({

              model: myModel,

              schema: z.object({

                name: z.string(),

                email: z.string().email(),

                phone: z.string().optional(),

              }),

              prompt: `Extract contact information from: ${text}`,

            });

            return object;

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

### Subagent delegation

Tools can delegate work to focused sub-calls with their own context. Use [ToolLoopAgent ↗](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool-loop-agent) to define a reusable agent, then call it from a tool's `execute`:

* [  JavaScript ](#tab-panel-2242)
* [  TypeScript ](#tab-panel-2243)

JavaScript

```

import {

  ToolLoopAgent,

  streamText,

  convertToModelMessages,

  tool,

  stepCountIs,

} from "ai";

import { z } from "zod";


// Define a reusable research agent with its own tools and instructions

const researchAgent = new ToolLoopAgent({

  model: researchModel,

  instructions: "You are a research assistant. Be thorough and cite sources.",

  tools: { webSearch: webSearchTool },

  stopWhen: stepCountIs(10),

});


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: orchestratorModel,

      messages: await convertToModelMessages(this.messages),

      tools: {

        deepResearch: tool({

          description: "Research a topic in depth",

          inputSchema: z.object({

            topic: z.string().describe("The topic to research"),

          }),

          execute: async ({ topic }) => {

            const { text } = await researchAgent.generate({

              prompt: topic,

            });

            return { summary: text };

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

TypeScript

```

import {

  ToolLoopAgent,

  streamText,

  convertToModelMessages,

  tool,

  stepCountIs,

} from "ai";

import { z } from "zod";


// Define a reusable research agent with its own tools and instructions

const researchAgent = new ToolLoopAgent({

  model: researchModel,

  instructions: "You are a research assistant. Be thorough and cite sources.",

  tools: { webSearch: webSearchTool },

  stopWhen: stepCountIs(10),

});


export class ChatAgent extends AIChatAgent {

  async onChatMessage() {

    const result = streamText({

      model: orchestratorModel,

      messages: await convertToModelMessages(this.messages),

      tools: {

        deepResearch: tool({

          description: "Research a topic in depth",

          inputSchema: z.object({

            topic: z.string().describe("The topic to research"),

          }),

          execute: async ({ topic }) => {

            const { text } = await researchAgent.generate({

              prompt: topic,

            });

            return { summary: text };

          },

        }),

      },

      stopWhen: stepCountIs(5),

    });


    return result.toUIMessageStreamResponse();

  }

}


```

The research agent runs in its own context — its token budget is separate from the orchestrator's. Only the summary goes back to the parent model.

Note

`ToolLoopAgent` is best suited for subagents, not as a replacement for `streamText` in `onChatMessage` itself. The main `onChatMessage` benefits from direct access to `this.env`, `this.messages`, and `options.body` — things that a pre-configured `ToolLoopAgent` instance cannot reference.

#### Streaming progress with preliminary results

By default, a tool part appears as loading until `execute` returns. Use an async generator (`async function*`) to stream progress updates to the client while the tool is still working:

* [  JavaScript ](#tab-panel-2232)
* [  TypeScript ](#tab-panel-2233)

JavaScript

```

deepResearch: tool({

  description: "Research a topic in depth",

  inputSchema: z.object({

    topic: z.string().describe("The topic to research"),

  }),

  async *execute({ topic }) {

    // Preliminary result — the client sees "searching" immediately

    yield { status: "searching", topic, summary: undefined };


    const { text } = await researchAgent.generate({ prompt: topic });


    // Final result — sent to the model for its next step

    yield { status: "done", topic, summary: text };

  },

});


```

TypeScript

```

deepResearch: tool({

  description: "Research a topic in depth",

  inputSchema: z.object({

    topic: z.string().describe("The topic to research"),

  }),

  async *execute({ topic }) {

    // Preliminary result — the client sees "searching" immediately

    yield { status: "searching", topic, summary: undefined };


    const { text } = await researchAgent.generate({ prompt: topic });


    // Final result — sent to the model for its next step

    yield { status: "done", topic, summary: text };

  },

});


```

Each `yield` updates the tool part on the client in real-time (with `preliminary: true`). The last yielded value becomes the final output that the model sees.

This pattern is useful when:

* A task requires exploring large amounts of information that would bloat the main context
* You want to show real-time progress for long-running tools
* You want to parallelize independent research (multiple tool calls run concurrently)
* You need different models or system prompts for different subtasks

For more, refer to the [AI SDK Agents docs ↗](https://ai-sdk.dev/docs/agents/overview), [Subagents ↗](https://ai-sdk.dev/docs/agents/subagents), and [Preliminary Tool Results ↗](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling#preliminary-tool-results).

## Multi-client sync

When multiple clients connect to the same agent instance, messages are automatically broadcast to all connections. If one client sends a message, all other connected clients receive the updated message list.

```

Client A ──── sendMessage("Hello") ────▶ AIChatAgent

                                              │

                                        persist + stream

                                              │

Client A ◀── CF_AGENT_USE_CHAT_RESPONSE ──────┤

Client B ◀── CF_AGENT_CHAT_MESSAGES ──────────┘


```

The originating client receives the streaming response. All other clients receive the final messages via a `CF_AGENT_CHAT_MESSAGES` broadcast.

## API reference

### Exports

| Import path               | Exports                                       |
| ------------------------- | --------------------------------------------- |
| @cloudflare/ai-chat       | AIChatAgent, createToolsFromClientSchemas     |
| @cloudflare/ai-chat/react | useAgentChat                                  |
| @cloudflare/ai-chat/types | MessageType, OutgoingMessage, IncomingMessage |

### WebSocket protocol

The chat protocol uses typed JSON messages over WebSocket:

| Message                            | Direction       | Purpose                     |
| ---------------------------------- | --------------- | --------------------------- |
| CF\_AGENT\_USE\_CHAT\_REQUEST      | Client → Server | Send a chat message         |
| CF\_AGENT\_USE\_CHAT\_RESPONSE     | Server → Client | Stream response chunks      |
| CF\_AGENT\_CHAT\_MESSAGES          | Server → Client | Broadcast updated messages  |
| CF\_AGENT\_CHAT\_CLEAR             | Bidirectional   | Clear conversation          |
| CF\_AGENT\_CHAT\_REQUEST\_CANCEL   | Client → Server | Cancel active stream        |
| CF\_AGENT\_TOOL\_RESULT            | Client → Server | Provide tool output         |
| CF\_AGENT\_TOOL\_APPROVAL          | Client → Server | Approve or reject a tool    |
| CF\_AGENT\_MESSAGE\_UPDATED        | Server → Client | Notify of message update    |
| CF\_AGENT\_STREAM\_RESUMING        | Server → Client | Notify of stream resumption |
| CF\_AGENT\_STREAM\_RESUME\_REQUEST | Client → Server | Request stream resume check |

## Deprecated APIs

The following APIs are deprecated and will emit a console warning when used. They will be removed in a future release.

| Deprecated                            | Replacement                                   | Notes                                           |
| ------------------------------------- | --------------------------------------------- | ----------------------------------------------- |
| addToolResult({ toolCallId, result }) | addToolOutput({ toolCallId, output })         | Renamed for consistency with AI SDK terminology |
| createToolsFromClientSchemas()        | Client tools are now registered automatically | No manual schema conversion needed              |
| extractClientToolSchemas()            | Client tools are now registered automatically | Schemas are sent with tool results              |
| detectToolsRequiringConfirmation()    | Use needsApproval on the tool definition      | Approval is now per-tool, not a global filter   |
| tools option on useAgentChat          | Define tools in onChatMessage on the server   | All tool definitions belong on the server       |
| toolsRequiringConfirmation option     | Use needsApproval on individual tools         | Per-tool approval replaces global list          |

If you are upgrading from an earlier version, replace deprecated calls with their replacements. The deprecated APIs still work but will be removed in a future major version.

## Next steps

[ Client SDK ](https://developers.cloudflare.com/agents/api-reference/client-sdk/) useAgent hook and AgentClient class. 

[ Human-in-the-loop ](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) Approval flows and manual intervention patterns. 

[ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) Step-by-step tutorial for building your first chat agent. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/chat-agents/","name":"Chat agents"}}]}
```

---

---
title: Client SDK
description: 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.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/client-sdk.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Client SDK

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

* [  JavaScript ](#tab-panel-2252)
* [  TypeScript ](#tab-panel-2253)

JavaScript

```

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>;

}


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-2248)
* [  TypeScript ](#tab-panel-2249)

JavaScript

```

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!"]);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-2244)
* [  TypeScript ](#tab-panel-2245)

JavaScript

```

// These are equivalent:

useAgent({ agent: "ChatAgent" }); // → /agents/chat-agent/...

useAgent({ agent: "MyCustomAgent" }); // → /agents/my-custom-agent/...

useAgent({ agent: "LOUD_AGENT" }); // → /agents/loud-agent/...


```

TypeScript

```

// 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"`:

* [  JavaScript ](#tab-panel-2246)
* [  TypeScript ](#tab-panel-2247)

JavaScript

```

// 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" });


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-2262)
* [  TypeScript ](#tab-panel-2263)

JavaScript

```

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),

});


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-2258)
* [  TypeScript ](#tab-panel-2259)

JavaScript

```

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

});


```

TypeScript

```

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.

### Receiving state updates

* [  JavaScript ](#tab-panel-2254)
* [  TypeScript ](#tab-panel-2255)

JavaScript

```

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);

  },

});


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-2250)
* [  TypeScript ](#tab-panel-2251)

JavaScript

```

// Update the agent's state from the client

agent.setState({ score: 100, level: 5 });


```

TypeScript

```

// 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://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()

* [  JavaScript ](#tab-panel-2256)
* [  TypeScript ](#tab-panel-2257)

JavaScript

```

// 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");


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-2260)
* [  TypeScript ](#tab-panel-2261)

JavaScript

```

// 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

```

// 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:

* [  JavaScript ](#tab-panel-2264)
* [  TypeScript ](#tab-panel-2265)

JavaScript

```

const agent = useAgent({

  agent: "MyAgent",

  name: "instance-1",

});


// Now stub methods are fully typed

const result = await agent.stub.processData({ input: "test" });


```

TypeScript

```

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

For methods that return `StreamingResponse`, handle chunks as they arrive:

* [  JavaScript ](#tab-panel-2282)
* [  TypeScript ](#tab-panel-2283)

JavaScript

```

// Agent-side:

class MyAgent extends Agent {

  @callable({ streaming: true })

  async generateText(stream, prompt) {

    for await (const chunk of llm.stream(prompt)) {

      await stream.write(chunk);

    }

  }

}


// 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);

  },

});


```

TypeScript

```

// Agent-side:

class MyAgent extends Agent {

  @callable({ streaming: true })

  async generateText(stream: StreamingResponse, prompt: string) {

    for await (const chunk of llm.stream(prompt)) {

      await stream.write(chunk);

    }

  }

}


// 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:

* [  JavaScript ](#tab-panel-2284)
* [  TypeScript ](#tab-panel-2285)

JavaScript

```

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" }),

  },

);


```

TypeScript

```

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:

* [  JavaScript ](#tab-panel-2268)
* [  TypeScript ](#tab-panel-2269)

JavaScript

```

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(", ")}`);

    }

  },

});


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-2270)
* [  TypeScript ](#tab-panel-2271)

JavaScript

```

const agent = useAgent({

  agent: "MyAgent",

  onError: (error) => {

    console.error("WebSocket error:", error);

  },

  onClose: () => {

    console.log("Connection closed, will auto-reconnect...");

  },

});


```

TypeScript

```

const agent = useAgent({

  agent: "MyAgent",

  onError: (error) => {

    console.error("WebSocket error:", error);

  },

  onClose: () => {

    console.log("Connection closed, will auto-reconnect...");

  },

});


```

### RPC errors

* [  JavaScript ](#tab-panel-2266)
* [  TypeScript ](#tab-panel-2267)

JavaScript

```

try {

  const result = await agent.call("riskyMethod", [data]);

} catch (error) {

  // Error thrown by the agent method

  console.error("RPC failed:", error.message);

}


```

TypeScript

```

try {

  const result = await agent.call("riskyMethod", [data]);

} catch (error) {

  // Error thrown by the agent method

  console.error("RPC failed:", error.message);

}


```

### Streaming errors

* [  JavaScript ](#tab-panel-2272)
* [  TypeScript ](#tab-panel-2273)

JavaScript

```

await agent.call("streamingMethod", [data], {

  onChunk: (chunk) => handleChunk(chunk),

  onError: (errorMessage) => {

    // Stream-specific error handling

    console.error("Stream error:", errorMessage);

  },

});


```

TypeScript

```

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

* [  JavaScript ](#tab-panel-2274)
* [  TypeScript ](#tab-panel-2275)

JavaScript

```

// Prefer this:

const user = await agent.stub.getUser(id);


// Over this:

const user = await agent.call("getUser", [id]);


```

TypeScript

```

// 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

* [  JavaScript ](#tab-panel-2276)
* [  TypeScript ](#tab-panel-2277)

JavaScript

```

// 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

});


```

TypeScript

```

// 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:

* [  JavaScript ](#tab-panel-2278)
* [  TypeScript ](#tab-panel-2279)

JavaScript

```

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


// When done:

client.close();


```

TypeScript

```

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


// When done:

client.close();


```

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

## React hook reference

### UseAgentOptions

TypeScript

```

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

TypeScript

```

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:

* [  JavaScript ](#tab-panel-2280)
* [  TypeScript ](#tab-panel-2281)

JavaScript

```

client.addEventListener("open", () => {});

client.addEventListener("close", () => {});

client.addEventListener("error", () => {});

client.addEventListener("message", () => {});


```

TypeScript

```

client.addEventListener("open", () => {});

client.addEventListener("close", () => {});

client.addEventListener("error", () => {});

client.addEventListener("message", () => {});


```

## Next steps

[ Routing ](https://developers.cloudflare.com/agents/api-reference/routing/) URL patterns and custom routing options. 

[ Callable methods ](https://developers.cloudflare.com/agents/api-reference/callable-methods/) RPC over WebSocket for client-server method calls. 

[ Cross-domain authentication ](https://developers.cloudflare.com/agents/guides/cross-domain-authentication/) Secure WebSocket connections across domains. 

[ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) Complete client integration with AI chat. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/client-sdk/","name":"Client SDK"}}]}
```

---

---
title: Codemode
description: Codemode lets LLMs write and execute code that orchestrates your tools, instead of calling them one at a time. Inspired by CodeAct, it works because LLMs are better at writing code than making individual tool calls — they have seen millions of lines of real-world code but only contrived tool-calling examples.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ AI ](https://developers.cloudflare.com/search/?tags=AI) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/codemode.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Codemode

Beta 

Codemode lets LLMs write and execute code that orchestrates your tools, instead of calling them one at a time. Inspired by [CodeAct ↗](https://machinelearning.apple.com/research/codeact), it works because LLMs are better at writing code than making individual tool calls — they have seen millions of lines of real-world code but only contrived tool-calling examples.

The `@cloudflare/codemode` package generates TypeScript type definitions from your tools, gives the LLM a single "write code" tool, and executes the generated JavaScript in a secure, isolated Worker sandbox.

Warning

Codemode is experimental and may have breaking changes in future releases. Use with caution in production.

## When to use Codemode

Codemode is most useful when the LLM needs to:

* **Chain multiple tool calls** with logic between them (conditionals, loops, error handling)
* **Compose results** from different tools before returning
* **Work with MCP servers** that expose many fine-grained operations
* **Perform multi-step workflows** that would require many round-trips with standard tool calling

For simple, single tool calls, standard AI SDK tool calling is simpler and sufficient.

## Installation

Terminal window

```

npm install @cloudflare/codemode


```

If you use `@cloudflare/codemode/ai`, also install the `ai` and `zod` peer dependencies:

Terminal window

```

npm install ai zod


```

## Quick start

### 1\. Define your tools

Use the standard AI SDK `tool()` function:

* [  JavaScript ](#tab-panel-2298)
* [  TypeScript ](#tab-panel-2299)

JavaScript

```

import { tool } from "ai";

import { z } from "zod";


const tools = {

  getWeather: tool({

    description: "Get weather for a location",

    inputSchema: z.object({ location: z.string() }),

    execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`,

  }),

  sendEmail: tool({

    description: "Send an email",

    inputSchema: z.object({

      to: z.string(),

      subject: z.string(),

      body: z.string(),

    }),

    execute: async ({ to, subject, body }) => `Email sent to ${to}`,

  }),

};


```

TypeScript

```

import { tool } from "ai";

import { z } from "zod";


const tools = {

  getWeather: tool({

    description: "Get weather for a location",

    inputSchema: z.object({ location: z.string() }),

    execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`,

  }),

  sendEmail: tool({

    description: "Send an email",

    inputSchema: z.object({

      to: z.string(),

      subject: z.string(),

      body: z.string(),

    }),

    execute: async ({ to, subject, body }) => `Email sent to ${to}`,

  }),

};


```

### 2\. Create the codemode tool

`createCodeTool` takes your tools and an executor, and returns a single AI SDK tool:

* [  JavaScript ](#tab-panel-2288)
* [  TypeScript ](#tab-panel-2289)

JavaScript

```

import { createCodeTool } from "@cloudflare/codemode/ai";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";


const executor = new DynamicWorkerExecutor({

  loader: env.LOADER,

});


const codemode = createCodeTool({ tools, executor });


```

TypeScript

```

import { createCodeTool } from "@cloudflare/codemode/ai";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";


const executor = new DynamicWorkerExecutor({

  loader: env.LOADER,

});


const codemode = createCodeTool({ tools, executor });


```

### 3\. Use with streamText

Pass the codemode tool to `streamText` or `generateText` like any other tool. You choose the model:

* [  JavaScript ](#tab-panel-2292)
* [  TypeScript ](#tab-panel-2293)

JavaScript

```

import { streamText } from "ai";


const result = streamText({

  model,

  system: "You are a helpful assistant.",

  messages,

  tools: { codemode },

});


```

TypeScript

```

import { streamText } from "ai";


const result = streamText({

  model,

  system: "You are a helpful assistant.",

  messages,

  tools: { codemode },

});


```

When the LLM decides to use codemode, it writes an async arrow function like:

JavaScript

```

async () => {

  const weather = await codemode.getWeather({ location: "London" });

  if (weather.includes("sunny")) {

    await codemode.sendEmail({

      to: "team@example.com",

      subject: "Nice day!",

      body: `It's ${weather}`,

    });

  }

  return { weather, notified: true };

};


```

The code runs in an isolated Worker sandbox, tool calls are dispatched back to the host via Workers RPC, and the result is returned to the LLM.

## Configuration

### Wrangler bindings

Add a `worker_loaders` binding to your `wrangler.jsonc`. This is the only binding required:

* [  wrangler.jsonc ](#tab-panel-2286)
* [  wrangler.toml ](#tab-panel-2287)

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "worker_loaders": [

    {

      "binding": "LOADER"

    }

  ],

  "compatibility_flags": [

    "nodejs_compat"

  ]

}


```

```

worker_loaders = [{ binding = "LOADER" }]

compatibility_flags = ["nodejs_compat"]


```

## How it works

1. `createCodeTool` generates TypeScript type definitions from your tools and builds a description the LLM can read.
2. The LLM writes an async arrow function that calls `codemode.toolName(args)`.
3. The code is normalized via AST parsing (acorn) and sent to the executor.
4. `DynamicWorkerExecutor` spins up an isolated Worker via `WorkerLoader`.
5. Inside the sandbox, a `Proxy` intercepts `codemode.*` calls and routes them back to the host via Workers RPC (`ToolDispatcher extends RpcTarget`).
6. Console output (`console.log`, `console.warn`, `console.error`) is captured and returned in the result.

### Network isolation

External `fetch()` and `connect()` are blocked by default — enforced at the Workers runtime level via `globalOutbound: null`. Sandboxed code can only interact with the host through `codemode.*` tool calls.

To allow controlled outbound access, pass a `Fetcher`:

* [  JavaScript ](#tab-panel-2290)
* [  TypeScript ](#tab-panel-2291)

JavaScript

```

const executor = new DynamicWorkerExecutor({

  loader: env.LOADER,

  globalOutbound: null, // default — fully isolated

  // globalOutbound: env.MY_OUTBOUND_SERVICE  // route through a Fetcher

});


```

TypeScript

```

const executor = new DynamicWorkerExecutor({

  loader: env.LOADER,

  globalOutbound: null, // default — fully isolated

  // globalOutbound: env.MY_OUTBOUND_SERVICE  // route through a Fetcher

});


```

## Using with an Agent

The typical pattern is to create the executor and codemode tool inside an Agent's message handler:

* [  JavaScript ](#tab-panel-2308)
* [  TypeScript ](#tab-panel-2309)

JavaScript

```

import { Agent } from "agents";

import { createCodeTool } from "@cloudflare/codemode/ai";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";

import { streamText, convertToModelMessages, stepCountIs } from "ai";


export class MyAgent extends Agent {

  async onChatMessage() {

    const executor = new DynamicWorkerExecutor({

      loader: this.env.LOADER,

    });


    const codemode = createCodeTool({

      tools: myTools,

      executor,

    });


    const result = streamText({

      model,

      system: "You are a helpful assistant.",

      messages: await convertToModelMessages(this.state.messages),

      tools: { codemode },

      stopWhen: stepCountIs(10),

    });


    // Stream response back to client...

  }

}


```

TypeScript

```

import { Agent } from "agents";

import { createCodeTool } from "@cloudflare/codemode/ai";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";

import { streamText, convertToModelMessages, stepCountIs } from "ai";


export class MyAgent extends Agent<Env, State> {

  async onChatMessage() {

    const executor = new DynamicWorkerExecutor({

      loader: this.env.LOADER,

    });


    const codemode = createCodeTool({

      tools: myTools,

      executor,

    });


    const result = streamText({

      model,

      system: "You are a helpful assistant.",

      messages: await convertToModelMessages(this.state.messages),

      tools: { codemode },

      stopWhen: stepCountIs(10),

    });


    // Stream response back to client...

  }

}


```

### With MCP tools

MCP tools work the same way — merge them into the tool set:

* [  JavaScript ](#tab-panel-2294)
* [  TypeScript ](#tab-panel-2295)

JavaScript

```

const codemode = createCodeTool({

  tools: {

    ...myTools,

    ...this.mcp.getAITools(),

  },

  executor,

});


```

TypeScript

```

const codemode = createCodeTool({

  tools: {

    ...myTools,

    ...this.mcp.getAITools(),

  },

  executor,

});


```

Tool names with hyphens or dots (common in MCP) are automatically sanitized to valid JavaScript identifiers (for example, `my-server.list-items` becomes `my_server_list_items`).

## MCP server wrappers

The `@cloudflare/codemode/mcp` export provides two functions that wrap MCP servers with Code Mode.

### `codeMcpServer`

Wraps an existing MCP server with a single `code` tool. Each upstream tool becomes a typed `codemode.*` method inside the sandbox:

* [  JavaScript ](#tab-panel-2296)
* [  TypeScript ](#tab-panel-2297)

JavaScript

```

import { codeMcpServer } from "@cloudflare/codemode/mcp";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";


const executor = new DynamicWorkerExecutor({ loader: env.LOADER });

const server = await codeMcpServer({ server: upstreamMcp, executor });


```

TypeScript

```

import { codeMcpServer } from "@cloudflare/codemode/mcp";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";


const executor = new DynamicWorkerExecutor({ loader: env.LOADER });

const server = await codeMcpServer({ server: upstreamMcp, executor });


```

### `openApiMcpServer`

Creates an MCP server with `search` and `execute` tools from an OpenAPI spec. All `$ref` pointers are resolved before being passed to the sandbox, and the host-side `request` handler keeps authentication out of the sandbox:

* [  JavaScript ](#tab-panel-2306)
* [  TypeScript ](#tab-panel-2307)

JavaScript

```

import { openApiMcpServer } from "@cloudflare/codemode/mcp";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";


const executor = new DynamicWorkerExecutor({ loader: env.LOADER });

const server = openApiMcpServer({

  spec: openApiSpec,

  executor,

  request: async ({ method, path, query, body }) => {

    // Runs on the host — add auth headers here

    const res = await fetch(`https://api.example.com${path}`, {

      method,

      headers: { Authorization: `Bearer ${token}` },

      body: body ? JSON.stringify(body) : undefined,

    });

    return res.json();

  },

});


```

TypeScript

```

import { openApiMcpServer } from "@cloudflare/codemode/mcp";

import { DynamicWorkerExecutor } from "@cloudflare/codemode";


const executor = new DynamicWorkerExecutor({ loader: env.LOADER });

const server = openApiMcpServer({

  spec: openApiSpec,

  executor,

  request: async ({ method, path, query, body }) => {

    // Runs on the host — add auth headers here

    const res = await fetch(`https://api.example.com${path}`, {

      method,

      headers: { Authorization: `Bearer ${token}` },

      body: body ? JSON.stringify(body) : undefined,

    });

    return res.json();

  },

});


```

## The Executor interface

The `Executor` interface is deliberately minimal — implement it to run code in any sandbox:

TypeScript

```

interface Executor {

  execute(

    code: string,

    fns: Record<string, (...args: unknown[]) => Promise<unknown>>,

  ): Promise<ExecuteResult>;

}


interface ExecuteResult {

  result: unknown;

  error?: string;

  logs?: string[];

}


```

`DynamicWorkerExecutor` is the built-in Cloudflare Workers implementation. You can build your own for Node VM, QuickJS, containers, or any other sandbox.

## API reference

### `createCodeTool(options)`

Returns an AI SDK compatible `Tool`.

| Option      | Type                       | Default        | Description                                                  |
| ----------- | -------------------------- | -------------- | ------------------------------------------------------------ |
| tools       | ToolSet \| ToolDescriptors | required       | Your tools (AI SDK tool() or raw descriptors)                |
| executor    | Executor                   | required       | Where to run the generated code                              |
| description | string                     | auto-generated | Custom tool description. Use \\{\\{types\\}\\} for type defs |

### `DynamicWorkerExecutor`

Executes code in an isolated Cloudflare Worker via `WorkerLoader`.

| Option         | Type                   | Default  | Description                                                                         |
| -------------- | ---------------------- | -------- | ----------------------------------------------------------------------------------- |
| loader         | WorkerLoader           | required | Worker Loader binding from env.LOADER                                               |
| timeout        | number                 | 30000    | Execution timeout in ms                                                             |
| globalOutbound | Fetcher \| null        | null     | Network access control. null \= blocked, Fetcher \= routed                          |
| modules        | Record<string, string> | —        | Custom ES modules available in the sandbox. Keys are specifiers, values are source. |

Code and tool names are normalized and sanitized internally — you do not need to call `normalizeCode()` or `sanitizeToolName()` before passing them to `execute()`.

### `generateTypes(tools)`

Generates TypeScript type definitions from your tools. Used internally by `createCodeTool` but exported for custom use (for example, displaying types in a frontend).

* [  JavaScript ](#tab-panel-2302)
* [  TypeScript ](#tab-panel-2303)

JavaScript

```

import { generateTypes } from "@cloudflare/codemode/ai";


const types = generateTypes(myTools);

// Returns:

// type CreateProjectInput = { name: string; description?: string }

// declare const codemode: {

//   createProject: (input: CreateProjectInput) => Promise<unknown>;

// }


```

TypeScript

```

import { generateTypes } from "@cloudflare/codemode/ai";


const types = generateTypes(myTools);

// Returns:

// type CreateProjectInput = { name: string; description?: string }

// declare const codemode: {

//   createProject: (input: CreateProjectInput) => Promise<unknown>;

// }


```

For JSON Schema inputs that do not depend on the AI SDK, use the main entry point:

* [  JavaScript ](#tab-panel-2300)
* [  TypeScript ](#tab-panel-2301)

JavaScript

```

import { generateTypesFromJsonSchema } from "@cloudflare/codemode";


const types = generateTypesFromJsonSchema(jsonSchemaToolDescriptors);


```

TypeScript

```

import { generateTypesFromJsonSchema } from "@cloudflare/codemode";


const types = generateTypesFromJsonSchema(jsonSchemaToolDescriptors);


```

### `sanitizeToolName(name)`

Converts tool names into valid JavaScript identifiers.

* [  JavaScript ](#tab-panel-2304)
* [  TypeScript ](#tab-panel-2305)

JavaScript

```

import { sanitizeToolName } from "@cloudflare/codemode";


sanitizeToolName("get-weather"); // "get_weather"

sanitizeToolName("3d-render"); // "_3d_render"

sanitizeToolName("delete"); // "delete_"


```

TypeScript

```

import { sanitizeToolName } from "@cloudflare/codemode";


sanitizeToolName("get-weather"); // "get_weather"

sanitizeToolName("3d-render"); // "_3d_render"

sanitizeToolName("delete"); // "delete_"


```

## Security considerations

* Code runs in **isolated Worker sandboxes** — each execution gets its own Worker instance.
* External network access (`fetch`, `connect`) is **blocked by default** at the runtime level.
* Tool calls are dispatched via Workers RPC, not network requests.
* Execution has a configurable **timeout** (default 30 seconds).
* Console output is captured separately and does not leak to the host.

## Current limitations

* **Tool approval (`needsApproval`) is not supported yet.** Tools with `needsApproval: true` execute immediately inside the sandbox without pausing for approval. Support for approval flows within codemode is planned. For now, do not pass approval-required tools to `createCodeTool` — use them through standard AI SDK tool calling instead.
* Requires Cloudflare Workers environment for `DynamicWorkerExecutor`.
* Limited to JavaScript execution.
* LLM code quality depends on prompt engineering and model capability.

## Related resources

[ Codemode example ](https://github.com/cloudflare/agents/tree/main/examples/codemode) Full working example — a project management assistant using codemode with SQLite. 

[ Using AI Models ](https://developers.cloudflare.com/agents/api-reference/using-ai-models/) Use AI models with your Agent. 

[ MCP Client ](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/) Connect to MCP servers and use their tools with codemode. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/codemode/","name":"Codemode"}}]}
```

---

---
title: Configuration
description: This guide covers everything you need to configure agents for local development and production deployment, including Wrangler configuration file setup, type generation, environment variables, and the Cloudflare dashboard.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/configuration.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Configuration

This guide covers everything you need to configure agents for local development and production deployment, including Wrangler configuration file setup, type generation, environment variables, and the Cloudflare dashboard.

## Project structure

The typical file structure for an Agent project created from `npm create cloudflare@latest agents-starter -- --template cloudflare/agents-starter` follows:

* Directorysrc/  
   * index.ts your Agent definition
* Directorypublic/  
   * index.html
* Directorytest/  
   * index.spec.ts your tests
* package.json
* tsconfig.json
* vitest.config.mts
* worker-configuration.d.ts
* wrangler.jsonc your Workers and Agent configuration

## Wrangler configuration file

The `wrangler.jsonc` file configures your Cloudflare Worker and its bindings. Here is a complete example for an agents project:

* [  wrangler.jsonc ](#tab-panel-2330)
* [  wrangler.toml ](#tab-panel-2331)

```

{

  "$schema": "node_modules/wrangler/config-schema.json",

  "name": "my-agent-app",

  "main": "src/server.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],


  // Static assets (optional)

  "assets": {

    "directory": "public",

    "binding": "ASSETS",

  },


  // Durable Object bindings for agents

  "durable_objects": {

    "bindings": [

      {

        "name": "MyAgent",

        "class_name": "MyAgent",

      },

      {

        "name": "ChatAgent",

        "class_name": "ChatAgent",

      },

    ],

  },


  // Required: Enable SQLite storage for agents

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["MyAgent", "ChatAgent"],

    },

  ],


  // AI binding (optional, for Workers AI)

  "ai": {

    "binding": "AI",

  },


  // Observability (recommended)

  "observability": {

    "enabled": true,

  },

}


```

```

"$schema" = "node_modules/wrangler/config-schema.json"

name = "my-agent-app"

main = "src/server.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[assets]

directory = "public"

binding = "ASSETS"


[[durable_objects.bindings]]

name = "MyAgent"

class_name = "MyAgent"


[[durable_objects.bindings]]

name = "ChatAgent"

class_name = "ChatAgent"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "MyAgent", "ChatAgent" ]


[ai]

binding = "AI"


[observability]

enabled = true


```

### Key fields

#### `compatibility_flags`

The `nodejs_compat` flag is required for agents:

* [  wrangler.jsonc ](#tab-panel-2310)
* [  wrangler.toml ](#tab-panel-2311)

```

{

  "compatibility_flags": ["nodejs_compat"],

}


```

```

compatibility_flags = [ "nodejs_compat" ]


```

This enables Node.js compatibility mode, which agents depend on for crypto, streams, and other Node.js APIs.

#### `durable_objects.bindings`

Each agent class needs a binding:

* [  wrangler.jsonc ](#tab-panel-2312)
* [  wrangler.toml ](#tab-panel-2313)

```

{

  "durable_objects": {

    "bindings": [

      {

        "name": "Counter",

        "class_name": "Counter",

      },

    ],

  },

}


```

```

[[durable_objects.bindings]]

name = "Counter"

class_name = "Counter"


```

| Field       | Description                                             |
| ----------- | ------------------------------------------------------- |
| name        | The property name on env. Use this in code: env.Counter |
| class\_name | Must match the exported class name exactly              |

When `name` and `class_name` differ

When `name` and `class_name` differ, follow the pattern shown below:

* [  wrangler.jsonc ](#tab-panel-2314)
* [  wrangler.toml ](#tab-panel-2315)

```

{

  "durable_objects": {

    "bindings": [

      {

        "name": "COUNTER_DO",

        "class_name": "CounterAgent",

      },

    ],

  },

}


```

```

[[durable_objects.bindings]]

name = "COUNTER_DO"

class_name = "CounterAgent"


```

This is useful when you want environment variable-style naming (`COUNTER_DO`) but more descriptive class names (`CounterAgent`).

#### `migrations`

Migrations tell Cloudflare how to set up storage for your Durable Objects:

* [  wrangler.jsonc ](#tab-panel-2316)
* [  wrangler.toml ](#tab-panel-2317)

```

{

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["MyAgent"],

    },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "MyAgent" ]


```

| Field                | Description                                                  |
| -------------------- | ------------------------------------------------------------ |
| tag                  | Version identifier (for example, "v1", "v2"). Must be unique |
| new\_sqlite\_classes | Agent classes that use SQLite storage (state persistence)    |
| deleted\_classes     | Classes being removed                                        |
| renamed\_classes     | Classes being renamed                                        |

#### `assets`

For serving static files (HTML, CSS, JS):

* [  wrangler.jsonc ](#tab-panel-2318)
* [  wrangler.toml ](#tab-panel-2319)

```

{

  "assets": {

    "directory": "public",

    "binding": "ASSETS",

  },

}


```

```

[assets]

directory = "public"

binding = "ASSETS"


```

With a binding, you can serve assets programmatically:

* [  JavaScript ](#tab-panel-2352)
* [  TypeScript ](#tab-panel-2353)

JavaScript

```

export default {

  async fetch(request, env) {

    // Static assets are served by the worker automatically by default


    // Route the request to the appropriate agent

    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    // Add your own routing logic here

    return new Response("Not found", { status: 404 });

  },

};


```

TypeScript

```

export default {

  async fetch(request: Request, env: Env) {

    // Static assets are served by the worker automatically by default


    // Route the request to the appropriate agent

    const agentResponse = await routeAgentRequest(request, env);

    if (agentResponse) return agentResponse;


    // Add your own routing logic here

    return new Response("Not found", { status: 404 });

  },

} satisfies ExportedHandler<Env>;


```

#### `ai`

For Workers AI integration:

* [  wrangler.jsonc ](#tab-panel-2320)
* [  wrangler.toml ](#tab-panel-2321)

```

{

  "ai": {

    "binding": "AI",

  },

}


```

```

[ai]

binding = "AI"


```

Access in your agent:

* [  JavaScript ](#tab-panel-2344)
* [  TypeScript ](#tab-panel-2345)

JavaScript

```

const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {

  prompt: "Hello!",

});


```

TypeScript

```

const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {

  prompt: "Hello!",

});


```

## Generating types

Wrangler can generate TypeScript types for your bindings.

### Automatic generation

Run the types command:

Terminal window

```

npx wrangler types


```

This creates or updates `worker-configuration.d.ts` with your `Env` type.

### Custom output path

Specify a custom path:

Terminal window

```

npx wrangler types env.d.ts


```

### Without runtime types

For cleaner output (recommended for agents):

Terminal window

```

npx wrangler types env.d.ts --include-runtime false


```

This generates just your bindings without Cloudflare runtime types.

### Example generated output

TypeScript

```

// env.d.ts (generated)

declare namespace Cloudflare {

  interface Env {

    OPENAI_API_KEY: string;

    Counter: DurableObjectNamespace;

    ChatAgent: DurableObjectNamespace;

  }

}

interface Env extends Cloudflare.Env {}


```

### Manual type definition

You can also define types manually:

* [  JavaScript ](#tab-panel-2356)
* [  TypeScript ](#tab-panel-2357)

JavaScript

```

// env.d.ts


```

TypeScript

```

// env.d.ts

import type { Counter } from "./src/agents/counter";

import type { ChatAgent } from "./src/agents/chat";


interface Env {

  // Secrets

  OPENAI_API_KEY: string;

  WEBHOOK_SECRET: string;


  // Agent bindings

  Counter: DurableObjectNamespace<Counter>;

  ChatAgent: DurableObjectNamespace<ChatAgent>;


  // Other bindings

  AI: Ai;

  ASSETS: Fetcher;

  MY_KV: KVNamespace;

}


```

### Adding to package.json

Add a script for easy regeneration:

```

{

  "scripts": {

    "types": "wrangler types env.d.ts --include-runtime false"

  }

}


```

## Environment variables and secrets

### Local development (`.env`)

Create a `.env` file for local secrets (add to `.gitignore`):

Terminal window

```

# .env

OPENAI_API_KEY=sk-...

GITHUB_WEBHOOK_SECRET=whsec_...

DATABASE_URL=postgres://...


```

Access in your agent:

* [  JavaScript ](#tab-panel-2350)
* [  TypeScript ](#tab-panel-2351)

JavaScript

```

class MyAgent extends Agent {

  async onStart() {

    const apiKey = this.env.OPENAI_API_KEY;

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onStart() {

    const apiKey = this.env.OPENAI_API_KEY;

  }

}


```

### Production secrets

Use `wrangler secret` for production:

Terminal window

```

# Add a secret

npx wrangler secret put OPENAI_API_KEY

# Enter value when prompted


# List secrets

npx wrangler secret list


# Delete a secret

npx wrangler secret delete OPENAI_API_KEY


```

### Non-secret variables

For non-sensitive configuration, use `vars` in the Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-2322)
* [  wrangler.toml ](#tab-panel-2323)

```

{

  "vars": {

    "API_BASE_URL": "https://api.example.com",

    "MAX_RETRIES": "3",

    "DEBUG_MODE": "false",

  },

}


```

```

[vars]

API_BASE_URL = "https://api.example.com"

MAX_RETRIES = "3"

DEBUG_MODE = "false"


```

All values must be strings. Parse numbers and booleans in code:

* [  JavaScript ](#tab-panel-2348)
* [  TypeScript ](#tab-panel-2349)

JavaScript

```

const maxRetries = parseInt(this.env.MAX_RETRIES, 10);

const debugMode = this.env.DEBUG_MODE === "true";


```

TypeScript

```

const maxRetries = parseInt(this.env.MAX_RETRIES, 10);

const debugMode = this.env.DEBUG_MODE === "true";


```

### Environment-specific variables

Use `env` sections for different environments (for example, staging, production):

* [  wrangler.jsonc ](#tab-panel-2328)
* [  wrangler.toml ](#tab-panel-2329)

```

{

  "name": "my-agent",

  "vars": {

    "API_URL": "https://api.example.com",

  },


  "env": {

    "staging": {

      "vars": {

        "API_URL": "https://staging-api.example.com",

      },

    },

    "production": {

      "vars": {

        "API_URL": "https://api.example.com",

      },

    },

  },

}


```

```

name = "my-agent"


[vars]

API_URL = "https://api.example.com"


[env.staging.vars]

API_URL = "https://staging-api.example.com"


[env.production.vars]

API_URL = "https://api.example.com"


```

Deploy to specific environment:

Terminal window

```

npx wrangler deploy --env staging

npx wrangler deploy --env production


```

## Local development

### Starting the dev server

With Vite (recommended for full stack apps):

Terminal window

```

npx vite dev


```

Without Vite:

Terminal window

```

npx wrangler dev


```

### Local state persistence

Durable Object state is persisted locally in `.wrangler/state/`:

* Directory.wrangler/  
   * Directorystate/  
         * Directoryv3/  
                  * Directoryd1/  
                              * Directoryminiflare-D1DatabaseObject/  
                                             * ... (SQLite files)

### Clearing local state

To reset all local Durable Object state:

Terminal window

```

rm -rf .wrangler/state


```

Or restart with fresh state:

Terminal window

```

npx wrangler dev --persist-to=""


```

### Inspecting local SQLite

You can inspect agent state directly:

Terminal window

```

# Find the SQLite file

ls .wrangler/state/v3/d1/


# Open with sqlite3

sqlite3 .wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite


```

## Dashboard setup

### Automatic resources

When you deploy, Cloudflare automatically creates:

* **Worker** \- Your deployed code
* **Durable Object namespaces** \- One per agent class
* **SQLite storage** \- Attached to each namespace

### Viewing Durable Objects

Log in to the Cloudflare dashboard, then go to Durable Objects.

[ Go to **Durable Objects** ](https://dash.cloudflare.com/?to=/:account/workers/durable-objects) 

Here you can:

* See all Durable Object namespaces
* View individual object instances
* Inspect storage (keys and values)
* Delete objects

### Real-time logs

View live logs from your agents:

Terminal window

```

npx wrangler tail


```

Or in the dashboard:

1. Go to your Worker.
2. Select the **Observability** tab.
3. Enable real-time logs.

Filter by:

* Status (success, error)
* Search text
* Sampling rate

## Production deployment

### Basic deploy

Terminal window

```

npx wrangler deploy


```

This:

1. Bundles your code
2. Uploads to Cloudflare
3. Applies migrations
4. Makes it live on `*.workers.dev`

### Custom domain

Add a route in the Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-2324)
* [  wrangler.toml ](#tab-panel-2325)

```

{

  "routes": [

    {

      "pattern": "agents.example.com/*",

      "zone_name": "example.com",

    },

  ],

}


```

```

[[routes]]

pattern = "agents.example.com/*"

zone_name = "example.com"


```

Or use a custom domain (simpler):

* [  wrangler.jsonc ](#tab-panel-2326)
* [  wrangler.toml ](#tab-panel-2327)

```

{

  "routes": [

    {

      "pattern": "agents.example.com",

      "custom_domain": true,

    },

  ],

}


```

```

[[routes]]

pattern = "agents.example.com"

custom_domain = true


```

### Preview deployments

Deploy without affecting production:

Terminal window

```

npx wrangler deploy --dry-run    # See what would be uploaded

npx wrangler versions upload     # Upload new version

npx wrangler versions deploy     # Gradually roll out


```

### Rollbacks

Roll back to a previous version:

Terminal window

```

npx wrangler rollback


```

## Multi-environment setup

### Environment configuration

Define environments in the Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-2354)
* [  wrangler.toml ](#tab-panel-2355)

```

{

  "name": "my-agent",

  "main": "src/server.ts",


  // Base configuration (shared)

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {

    "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }],

  },

  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],


  // Environment overrides

  "env": {

    "staging": {

      "name": "my-agent-staging",

      "vars": {

        "ENVIRONMENT": "staging",

      },

    },

    "production": {

      "name": "my-agent-production",

      "vars": {

        "ENVIRONMENT": "production",

      },

    },

  },

}


```

```

name = "my-agent"

main = "src/server.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[[durable_objects.bindings]]

name = "MyAgent"

class_name = "MyAgent"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "MyAgent" ]


[env.staging]

name = "my-agent-staging"


  [env.staging.vars]

  ENVIRONMENT = "staging"


[env.production]

name = "my-agent-production"


  [env.production.vars]

  ENVIRONMENT = "production"


```

### Deploying to environments

Terminal window

```

# Deploy to staging

npx wrangler deploy --env staging


# Deploy to production

npx wrangler deploy --env production


# Set secrets per environment

npx wrangler secret put OPENAI_API_KEY --env staging

npx wrangler secret put OPENAI_API_KEY --env production


```

### Separate Durable Objects

Each environment gets its own Durable Objects. Staging agents do not share state with production agents.

To explicitly separate:

* [  wrangler.jsonc ](#tab-panel-2334)
* [  wrangler.toml ](#tab-panel-2335)

```

{

  "env": {

    "staging": {

      "durable_objects": {

        "bindings": [

          {

            "name": "MyAgent",

            "class_name": "MyAgent",

            "script_name": "my-agent-staging",

          },

        ],

      },

    },

  },

}


```

```

[[env.staging.durable_objects.bindings]]

name = "MyAgent"

class_name = "MyAgent"

script_name = "my-agent-staging"


```

## Migrations

Migrations manage Durable Object storage schema changes.

### Adding a new agent

Add to `new_sqlite_classes` in a new migration:

* [  wrangler.jsonc ](#tab-panel-2332)
* [  wrangler.toml ](#tab-panel-2333)

```

{

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["ExistingAgent"],

    },

    {

      "tag": "v2",

      "new_sqlite_classes": ["NewAgent"],

    },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "ExistingAgent" ]


[[migrations]]

tag = "v2"

new_sqlite_classes = [ "NewAgent" ]


```

### Renaming an agent class

Use `renamed_classes`:

* [  wrangler.jsonc ](#tab-panel-2346)
* [  wrangler.toml ](#tab-panel-2347)

```

{

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["OldName"],

    },

    {

      "tag": "v2",

      "renamed_classes": [

        {

          "from": "OldName",

          "to": "NewName",

        },

      ],

    },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "OldName" ]


[[migrations]]

tag = "v2"


  [[migrations.renamed_classes]]

  from = "OldName"

  to = "NewName"


```

Also update:

1. The class name in code
2. The `class_name` in bindings
3. Export statements

### Deleting an agent class

Use `deleted_classes`:

* [  wrangler.jsonc ](#tab-panel-2340)
* [  wrangler.toml ](#tab-panel-2341)

```

{

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["AgentToDelete", "AgentToKeep"],

    },

    {

      "tag": "v2",

      "deleted_classes": ["AgentToDelete"],

    },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "AgentToDelete", "AgentToKeep" ]


[[migrations]]

tag = "v2"

deleted_classes = [ "AgentToDelete" ]


```

Warning

This permanently deletes all data for that class.

### Migration best practices

1. **Never modify existing migrations** \- Always add new ones.
2. **Use sequential tags** \- v1, v2, v3 (or use dates: 2025-01-15).
3. **Test locally first** \- Migrations run on deploy.
4. **Back up production data** \- Before renaming or deleting.

## Troubleshooting

### No such Durable Object class

The class is not in migrations:

* [  wrangler.jsonc ](#tab-panel-2336)
* [  wrangler.toml ](#tab-panel-2337)

```

{

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["MissingClassName"],

    },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "MissingClassName" ]


```

### Cannot find module in types

Regenerate types:

Terminal window

```

npx wrangler types env.d.ts --include-runtime false


```

### Secrets not loading locally

Check that `.env` exists and contains the variable:

Terminal window

```

cat .env

# Should show: MY_SECRET=value


```

### Migration tag conflict

Migration tags must be unique. If you see conflicts:

* [  wrangler.jsonc ](#tab-panel-2338)
* [  wrangler.toml ](#tab-panel-2339)

```

{

  // Wrong - duplicate tags

  "migrations": [

    { "tag": "v1", "new_sqlite_classes": ["A"] },

    { "tag": "v1", "new_sqlite_classes": ["B"] },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "A" ]


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "B" ]


```

* [  wrangler.jsonc ](#tab-panel-2342)
* [  wrangler.toml ](#tab-panel-2343)

```

{

  // Correct - sequential tags

  "migrations": [

    { "tag": "v1", "new_sqlite_classes": ["A"] },

    { "tag": "v2", "new_sqlite_classes": ["B"] },

  ],

}


```

```

[[migrations]]

tag = "v1"

new_sqlite_classes = [ "A" ]


[[migrations]]

tag = "v2"

new_sqlite_classes = [ "B" ]


```

## Next steps

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ Routing ](https://developers.cloudflare.com/agents/api-reference/routing/) Route requests to your agent instances. 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Background processing with delayed and cron-based tasks. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/configuration/","name":"Configuration"}}]}
```

---

---
title: Email routing
description: Agents can receive and process emails using Cloudflare Email Routing. This guide covers how to route inbound emails to your Agents and handle replies securely.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/email.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Email routing

Agents can receive and process emails using Cloudflare [Email Routing](https://developers.cloudflare.com/email-routing/email-workers/). This guide covers how to route inbound emails to your Agents and handle replies securely.

## Prerequisites

1. A domain configured with [Cloudflare Email Routing](https://developers.cloudflare.com/email-routing/).
2. An Email Worker configured to receive emails.
3. An Agent to process emails.

## Quick start

* [  JavaScript ](#tab-panel-2370)
* [  TypeScript ](#tab-panel-2371)

JavaScript

```

import { Agent, routeAgentEmail } from "agents";

import { createAddressBasedEmailResolver } from "agents/email";


// Your Agent that handles emails

export class EmailAgent extends Agent {

  async onEmail(email) {

    console.log("Received email from:", email.from);

    console.log("Subject:", email.headers.get("subject"));


    // Reply to the email

    await this.replyToEmail(email, {

      fromName: "My Agent",

      body: "Thanks for your email!",

    });

  }

}


// Route emails to your Agent

export default {

  async email(message, env) {

    await routeAgentEmail(message, env, {

      resolver: createAddressBasedEmailResolver("EmailAgent"),

    });

  },

};


```

TypeScript

```

import { Agent, routeAgentEmail } from "agents";

import { createAddressBasedEmailResolver, type AgentEmail } from "agents/email";


// Your Agent that handles emails

export class EmailAgent extends Agent {

  async onEmail(email: AgentEmail) {

    console.log("Received email from:", email.from);

    console.log("Subject:", email.headers.get("subject"));


    // Reply to the email

    await this.replyToEmail(email, {

      fromName: "My Agent",

      body: "Thanks for your email!",

    });

  }

}


// Route emails to your Agent

export default {

  async email(message, env) {

    await routeAgentEmail(message, env, {

      resolver: createAddressBasedEmailResolver("EmailAgent"),

    });

  },

} satisfies ExportedHandler<Env>;


```

## Resolvers

Resolvers determine which Agent instance receives an incoming email. Choose the resolver that matches your use case.

### `createAddressBasedEmailResolver`

Recommended for inbound mail. Routes emails based on the recipient address.

* [  JavaScript ](#tab-panel-2360)
* [  TypeScript ](#tab-panel-2361)

JavaScript

```

import { createAddressBasedEmailResolver } from "agents/email";


const resolver = createAddressBasedEmailResolver("EmailAgent");


```

TypeScript

```

import { createAddressBasedEmailResolver } from "agents/email";


const resolver = createAddressBasedEmailResolver("EmailAgent");


```

**Routing logic:**

| Recipient Address                     | Agent Name           | Agent ID |
| ------------------------------------- | -------------------- | -------- |
| support@example.com                   | EmailAgent (default) | support  |
| sales@example.com                     | EmailAgent (default) | sales    |
| NotificationAgent+user123@example.com | NotificationAgent    | user123  |

The sub-address format (`agent+id@domain`) allows routing to different agent namespaces and instances from a single email domain.

### `createSecureReplyEmailResolver`

For reply flows with signature verification. Verifies that incoming emails are authentic replies to your outbound emails, preventing attackers from routing emails to arbitrary agent instances.

* [  JavaScript ](#tab-panel-2362)
* [  TypeScript ](#tab-panel-2363)

JavaScript

```

import { createSecureReplyEmailResolver } from "agents/email";


const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);


```

TypeScript

```

import { createSecureReplyEmailResolver } from "agents/email";


const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);


```

When your agent sends an email with `replyToEmail()` and a `secret`, it signs the routing headers with a timestamp. When a reply comes back, this resolver verifies the signature and checks that it has not expired before routing.

**Options:**

* [  JavaScript ](#tab-panel-2366)
* [  TypeScript ](#tab-panel-2367)

JavaScript

```

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {

  // Maximum age of signature in seconds (default: 30 days)

  maxAge: 7 * 24 * 60 * 60, // 7 days


  // Callback for logging/debugging signature failures

  onInvalidSignature: (email, reason) => {

    console.warn(`Invalid signature from ${email.from}: ${reason}`);

    // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"

  },

});


```

TypeScript

```

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {

  // Maximum age of signature in seconds (default: 30 days)

  maxAge: 7 * 24 * 60 * 60, // 7 days


  // Callback for logging/debugging signature failures

  onInvalidSignature: (email, reason) => {

    console.warn(`Invalid signature from ${email.from}: ${reason}`);

    // reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"

  },

});


```

**When to use:** If your agent initiates email conversations and you need replies to route back to the same agent instance securely.

### `createCatchAllEmailResolver`

For single-instance routing. Routes all emails to a specific agent instance regardless of the recipient address.

* [  JavaScript ](#tab-panel-2364)
* [  TypeScript ](#tab-panel-2365)

JavaScript

```

import { createCatchAllEmailResolver } from "agents/email";


const resolver = createCatchAllEmailResolver("EmailAgent", "default");


```

TypeScript

```

import { createCatchAllEmailResolver } from "agents/email";


const resolver = createCatchAllEmailResolver("EmailAgent", "default");


```

**When to use:** When you have a single agent instance that handles all emails (for example, a shared inbox).

### Combining resolvers

You can combine resolvers to handle different scenarios:

* [  JavaScript ](#tab-panel-2382)
* [  TypeScript ](#tab-panel-2383)

JavaScript

```

export default {

  async email(message, env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        // First, check if this is a signed reply

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;


        // Otherwise, route based on recipient address

        return addressResolver(email, env);

      },


      // Handle emails that do not match any routing rule

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

};


```

TypeScript

```

export default {

  async email(message, env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        // First, check if this is a signed reply

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;


        // Otherwise, route based on recipient address

        return addressResolver(email, env);

      },


      // Handle emails that do not match any routing rule

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

} satisfies ExportedHandler<Env>;


```

## Handling emails in your Agent

### The AgentEmail interface

When your agent's `onEmail` method is called, it receives an `AgentEmail` object:

TypeScript

```

type AgentEmail = {

  from: string; // Sender's email address

  to: string; // Recipient's email address

  headers: Headers; // Email headers (subject, message-id, etc.)

  rawSize: number; // Size of the raw email in bytes


  getRaw(): Promise<Uint8Array>; // Get the full raw email content

  reply(options): Promise<void>; // Send a reply

  forward(rcptTo, headers?): Promise<void>; // Forward the email

  setReject(reason): void; // Reject the email with a reason

};


```

### Parsing email content

Use a library like [postal-mime ↗](https://www.npmjs.com/package/postal-mime) to parse the raw email:

* [  JavaScript ](#tab-panel-2372)
* [  TypeScript ](#tab-panel-2373)

JavaScript

```

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log("Subject:", parsed.subject);

    console.log("Text body:", parsed.text);

    console.log("HTML body:", parsed.html);

    console.log("Attachments:", parsed.attachments);

  }

}


```

TypeScript

```

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log("Subject:", parsed.subject);

    console.log("Text body:", parsed.text);

    console.log("HTML body:", parsed.html);

    console.log("Attachments:", parsed.attachments);

  }

}


```

### Detecting auto-reply emails

Use `isAutoReplyEmail()` to detect auto-reply emails and avoid mail loops:

* [  JavaScript ](#tab-panel-2376)
* [  TypeScript ](#tab-panel-2377)

JavaScript

```

import { isAutoReplyEmail } from "agents/email";

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    // Detect auto-reply emails to avoid sending duplicate responses

    if (isAutoReplyEmail(parsed.headers)) {

      console.log("Skipping auto-reply email");

      return;

    }


    // Process the email...

  }

}


```

TypeScript

```

import { isAutoReplyEmail } from "agents/email";

import PostalMime from "postal-mime";


class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    // Detect auto-reply emails to avoid sending duplicate responses

    if (isAutoReplyEmail(parsed.headers)) {

      console.log("Skipping auto-reply email");

      return;

    }


    // Process the email...

  }

}


```

This checks for standard RFC 3834 headers (`Auto-Submitted`, `X-Auto-Response-Suppress`, `Precedence`) that indicate an email is an auto-reply.

### Replying to emails

Use `this.replyToEmail()` to send a reply:

* [  JavaScript ](#tab-panel-2378)
* [  TypeScript ](#tab-panel-2379)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    await this.replyToEmail(email, {

      fromName: "Support Bot", // Display name for the sender

      subject: "Re: Your inquiry", // Optional, defaults to "Re: "

      body: "Thanks for contacting us!", // Email body

      contentType: "text/plain", // Optional, defaults to "text/plain"

      headers: {

        // Optional custom headers

        "X-Custom-Header": "value",

      },

      secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing

    });

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    await this.replyToEmail(email, {

      fromName: "Support Bot", // Display name for the sender

      subject: "Re: Your inquiry", // Optional, defaults to "Re: "

      body: "Thanks for contacting us!", // Email body

      contentType: "text/plain", // Optional, defaults to "text/plain"

      headers: {

        // Optional custom headers

        "X-Custom-Header": "value",

      },

      secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing

    });

  }

}


```

### Forwarding emails

* [  JavaScript ](#tab-panel-2368)
* [  TypeScript ](#tab-panel-2369)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    await email.forward("admin@example.com");

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    await email.forward("admin@example.com");

  }

}


```

### Rejecting emails

* [  JavaScript ](#tab-panel-2374)
* [  TypeScript ](#tab-panel-2375)

JavaScript

```

class MyAgent extends Agent {

  async onEmail(email) {

    if (isSpam(email)) {

      email.setReject("Message rejected as spam");

      return;

    }

    // Process the email...

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    if (isSpam(email)) {

      email.setReject("Message rejected as spam");

      return;

    }

    // Process the email...

  }

}


```

## Secure reply routing

When your agent sends emails and expects replies, use secure reply routing to prevent attackers from forging headers to route emails to arbitrary agent instances.

### How it works

1. **Outbound:** When you call `replyToEmail()` with a `secret`, the agent signs the routing headers (`X-Agent-Name`, `X-Agent-ID`) using HMAC-SHA256.
2. **Inbound:** `createSecureReplyEmailResolver` verifies the signature before routing.
3. **Enforcement:** If an email was routed via the secure resolver, `replyToEmail()` requires a secret (or explicit `null` to opt-out).

### Setup

1. Add a secret to your `wrangler.jsonc`:  
   * [  wrangler.jsonc ](#tab-panel-2358)  
   * [  wrangler.toml ](#tab-panel-2359)  
```  
{  
  "vars": {  
    "EMAIL_SECRET": "change-me-in-production",  
  },  
}  
```  
```  
[vars]  
EMAIL_SECRET = "change-me-in-production"  
```  
For production, use Wrangler secrets instead:  
Terminal window  
```  
npx wrangler secret put EMAIL_SECRET  
```
2. Use the combined resolver pattern:  
   * [  JavaScript ](#tab-panel-2384)  
   * [  TypeScript ](#tab-panel-2385)  
JavaScript  
```  
export default {  
  async email(message, env) {  
    const secureReplyResolver = createSecureReplyEmailResolver(  
      env.EMAIL_SECRET,  
    );  
    const addressResolver = createAddressBasedEmailResolver("EmailAgent");  
    await routeAgentEmail(message, env, {  
      resolver: async (email, env) => {  
        const replyRouting = await secureReplyResolver(email, env);  
        if (replyRouting) return replyRouting;  
        return addressResolver(email, env);  
      },  
    });  
  },  
};  
```  
TypeScript  
```  
export default {  
  async email(message, env) {  
    const secureReplyResolver = createSecureReplyEmailResolver(  
      env.EMAIL_SECRET,  
    );  
    const addressResolver = createAddressBasedEmailResolver("EmailAgent");  
    await routeAgentEmail(message, env, {  
      resolver: async (email, env) => {  
        const replyRouting = await secureReplyResolver(email, env);  
        if (replyRouting) return replyRouting;  
        return addressResolver(email, env);  
      },  
    });  
  },  
} satisfies ExportedHandler<Env>;  
```
3. Sign outbound emails:  
   * [  JavaScript ](#tab-panel-2380)  
   * [  TypeScript ](#tab-panel-2381)  
JavaScript  
```  
class MyAgent extends Agent {  
  async onEmail(email) {  
    await this.replyToEmail(email, {  
      fromName: "My Agent",  
      body: "Thanks for your email!",  
      secret: this.env.EMAIL_SECRET, // Signs the routing headers  
    });  
  }  
}  
```  
TypeScript  
```  
class MyAgent extends Agent {  
  async onEmail(email: AgentEmail) {  
    await this.replyToEmail(email, {  
      fromName: "My Agent",  
      body: "Thanks for your email!",  
      secret: this.env.EMAIL_SECRET, // Signs the routing headers  
    });  
  }  
}  
```

### Enforcement behavior

When an email is routed via `createSecureReplyEmailResolver`, the `replyToEmail()` method enforces signing:

| secret value        | Behavior                                                     |
| ------------------- | ------------------------------------------------------------ |
| "my-secret"         | Signs headers (secure)                                       |
| undefined (omitted) | **Throws error** \- must provide secret or explicit opt-out  |
| null                | Allowed but not recommended - explicitly opts out of signing |

## Complete example

Here is a complete email agent with secure reply routing:

* [  JavaScript ](#tab-panel-2386)
* [  TypeScript ](#tab-panel-2387)

JavaScript

```

import { Agent, routeAgentEmail } from "agents";

import {

  createAddressBasedEmailResolver,

  createSecureReplyEmailResolver,

} from "agents/email";

import PostalMime from "postal-mime";


export class EmailAgent extends Agent {

  async onEmail(email) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log(`Email from ${email.from}: ${parsed.subject}`);


    // Store the email in state

    const emails = this.state.emails || [];

    emails.push({

      from: email.from,

      subject: parsed.subject,

      receivedAt: new Date().toISOString(),

    });

    this.setState({ ...this.state, emails });


    // Send auto-reply with signed headers

    await this.replyToEmail(email, {

      fromName: "Support Bot",

      body: `Thanks for your email! We received: "${parsed.subject}"`,

      secret: this.env.EMAIL_SECRET,

    });

  }

}


export default {

  async email(message, env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

      {

        maxAge: 7 * 24 * 60 * 60, // 7 days

        onInvalidSignature: (email, reason) => {

          console.warn(`Invalid signature from ${email.from}: ${reason}`);

        },

      },

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        // Try secure reply routing first

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;

        // Fall back to address-based routing

        return addressResolver(email, env);

      },

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

};


```

TypeScript

```

import { Agent, routeAgentEmail } from "agents";

import {

  createAddressBasedEmailResolver,

  createSecureReplyEmailResolver,

  type AgentEmail,

} from "agents/email";

import PostalMime from "postal-mime";


interface Env {

  EmailAgent: DurableObjectNamespace;

  EMAIL_SECRET: string;

}


export class EmailAgent extends Agent {

  async onEmail(email: AgentEmail) {

    const raw = await email.getRaw();

    const parsed = await PostalMime.parse(raw);


    console.log(`Email from ${email.from}: ${parsed.subject}`);


    // Store the email in state

    const emails = this.state.emails || [];

    emails.push({

      from: email.from,

      subject: parsed.subject,

      receivedAt: new Date().toISOString(),

    });

    this.setState({ ...this.state, emails });


    // Send auto-reply with signed headers

    await this.replyToEmail(email, {

      fromName: "Support Bot",

      body: `Thanks for your email! We received: "${parsed.subject}"`,

      secret: this.env.EMAIL_SECRET,

    });

  }

}


export default {

  async email(message, env: Env) {

    const secureReplyResolver = createSecureReplyEmailResolver(

      env.EMAIL_SECRET,

      {

        maxAge: 7 * 24 * 60 * 60, // 7 days

        onInvalidSignature: (email, reason) => {

          console.warn(`Invalid signature from ${email.from}: ${reason}`);

        },

      },

    );

    const addressResolver = createAddressBasedEmailResolver("EmailAgent");


    await routeAgentEmail(message, env, {

      resolver: async (email, env) => {

        // Try secure reply routing first

        const replyRouting = await secureReplyResolver(email, env);

        if (replyRouting) return replyRouting;

        // Fall back to address-based routing

        return addressResolver(email, env);

      },

      onNoRoute: (email) => {

        console.warn(`No route found for email from ${email.from}`);

        email.setReject("Unknown recipient");

      },

    });

  },

} satisfies ExportedHandler<Env>;


```

## API reference

### `routeAgentEmail`

TypeScript

```

function routeAgentEmail<Env>(

  email: ForwardableEmailMessage,

  env: Env,

  options: {

    resolver: EmailResolver;

    onNoRoute?: (email: ForwardableEmailMessage) => void | Promise<void>;

  },

): Promise<void>;


```

Routes an incoming email to the appropriate Agent based on the resolver's decision.

| Option    | Description                                                                                                                                                                             |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| resolver  | Function that determines which agent to route the email to                                                                                                                              |
| onNoRoute | Optional callback invoked when no routing information is found. Use this to reject the email or perform custom handling. If not provided, a warning is logged and the email is dropped. |

### `createSecureReplyEmailResolver`

TypeScript

```

function createSecureReplyEmailResolver(

  secret: string,

  options?: {

    maxAge?: number;

    onInvalidSignature?: (

      email: ForwardableEmailMessage,

      reason: SignatureFailureReason,

    ) => void;

  },

): EmailResolver;


type SignatureFailureReason =

  | "missing_headers"

  | "expired"

  | "invalid"

  | "malformed_timestamp";


```

Creates a resolver for routing email replies with signature verification.

| Option             | Description                                                              |
| ------------------ | ------------------------------------------------------------------------ |
| secret             | Secret key for HMAC verification (must match the key used to sign)       |
| maxAge             | Maximum age of signature in seconds (default: 30 days / 2592000 seconds) |
| onInvalidSignature | Optional callback for logging when signature verification fails          |

### `signAgentHeaders`

TypeScript

```

function signAgentHeaders(

  secret: string,

  agentName: string,

  agentId: string,

): Promise<Record<string, string>>;


```

Manually sign agent routing headers. Returns an object with `X-Agent-Name`, `X-Agent-ID`, `X-Agent-Sig`, and `X-Agent-Sig-Ts` headers.

Useful when sending emails through external services while maintaining secure reply routing. The signature includes a timestamp and will be valid for 30 days by default.

## Next steps

[ HTTP and SSE ](https://developers.cloudflare.com/agents/api-reference/http-sse/) Handle HTTP requests in your Agent. 

[ Webhooks ](https://developers.cloudflare.com/agents/guides/webhooks/) Receive events from external services. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/email/","name":"Email routing"}}]}
```

---

---
title: getCurrentAgent()
description: The getCurrentAgent() function allows you to access the current agent context from anywhere in your code, including external utility functions and libraries. This is useful when you need agent information in functions that do not have direct access to this.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/get-current-agent.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# getCurrentAgent()

The `getCurrentAgent()` function allows you to access the current agent context from anywhere in your code, including external utility functions and libraries. This is useful when you need agent information in functions that do not have direct access to `this`.

## Automatic context for custom methods

All custom methods automatically have full agent context. The framework automatically detects and wraps your custom methods during initialization, ensuring `getCurrentAgent()` works everywhere.

## How it works

* [  JavaScript ](#tab-panel-2390)
* [  TypeScript ](#tab-panel-2391)

JavaScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";


export class MyAgent extends AIChatAgent {

  async customMethod() {

    const { agent } = getCurrentAgent();

    // agent is automatically available

    console.log(agent.name);

  }


  async anotherMethod() {

    // This works too - no setup needed

    const { agent } = getCurrentAgent();

    return agent.state;

  }

}


```

TypeScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";


export class MyAgent extends AIChatAgent {

  async customMethod() {

    const { agent } = getCurrentAgent();

    // agent is automatically available

    console.log(agent.name);

  }


  async anotherMethod() {

    // This works too - no setup needed

    const { agent } = getCurrentAgent();

    return agent.state;

  }

}


```

No configuration is required. The framework automatically:

1. Scans your agent class for custom methods.
2. Wraps them with agent context during initialization.
3. Ensures `getCurrentAgent()` works in all external functions called from your methods.

## Real-world example

* [  JavaScript ](#tab-panel-2406)
* [  TypeScript ](#tab-panel-2407)

JavaScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";

import { generateText } from "ai";

import { openai } from "@ai-sdk/openai";


// External utility function that needs agent context

async function processWithAI(prompt) {

  const { agent } = getCurrentAgent();

  // External functions can access the current agent


  return await generateText({

    model: openai("gpt-4"),

    prompt: `Agent ${agent?.name}: ${prompt}`,

  });

}


export class MyAgent extends AIChatAgent {

  async customMethod(message) {

    // Use this.* to access agent properties directly

    console.log("Agent name:", this.name);

    console.log("Agent state:", this.state);


    // External functions automatically work

    const result = await processWithAI(message);

    return result.text;

  }

}


```

TypeScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";

import { generateText } from "ai";

import { openai } from "@ai-sdk/openai";


// External utility function that needs agent context

async function processWithAI(prompt: string) {

  const { agent } = getCurrentAgent();

  // External functions can access the current agent


  return await generateText({

    model: openai("gpt-4"),

    prompt: `Agent ${agent?.name}: ${prompt}`,

  });

}


export class MyAgent extends AIChatAgent {

  async customMethod(message: string) {

    // Use this.* to access agent properties directly

    console.log("Agent name:", this.name);

    console.log("Agent state:", this.state);


    // External functions automatically work

    const result = await processWithAI(message);

    return result.text;

  }

}


```

### Built-in vs custom methods

* **Built-in methods** (`onRequest`, `onEmail`, `onStateChanged`): Already have context.
* **Custom methods** (your methods): Automatically wrapped during initialization.
* **External functions**: Access context through `getCurrentAgent()`.

### The context flow

* [  JavaScript ](#tab-panel-2388)
* [  TypeScript ](#tab-panel-2389)

JavaScript

```

// When you call a custom method:

agent.customMethod();

// → automatically wrapped with agentContext.run()

// → your method executes with full context

// → external functions can use getCurrentAgent()


```

TypeScript

```

// When you call a custom method:

agent.customMethod();

// → automatically wrapped with agentContext.run()

// → your method executes with full context

// → external functions can use getCurrentAgent()


```

## Common use cases

### Working with AI SDK tools

* [  JavaScript ](#tab-panel-2400)
* [  TypeScript ](#tab-panel-2401)

JavaScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { generateText } from "ai";

import { openai } from "@ai-sdk/openai";


export class MyAgent extends AIChatAgent {

  async generateResponse(prompt) {

    // AI SDK tools automatically work

    const response = await generateText({

      model: openai("gpt-4"),

      prompt,

      tools: {

        // Tools that use getCurrentAgent() work perfectly

      },

    });


    return response.text;

  }

}


```

TypeScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { generateText } from "ai";

import { openai } from "@ai-sdk/openai";


export class MyAgent extends AIChatAgent {

  async generateResponse(prompt: string) {

    // AI SDK tools automatically work

    const response = await generateText({

      model: openai("gpt-4"),

      prompt,

      tools: {

        // Tools that use getCurrentAgent() work perfectly

      },

    });


    return response.text;

  }

}


```

### Calling external libraries

* [  JavaScript ](#tab-panel-2398)
* [  TypeScript ](#tab-panel-2399)

JavaScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";


async function saveToDatabase(data) {

  const { agent } = getCurrentAgent();

  // Can access agent info for logging, context, etc.

  console.log(`Saving data for agent: ${agent?.name}`);

}


export class MyAgent extends AIChatAgent {

  async processData(data) {

    // External functions automatically have context

    await saveToDatabase(data);

  }

}


```

TypeScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";


async function saveToDatabase(data: any) {

  const { agent } = getCurrentAgent();

  // Can access agent info for logging, context, etc.

  console.log(`Saving data for agent: ${agent?.name}`);

}


export class MyAgent extends AIChatAgent {

  async processData(data: any) {

    // External functions automatically have context

    await saveToDatabase(data);

  }

}


```

### Accessing request and connection context

* [  JavaScript ](#tab-panel-2402)
* [  TypeScript ](#tab-panel-2403)

JavaScript

```

import { getCurrentAgent } from "agents";


function logRequestInfo() {

  const { agent, connection, request } = getCurrentAgent();


  if (request) {

    console.log("Request URL:", request.url);

    console.log("Request method:", request.method);

  }


  if (connection) {

    console.log("Connection ID:", connection.id);

  }

}


```

TypeScript

```

import { getCurrentAgent } from "agents";


function logRequestInfo() {

  const { agent, connection, request } = getCurrentAgent();


  if (request) {

    console.log("Request URL:", request.url);

    console.log("Request method:", request.method);

  }


  if (connection) {

    console.log("Connection ID:", connection.id);

  }

}


```

## API reference

### `getCurrentAgent()`

Gets the current agent from any context where it is available.

* [  JavaScript ](#tab-panel-2392)
* [  TypeScript ](#tab-panel-2393)

JavaScript

```

import { getCurrentAgent } from "agents";


```

TypeScript

```

import { getCurrentAgent } from "agents";


function getCurrentAgent<T extends Agent>(): {

  agent: T | undefined;

  connection: Connection | undefined;

  request: Request | undefined;

  email: AgentEmail | undefined;

};


```

#### Returns:

| Property   | Type                    | Description                                                   |
| ---------- | ----------------------- | ------------------------------------------------------------- |
| agent      | T \| undefined          | The current agent instance                                    |
| connection | Connection \| undefined | The WebSocket connection (if called from a WebSocket handler) |
| request    | Request \| undefined    | The HTTP request (if called from a request handler)           |
| email      | AgentEmail \| undefined | The email (if called from an email handler)                   |

#### Usage:

* [  JavaScript ](#tab-panel-2404)
* [  TypeScript ](#tab-panel-2405)

JavaScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";


export class MyAgent extends AIChatAgent {

  async customMethod() {

    const { agent, connection, request } = getCurrentAgent();

    // agent is properly typed as MyAgent

    // connection and request available if called from a request handler

  }

}


```

TypeScript

```

import { AIChatAgent } from "agents/ai-chat-agent";

import { getCurrentAgent } from "agents";


export class MyAgent extends AIChatAgent {

  async customMethod() {

    const { agent, connection, request } = getCurrentAgent<MyAgent>();

    // agent is properly typed as MyAgent

    // connection and request available if called from a request handler

  }

}


```

### Context availability

The context available depends on how the method was invoked:

| Invocation              | agent | connection | request | email   |
| ----------------------- | ----- | ---------- | ------- | ------- |
| onRequest()             | Yes   | No         | Yes     | No      |
| onConnect()             | Yes   | Yes        | Yes     | No      |
| onMessage()             | Yes   | Yes        | No      | No      |
| onEmail()               | Yes   | No         | No      | Yes     |
| Custom method (via RPC) | Yes   | Yes        | No      | No      |
| Scheduled task          | Yes   | No         | No      | No      |
| Queue callback          | Yes   | Depends    | Depends | Depends |

## Best practices

1. **Use `this` when possible**: Inside agent methods, prefer `this.name`, `this.state`, etc. over `getCurrentAgent()`.
2. **Use `getCurrentAgent()` in external functions**: When you need agent context in utility functions or libraries that do not have access to `this`.
3. **Check for undefined**: The returned values may be `undefined` if called outside an agent context.  
   * [  JavaScript ](#tab-panel-2396)  
   * [  TypeScript ](#tab-panel-2397)  
JavaScript  
```  
const { agent } = getCurrentAgent();  
if (agent) {  
  // Safe to use agent  
  console.log(agent.name);  
}  
```  
TypeScript  
```  
const { agent } = getCurrentAgent();  
if (agent) {  
  // Safe to use agent  
  console.log(agent.name);  
}  
```
4. **Type the agent**: Pass your agent class as a type parameter for proper typing.  
   * [  JavaScript ](#tab-panel-2394)  
   * [  TypeScript ](#tab-panel-2395)  
JavaScript  
```  
const { agent } = getCurrentAgent();  
// agent is typed as MyAgent | undefined  
```  
TypeScript  
```  
const { agent } = getCurrentAgent<MyAgent>();  
// agent is typed as MyAgent | undefined  
```

## Next steps

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ Callable methods ](https://developers.cloudflare.com/agents/api-reference/callable-methods/) Expose methods to clients via RPC. 

[ State management ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Manage and sync agent state. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/get-current-agent/","name":"getCurrentAgent()"}}]}
```

---

---
title: HTTP and Server-Sent Events
description: Agents can handle HTTP requests and stream responses using Server-Sent Events (SSE). This page covers the onRequest method and SSE patterns.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/http-sse.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# HTTP and Server-Sent Events

Agents can handle HTTP requests and stream responses using Server-Sent Events (SSE). This page covers the `onRequest` method and SSE patterns.

## Handling HTTP requests

Define the `onRequest` method to handle HTTP requests to your agent:

* [  JavaScript ](#tab-panel-2410)
* [  TypeScript ](#tab-panel-2411)

JavaScript

```

import { Agent } from "agents";


export class APIAgent extends Agent {

  async onRequest(request) {

    const url = new URL(request.url);


    // Route based on path

    if (url.pathname.endsWith("/status")) {

      return Response.json({ status: "ok", state: this.state });

    }


    if (url.pathname.endsWith("/action")) {

      if (request.method !== "POST") {

        return new Response("Method not allowed", { status: 405 });

      }

      const data = await request.json();

      await this.processAction(data.action);

      return Response.json({ success: true });

    }


    return new Response("Not found", { status: 404 });

  }


  async processAction(action) {

    // Handle the action

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class APIAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const url = new URL(request.url);


    // Route based on path

    if (url.pathname.endsWith("/status")) {

      return Response.json({ status: "ok", state: this.state });

    }


    if (url.pathname.endsWith("/action")) {

      if (request.method !== "POST") {

        return new Response("Method not allowed", { status: 405 });

      }

      const data = await request.json<{ action: string }>();

      await this.processAction(data.action);

      return Response.json({ success: true });

    }


    return new Response("Not found", { status: 404 });

  }


  async processAction(action: string) {

    // Handle the action

  }

}


```

## Server-Sent Events (SSE)

SSE allows you to stream data to clients over a long-running HTTP connection. This is ideal for AI model responses that generate tokens incrementally.

### Manual SSE

Create an SSE stream manually using `ReadableStream`:

* [  JavaScript ](#tab-panel-2414)
* [  TypeScript ](#tab-panel-2415)

JavaScript

```

export class StreamAgent extends Agent {

  async onRequest(request) {

    const encoder = new TextEncoder();


    const stream = new ReadableStream({

      async start(controller) {

        // Send events

        controller.enqueue(encoder.encode("data: Starting...\n\n"));


        for (let i = 1; i <= 5; i++) {

          await new Promise((r) => setTimeout(r, 500));

          controller.enqueue(encoder.encode(`data: Step ${i} complete\n\n`));

        }


        controller.enqueue(encoder.encode("data: Done!\n\n"));

        controller.close();

      },

    });


    return new Response(stream, {

      headers: {

        "Content-Type": "text/event-stream",

        "Cache-Control": "no-cache",

        Connection: "keep-alive",

      },

    });

  }

}


```

TypeScript

```

export class StreamAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const encoder = new TextEncoder();


    const stream = new ReadableStream({

      async start(controller) {

        // Send events

        controller.enqueue(encoder.encode("data: Starting...\n\n"));


        for (let i = 1; i <= 5; i++) {

          await new Promise((r) => setTimeout(r, 500));

          controller.enqueue(encoder.encode(`data: Step ${i} complete\n\n`));

        }


        controller.enqueue(encoder.encode("data: Done!\n\n"));

        controller.close();

      },

    });


    return new Response(stream, {

      headers: {

        "Content-Type": "text/event-stream",

        "Cache-Control": "no-cache",

        Connection: "keep-alive",

      },

    });

  }

}


```

### SSE message format

SSE messages follow a specific format:

```

data: your message here\n\n


```

You can also include event types and IDs:

```

event: update\n

id: 123\n

data: {"count": 42}\n\n


```

### With AI SDK

The [AI SDK ↗](https://sdk.vercel.ai/) provides built-in SSE streaming:

* [  JavaScript ](#tab-panel-2408)
* [  TypeScript ](#tab-panel-2409)

JavaScript

```

import { Agent } from "agents";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class ChatAgent extends Agent {

  async onRequest(request) {

    const { prompt } = await request.json();


    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: prompt,

    });


    return result.toTextStreamResponse();

  }

}


```

TypeScript

```

import { Agent } from "agents";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


interface Env {

  AI: Ai;

}


export class ChatAgent extends Agent<Env> {

  async onRequest(request: Request): Promise<Response> {

    const { prompt } = await request.json<{ prompt: string }>();


    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: prompt,

    });


    return result.toTextStreamResponse();

  }

}


```

## Connection handling

SSE connections can be long-lived. Handle client disconnects gracefully:

* **Persist progress** — Write to [agent state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) so clients can resume
* **Use agent routing** — Clients can [reconnect to the same agent instance](https://developers.cloudflare.com/agents/api-reference/routing/) without session stores
* **No timeout limits** — Cloudflare Workers have no effective limit on SSE response duration

* [  JavaScript ](#tab-panel-2412)
* [  TypeScript ](#tab-panel-2413)

JavaScript

```

export class ResumeAgent extends Agent {

  async onRequest(request) {

    const url = new URL(request.url);

    const lastEventId = request.headers.get("Last-Event-ID");


    if (lastEventId) {

      // Client is resuming - send events after lastEventId

      return this.resumeStream(lastEventId);

    }


    return this.startStream();

  }


  async startStream() {

    // Start new stream, saving progress to this.state

  }


  async resumeStream(fromId) {

    // Resume from saved state

  }

}


```

TypeScript

```

export class ResumeAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const url = new URL(request.url);

    const lastEventId = request.headers.get("Last-Event-ID");


    if (lastEventId) {

      // Client is resuming - send events after lastEventId

      return this.resumeStream(lastEventId);

    }


    return this.startStream();

  }


  async startStream(): Promise<Response> {

    // Start new stream, saving progress to this.state

  }


  async resumeStream(fromId: string): Promise<Response> {

    // Resume from saved state

  }

}


```

## WebSockets vs SSE

| Feature      | WebSockets             | SSE                                |
| ------------ | ---------------------- | ---------------------------------- |
| Direction    | Bi-directional         | Server → Client only               |
| Protocol     | ws:// / wss://         | HTTP                               |
| Binary data  | Yes                    | No (text only)                     |
| Reconnection | Manual                 | Automatic (browser)                |
| Best for     | Interactive apps, chat | Streaming responses, notifications |

**Recommendation:** Use WebSockets for interactive applications. Use SSE for streaming AI responses or server-push notifications.

Refer to [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/) for WebSocket documentation.

## Next steps

[ WebSockets ](https://developers.cloudflare.com/agents/api-reference/websockets/) Bi-directional real-time communication. 

[ State management ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Persist stream progress and agent state. 

[ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) Streaming responses with AI chat. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/http-sse/","name":"HTTP and Server-Sent Events"}}]}
```

---

---
title: McpAgent
description: When you build MCP Servers on Cloudflare, you extend the McpAgent class, from the Agents SDK:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/mcp-agent-api.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# McpAgent

When you build MCP Servers on Cloudflare, you extend the [McpAgent class ↗](https://github.com/cloudflare/agents/blob/main/packages/agents/src/mcp.ts), from the Agents SDK:

* [  JavaScript ](#tab-panel-2422)
* [  TypeScript ](#tab-panel-2423)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "Demo", version: "1.0.0" });


  async init() {

    this.server.tool(

      "add",

      { a: z.number(), b: z.number() },

      async ({ a, b }) => ({

        content: [{ type: "text", text: String(a + b) }],

      }),

    );

  }

}


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "Demo", version: "1.0.0" });


  async init() {

    this.server.tool(

      "add",

      { a: z.number(), b: z.number() },

      async ({ a, b }) => ({

        content: [{ type: "text", text: String(a + b) }],

      }),

    );

  }

}


```

This means that each instance of your MCP server has its own durable state, backed by a [Durable Object](https://developers.cloudflare.com/durable-objects/), with its own [SQL database](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state).

Your MCP server doesn't necessarily have to be an Agent. You can build MCP servers that are stateless, and just add [tools](https://developers.cloudflare.com/agents/model-context-protocol/tools) to your MCP server using the `@modelcontextprotocol/sdk` package.

But if you want your MCP server to:

* remember previous tool calls, and responses it provided
* provide a game to the MCP client, remembering the state of the game board, previous moves, and the score
* cache the state of a previous external API call, so that subsequent tool calls can reuse it
* do anything that an Agent can do, but allow MCP clients to communicate with it

You can use the APIs below in order to do so.

## API overview

| Property/Method               | Description                                        |
| ----------------------------- | -------------------------------------------------- |
| state                         | Current state object (persisted)                   |
| initialState                  | Default state when instance starts                 |
| setState(state)               | Update and persist state                           |
| onStateChanged(state)         | Called when state changes                          |
| sql                           | Execute SQL queries on embedded database           |
| server                        | The McpServer instance for registering tools       |
| props                         | User identity and tokens from OAuth authentication |
| elicitInput(options, context) | Request structured input from user                 |
| McpAgent.serve(path, options) | Static method to create a Worker handler           |

## Deploying with McpAgent.serve()

The `McpAgent.serve()` static method creates a Worker handler that routes requests to your MCP server:

* [  JavaScript ](#tab-panel-2424)
* [  TypeScript ](#tab-panel-2425)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "my-server", version: "1.0.0" });


  async init() {

    this.server.tool("square", { n: z.number() }, async ({ n }) => ({

      content: [{ type: "text", text: String(n * n) }],

    }));

  }

}


// Export the Worker handler

export default MyMCP.serve("/mcp");


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({ name: "my-server", version: "1.0.0" });


  async init() {

    this.server.tool("square", { n: z.number() }, async ({ n }) => ({

      content: [{ type: "text", text: String(n * n) }],

    }));

  }

}


// Export the Worker handler

export default MyMCP.serve("/mcp");


```

This is the simplest way to deploy an MCP server — about 15 lines of code. The `serve()` method handles Streamable HTTP transport automatically.

### With OAuth authentication

When using the [OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider), pass your MCP server to `apiHandlers`:

* [  JavaScript ](#tab-panel-2418)
* [  TypeScript ](#tab-panel-2419)

JavaScript

```

import { OAuthProvider } from "@cloudflare/workers-oauth-provider";


export default new OAuthProvider({

  apiHandlers: { "/mcp": MyMCP.serve("/mcp") },

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

  defaultHandler: AuthHandler,

});


```

TypeScript

```

import { OAuthProvider } from "@cloudflare/workers-oauth-provider";


export default new OAuthProvider({

  apiHandlers: { "/mcp": MyMCP.serve("/mcp") },

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

  defaultHandler: AuthHandler,

});


```

## Data jurisdiction

For GDPR and data residency compliance, specify a jurisdiction to ensure your MCP server instances run in specific regions:

* [  JavaScript ](#tab-panel-2416)
* [  TypeScript ](#tab-panel-2417)

JavaScript

```

// EU jurisdiction for GDPR compliance

export default MyMCP.serve("/mcp", { jurisdiction: "eu" });


```

TypeScript

```

// EU jurisdiction for GDPR compliance

export default MyMCP.serve("/mcp", { jurisdiction: "eu" });


```

With OAuth:

* [  JavaScript ](#tab-panel-2420)
* [  TypeScript ](#tab-panel-2421)

JavaScript

```

export default new OAuthProvider({

  apiHandlers: {

    "/mcp": MyMCP.serve("/mcp", { jurisdiction: "eu" }),

  },

  // ... other OAuth config

});


```

TypeScript

```

export default new OAuthProvider({

  apiHandlers: {

    "/mcp": MyMCP.serve("/mcp", { jurisdiction: "eu" }),

  },

  // ... other OAuth config

});


```

When you specify `jurisdiction: "eu"`:

* All MCP session data stays within the EU
* User data processed by your tools remains in the EU
* State stored in the Durable Object stays in the EU

Available jurisdictions include `"eu"` (European Union) and `"fedramp"` (FedRAMP compliant locations). Refer to [Durable Objects data location](https://developers.cloudflare.com/durable-objects/reference/data-location/) for more options.

## Hibernation support

`McpAgent` instances automatically support [WebSockets Hibernation](https://developers.cloudflare.com/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api), allowing stateful MCP servers to sleep during inactive periods while preserving their state. This means your agents only consume compute resources when actively processing requests, optimizing costs while maintaining the full context and conversation history.

Hibernation is enabled by default and requires no additional configuration.

## Authentication and authorization

The McpAgent class provides seamless integration with the [OAuth Provider Library ↗](https://github.com/cloudflare/workers-oauth-provider) for [authentication and authorization](https://developers.cloudflare.com/agents/model-context-protocol/authorization/).

When a user authenticates to your MCP server, their identity information and tokens are made available through the `props` parameter, allowing you to:

* access user-specific data
* check user permissions before performing operations
* customize responses based on user attributes
* use authentication tokens to make requests to external services on behalf of the user

## State synchronization APIs

The `McpAgent` class provides full access to the [Agent state APIs](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/):

* [state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) — Current persisted state
* [initialState](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/#set-the-initial-state-for-an-agent) — Default state when instance starts
* [setState](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) — Update and persist state
* [onStateChanged](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/#synchronizing-state) — React to state changes
* [sql](https://developers.cloudflare.com/agents/api-reference/agents-api/#sql-api) — Execute SQL queries on embedded database

State resets after the session ends

Currently, each client session is backed by an instance of the `McpAgent` class. This is handled automatically for you, as shown in the [getting started guide](https://developers.cloudflare.com/agents/guides/remote-mcp-server). This means that when the same client reconnects, they will start a new session, and the state will be reset.

For example, the following code implements an MCP server that remembers a counter value, and updates the counter when the `add` tool is called:

* [  JavaScript ](#tab-panel-2428)
* [  TypeScript ](#tab-panel-2429)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class MyMCP extends McpAgent {

  server = new McpServer({

    name: "Demo",

    version: "1.0.0",

  });


  initialState = {

    counter: 1,

  };


  async init() {

    this.server.resource(`counter`, `mcp://resource/counter`, (uri) => {

      return {

        contents: [{ uri: uri.href, text: String(this.state.counter) }],

      };

    });


    this.server.tool(

      "add",

      "Add to the counter, stored in the MCP",

      { a: z.number() },

      async ({ a }) => {

        this.setState({ ...this.state, counter: this.state.counter + a });


        return {

          content: [

            {

              type: "text",

              text: String(`Added ${a}, total is now ${this.state.counter}`),

            },

          ],

        };

      },

    );

  }


  onStateChanged(state) {

    console.log({ stateUpdate: state });

  }

}


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


type State = { counter: number };


export class MyMCP extends McpAgent<Env, State, {}> {

  server = new McpServer({

    name: "Demo",

    version: "1.0.0",

  });


  initialState: State = {

    counter: 1,

  };


  async init() {

    this.server.resource(`counter`, `mcp://resource/counter`, (uri) => {

      return {

        contents: [{ uri: uri.href, text: String(this.state.counter) }],

      };

    });


    this.server.tool(

      "add",

      "Add to the counter, stored in the MCP",

      { a: z.number() },

      async ({ a }) => {

        this.setState({ ...this.state, counter: this.state.counter + a });


        return {

          content: [

            {

              type: "text",

              text: String(`Added ${a}, total is now ${this.state.counter}`),

            },

          ],

        };

      },

    );

  }


  onStateChanged(state: State) {

    console.log({ stateUpdate: state });

  }

}


```

## Elicitation (human-in-the-loop)

MCP servers can request additional user input during tool execution using **elicitation**. The MCP client (like Claude Desktop) renders a form based on your JSON Schema and returns the user's response.

### When to use elicitation

* Request structured input that was not part of the original tool call
* Confirm high-stakes operations before proceeding
* Gather additional context or preferences mid-execution

### `elicitInput(options, context)`

Request structured input from the user during tool execution.

**Parameters:**

| Parameter                | Type        | Description                                  |
| ------------------------ | ----------- | -------------------------------------------- |
| options.message          | string      | Message explaining what input is needed      |
| options.requestedSchema  | JSON Schema | Schema defining the expected input structure |
| context.relatedRequestId | string      | The extra.requestId from the tool handler    |

**Returns:** `Promise<{ action: "accept" | "decline", content?: object }>`

* [  JavaScript ](#tab-panel-2430)
* [  TypeScript ](#tab-panel-2431)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class CounterMCP extends McpAgent {

  server = new McpServer({

    name: "counter-server",

    version: "1.0.0",

  });


  initialState = { counter: 0 };


  async init() {

    this.server.tool(

      "increase-counter",

      "Increase the counter by a user-specified amount",

      { confirm: z.boolean().describe("Do you want to increase the counter?") },

      async ({ confirm }, extra) => {

        if (!confirm) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Request additional input from the user

        const userInput = await this.server.server.elicitInput(

          {

            message: "By how much do you want to increase the counter?",

            requestedSchema: {

              type: "object",

              properties: {

                amount: {

                  type: "number",

                  title: "Amount",

                  description: "The amount to increase the counter by",

                },

              },

              required: ["amount"],

            },

          },

          { relatedRequestId: extra.requestId },

        );


        // Check if user accepted or cancelled

        if (userInput.action !== "accept" || !userInput.content) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Use the input

        const amount = Number(userInput.content.amount);

        this.setState({

          ...this.state,

          counter: this.state.counter + amount,

        });


        return {

          content: [

            {

              type: "text",

              text: `Counter increased by ${amount}, now at ${this.state.counter}`,

            },

          ],

        };

      },

    );

  }

}


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


type State = { counter: number };


export class CounterMCP extends McpAgent<Env, State, {}> {

  server = new McpServer({

    name: "counter-server",

    version: "1.0.0",

  });


  initialState: State = { counter: 0 };


  async init() {

    this.server.tool(

      "increase-counter",

      "Increase the counter by a user-specified amount",

      { confirm: z.boolean().describe("Do you want to increase the counter?") },

      async ({ confirm }, extra) => {

        if (!confirm) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Request additional input from the user

        const userInput = await this.server.server.elicitInput(

          {

            message: "By how much do you want to increase the counter?",

            requestedSchema: {

              type: "object",

              properties: {

                amount: {

                  type: "number",

                  title: "Amount",

                  description: "The amount to increase the counter by",

                },

              },

              required: ["amount"],

            },

          },

          { relatedRequestId: extra.requestId },

        );


        // Check if user accepted or cancelled

        if (userInput.action !== "accept" || !userInput.content) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Use the input

        const amount = Number(userInput.content.amount);

        this.setState({

          ...this.state,

          counter: this.state.counter + amount,

        });


        return {

          content: [

            {

              type: "text",

              text: `Counter increased by ${amount}, now at ${this.state.counter}`,

            },

          ],

        };

      },

    );

  }

}


```

### JSON Schema for forms

The `requestedSchema` defines the form structure shown to the user:

TypeScript

```

const schema = {

  type: "object",

  properties: {

    // Text input

    name: {

      type: "string",

      title: "Name",

      description: "Enter your name",

    },

    // Number input

    amount: {

      type: "number",

      title: "Amount",

      minimum: 1,

      maximum: 100,

    },

    // Boolean (checkbox)

    confirm: {

      type: "boolean",

      title: "I confirm this action",

    },

    // Enum (dropdown)

    priority: {

      type: "string",

      enum: ["low", "medium", "high"],

      title: "Priority",

    },

  },

  required: ["name", "amount"],

};


```

### Handling responses

* [  JavaScript ](#tab-panel-2426)
* [  TypeScript ](#tab-panel-2427)

JavaScript

```

const result = await this.server.server.elicitInput(

  { message: "Confirm action", requestedSchema: schema },

  { relatedRequestId: extra.requestId },

);


switch (result.action) {

  case "accept":

    // User submitted the form

    const { name, amount } = result.content;

    // Process the input...

    break;


  case "decline":

    // User cancelled

    return { content: [{ type: "text", text: "Operation cancelled." }] };

}


```

TypeScript

```

const result = await this.server.server.elicitInput(

  { message: "Confirm action", requestedSchema: schema },

  { relatedRequestId: extra.requestId },

);


switch (result.action) {

  case "accept":

    // User submitted the form

    const { name, amount } = result.content as { name: string; amount: number };

    // Process the input...

    break;


  case "decline":

    // User cancelled

    return { content: [{ type: "text", text: "Operation cancelled." }] };

}


```

MCP client support

Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility.

For more human-in-the-loop patterns including workflow-based approval, refer to [Human-in-the-loop patterns](https://developers.cloudflare.com/agents/guides/human-in-the-loop/).

## Next steps

[ Build a Remote MCP server ](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) Get started with MCP servers on Cloudflare. 

[ MCP Tools ](https://developers.cloudflare.com/agents/model-context-protocol/tools/) Design and add tools to your MCP server. 

[ Authorization ](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) Set up OAuth authentication. 

[ Securing MCP servers ](https://developers.cloudflare.com/agents/guides/securing-mcp-server/) Security best practices for production. 

[ createMcpHandler ](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/) Build stateless MCP servers. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/mcp-agent-api/","name":"McpAgent"}}]}
```

---

---
title: McpClient
description: Connect your agent to external Model Context Protocol (MCP) servers to use their tools, resources, and prompts. This enables your agent to interact with GitHub, Slack, databases, and other services through a standardized protocol.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/mcp-client-api.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# McpClient

Connect your agent to external [Model Context Protocol (MCP)](https://developers.cloudflare.com/agents/model-context-protocol/) servers to use their tools, resources, and prompts. This enables your agent to interact with GitHub, Slack, databases, and other services through a standardized protocol.

## Overview

The MCP client capability lets your agent:

* **Connect to external MCP servers** \- GitHub, Slack, databases, AI services
* **Use their tools** \- Call functions exposed by MCP servers
* **Access resources** \- Read data from MCP servers
* **Use prompts** \- Leverage pre-built prompt templates

Note

This page covers connecting to MCP servers as a client. To create your own MCP server, refer to [Creating MCP servers](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/).

## Quick start

* [  JavaScript ](#tab-panel-2438)
* [  TypeScript ](#tab-panel-2439)

JavaScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async onRequest(request) {

    // Add an MCP server

    const result = await this.addMcpServer(

      "github",

      "https://mcp.github.com/mcp",

    );


    if (result.state === "authenticating") {

      // Server requires OAuth - redirect user to authorize

      return Response.redirect(result.authUrl);

    }


    // Server is ready - tools are now available

    const state = this.getMcpServers();

    console.log(`Connected! ${state.tools.length} tools available`);


    return new Response("MCP server connected");

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async onRequest(request: Request) {

    // Add an MCP server

    const result = await this.addMcpServer(

      "github",

      "https://mcp.github.com/mcp",

    );


    if (result.state === "authenticating") {

      // Server requires OAuth - redirect user to authorize

      return Response.redirect(result.authUrl);

    }


    // Server is ready - tools are now available

    const state = this.getMcpServers();

    console.log(`Connected! ${state.tools.length} tools available`);


    return new Response("MCP server connected");

  }

}


```

Connections persist in the agent's [SQL storage](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/), and when an agent connects to an MCP server, all tools from that server become available automatically.

## Adding MCP servers

Use `addMcpServer()` to connect to an MCP server. For non-OAuth servers, no options are needed:

* [  JavaScript ](#tab-panel-2432)
* [  TypeScript ](#tab-panel-2433)

JavaScript

```

// Non-OAuth server — no options required

await this.addMcpServer("notion", "https://mcp.notion.so/mcp");


// OAuth server — provide callbackHost for the OAuth redirect flow

await this.addMcpServer("github", "https://mcp.github.com/mcp", {

  callbackHost: "https://my-worker.workers.dev",

});


```

TypeScript

```

// Non-OAuth server — no options required

await this.addMcpServer("notion", "https://mcp.notion.so/mcp");


// OAuth server — provide callbackHost for the OAuth redirect flow

await this.addMcpServer("github", "https://mcp.github.com/mcp", {

  callbackHost: "https://my-worker.workers.dev",

});


```

### Transport options

MCP supports multiple transport types:

* [  JavaScript ](#tab-panel-2434)
* [  TypeScript ](#tab-panel-2435)

JavaScript

```

await this.addMcpServer("server", "https://mcp.example.com/mcp", {

  transport: {

    type: "streamable-http",

  },

});


```

TypeScript

```

await this.addMcpServer("server", "https://mcp.example.com/mcp", {

  transport: {

    type: "streamable-http",

  },

});


```

| Transport       | Description                                         |
| --------------- | --------------------------------------------------- |
| auto            | Auto-detect based on server response (default)      |
| streamable-http | HTTP with streaming                                 |
| sse             | Server-Sent Events - legacy/compatibility transport |

### Custom headers

For servers behind authentication (like Cloudflare Access) or using bearer tokens:

* [  JavaScript ](#tab-panel-2436)
* [  TypeScript ](#tab-panel-2437)

JavaScript

```

await this.addMcpServer("internal", "https://internal-mcp.example.com/mcp", {

  transport: {

    headers: {

      Authorization: "Bearer my-token",

      "CF-Access-Client-Id": "...",

      "CF-Access-Client-Secret": "...",

    },

  },

});


```

TypeScript

```

await this.addMcpServer("internal", "https://internal-mcp.example.com/mcp", {

  transport: {

    headers: {

      Authorization: "Bearer my-token",

      "CF-Access-Client-Id": "...",

      "CF-Access-Client-Secret": "...",

    },

  },

});


```

### URL security

MCP server URLs are validated before connection to prevent Server-Side Request Forgery (SSRF). The following URL targets are blocked:

* Private/internal IP ranges (RFC 1918: `10.x`, `172.16-31.x`, `192.168.x`)
* Loopback addresses (`127.x`, `::1`)
* Link-local addresses (`169.254.x`, `fe80::`)
* Cloud metadata endpoints (`169.254.169.254`)

If you need to connect to an internal MCP server, use the [RPC transport](https://developers.cloudflare.com/agents/model-context-protocol/transport/) with a Durable Object binding instead of HTTP.

### Return value

`addMcpServer()` returns the connection state:

* `ready` \- Server connected and tools discovered
* `authenticating` \- Server requires OAuth; redirect user to `authUrl`

## OAuth authentication

Many MCP servers require OAuth authentication. The agent handles the OAuth flow automatically.

### How it works

sequenceDiagram
    participant Client
    participant Agent
    participant MCPServer

    Client->>Agent: addMcpServer(name, url)
    Agent->>MCPServer: Connect
    MCPServer-->>Agent: Requires OAuth
    Agent-->>Client: state: authenticating, authUrl
    Client->>MCPServer: User authorizes
    MCPServer->>Agent: Callback with code
    Agent->>MCPServer: Exchange for token
    Agent-->>Client: onMcpUpdate (ready)

### Handling OAuth in your agent

* [  JavaScript ](#tab-panel-2442)
* [  TypeScript ](#tab-panel-2443)

JavaScript

```

class MyAgent extends Agent {

  async onRequest(request) {

    const result = await this.addMcpServer(

      "github",

      "https://mcp.github.com/mcp",

    );


    if (result.state === "authenticating") {

      // Redirect the user to the OAuth authorization page

      return Response.redirect(result.authUrl);

    }


    return Response.json({ status: "connected", id: result.id });

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onRequest(request: Request) {

    const result = await this.addMcpServer(

      "github",

      "https://mcp.github.com/mcp",

    );


    if (result.state === "authenticating") {

      // Redirect the user to the OAuth authorization page

      return Response.redirect(result.authUrl);

    }


    return Response.json({ status: "connected", id: result.id });

  }

}


```

### OAuth callback

The callback URL is automatically constructed:

```

https://{host}/{agentsPrefix}/{agent-name}/{instance-name}/callback


```

For example: `https://my-worker.workers.dev/agents/my-agent/default/callback`

OAuth tokens are securely stored in SQLite, and persist across agent restarts.

### Protecting instance names in OAuth callbacks

When using `sendIdentityOnConnect: false` to hide sensitive instance names (like session IDs or user IDs), the default OAuth callback URL would expose the instance name. To prevent this security issue, you must provide a custom `callbackPath`.

* [  JavaScript ](#tab-panel-2462)
* [  TypeScript ](#tab-panel-2463)

JavaScript

```

import { Agent, routeAgentRequest, getAgentByName } from "agents";


export class SecureAgent extends Agent {

  static options = { sendIdentityOnConnect: false };


  async onRequest(request) {

    // callbackPath is required when sendIdentityOnConnect is false

    const result = await this.addMcpServer(

      "github",

      "https://mcp.github.com/mcp",

      {

        callbackPath: "mcp-oauth-callback", // Custom path without instance name

      },

    );


    if (result.state === "authenticating") {

      return Response.redirect(result.authUrl);

    }


    return new Response("Connected!");

  }

}


// Route the custom callback path to the agent

export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    // Route custom MCP OAuth callback to agent instance

    if (url.pathname.startsWith("/mcp-oauth-callback")) {

      // Implement this to extract the instance name from your session/auth mechanism

      const instanceName = await getInstanceNameFromSession(request);


      const agent = await getAgentByName(env.SecureAgent, instanceName);

      return agent.fetch(request);

    }


    // Standard agent routing

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { Agent, routeAgentRequest, getAgentByName } from "agents";


export class SecureAgent extends Agent {

  static options = { sendIdentityOnConnect: false };


  async onRequest(request: Request) {

    // callbackPath is required when sendIdentityOnConnect is false

    const result = await this.addMcpServer(

      "github",

      "https://mcp.github.com/mcp",

      {

        callbackPath: "mcp-oauth-callback", // Custom path without instance name

      },

    );


    if (result.state === "authenticating") {

      return Response.redirect(result.authUrl);

    }


    return new Response("Connected!");

  }

}


// Route the custom callback path to the agent

export default {

  async fetch(request: Request, env: Env) {

    const url = new URL(request.url);


    // Route custom MCP OAuth callback to agent instance

    if (url.pathname.startsWith("/mcp-oauth-callback")) {

      // Implement this to extract the instance name from your session/auth mechanism

      const instanceName = await getInstanceNameFromSession(request);


      const agent = await getAgentByName(env.SecureAgent, instanceName);

      return agent.fetch(request);

    }


    // Standard agent routing

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

How callback matching works

OAuth callbacks are matched by the `state` query parameter (format: `{serverId}:{stateValue}`), not by URL path. This means your custom `callbackPath` can be any path you choose, as long as requests to that path are routed to the correct agent instance.

### Custom OAuth callback handling

Configure how OAuth completion is handled. By default, successful authentication redirects to your application origin, while failed authentication displays an HTML error page.

* [  JavaScript ](#tab-panel-2452)
* [  TypeScript ](#tab-panel-2453)

JavaScript

```

export class MyAgent extends Agent {

  onStart() {

    this.mcp.configureOAuthCallback({

      // Redirect after successful auth

      successRedirect: "https://myapp.com/success",


      // Redirect on error with error message in query string

      errorRedirect: "https://myapp.com/error",


      // Or use a custom handler

      customHandler: () => {

        // Close popup window after auth completes

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }

}


```

TypeScript

```

export class MyAgent extends Agent {

  onStart() {

    this.mcp.configureOAuthCallback({

      // Redirect after successful auth

      successRedirect: "https://myapp.com/success",


      // Redirect on error with error message in query string

      errorRedirect: "https://myapp.com/error",


      // Or use a custom handler

      customHandler: () => {

        // Close popup window after auth completes

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }

}


```

## Using MCP capabilities

Once connected, access the server's capabilities:

### Getting available tools

* [  JavaScript ](#tab-panel-2440)
* [  TypeScript ](#tab-panel-2441)

JavaScript

```

const state = this.getMcpServers();


// All tools from all connected servers

for (const tool of state.tools) {

  console.log(`Tool: ${tool.name}`);

  console.log(`  From server: ${tool.serverId}`);

  console.log(`  Description: ${tool.description}`);

}


```

TypeScript

```

const state = this.getMcpServers();


// All tools from all connected servers

for (const tool of state.tools) {

  console.log(`Tool: ${tool.name}`);

  console.log(`  From server: ${tool.serverId}`);

  console.log(`  Description: ${tool.description}`);

}


```

### Resources and prompts

* [  JavaScript ](#tab-panel-2448)
* [  TypeScript ](#tab-panel-2449)

JavaScript

```

const state = this.getMcpServers();


// Available resources

for (const resource of state.resources) {

  console.log(`Resource: ${resource.name} (${resource.uri})`);

}


// Available prompts

for (const prompt of state.prompts) {

  console.log(`Prompt: ${prompt.name}`);

}


```

TypeScript

```

const state = this.getMcpServers();


// Available resources

for (const resource of state.resources) {

  console.log(`Resource: ${resource.name} (${resource.uri})`);

}


// Available prompts

for (const prompt of state.prompts) {

  console.log(`Prompt: ${prompt.name}`);

}


```

### Server status

* [  JavaScript ](#tab-panel-2446)
* [  TypeScript ](#tab-panel-2447)

JavaScript

```

const state = this.getMcpServers();


for (const [id, server] of Object.entries(state.servers)) {

  console.log(`${server.name}: ${server.state}`);

  // state: "ready" | "authenticating" | "connecting" | "connected" | "discovering" | "failed"

}


```

TypeScript

```

const state = this.getMcpServers();


for (const [id, server] of Object.entries(state.servers)) {

  console.log(`${server.name}: ${server.state}`);

  // state: "ready" | "authenticating" | "connecting" | "connected" | "discovering" | "failed"

}


```

### Integration with AI SDK

To use MCP tools with the Vercel AI SDK, use `this.mcp.getAITools()` which converts MCP tools to AI SDK format:

* [  JavaScript ](#tab-panel-2454)
* [  TypeScript ](#tab-panel-2455)

JavaScript

```

import { generateText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class MyAgent extends Agent {

  async onRequest(request) {

    const workersai = createWorkersAI({ binding: this.env.AI });

    const response = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: "What's the weather in San Francisco?",

      tools: this.mcp.getAITools(),

    });


    return new Response(response.text);

  }

}


```

TypeScript

```

import { generateText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class MyAgent extends Agent<Env> {

  async onRequest(request: Request) {

    const workersai = createWorkersAI({ binding: this.env.AI });

    const response = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: "What's the weather in San Francisco?",

      tools: this.mcp.getAITools(),

    });


    return new Response(response.text);

  }

}


```

Note

`getMcpServers().tools` returns raw MCP `Tool` objects for inspection. Use `this.mcp.getAITools()` when passing tools to the AI SDK.

## Managing servers

### Removing a server

* [  JavaScript ](#tab-panel-2444)
* [  TypeScript ](#tab-panel-2445)

JavaScript

```

await this.removeMcpServer(serverId);


```

TypeScript

```

await this.removeMcpServer(serverId);


```

This disconnects from the server and removes it from storage.

### Persistence

MCP servers persist across agent restarts:

* Server configuration stored in SQLite
* OAuth tokens stored securely
* Connections restored automatically when agent wakes

### Listing all servers

* [  JavaScript ](#tab-panel-2450)
* [  TypeScript ](#tab-panel-2451)

JavaScript

```

const state = this.getMcpServers();


for (const [id, server] of Object.entries(state.servers)) {

  console.log(`${id}: ${server.name} (${server.server_url})`);

}


```

TypeScript

```

const state = this.getMcpServers();


for (const [id, server] of Object.entries(state.servers)) {

  console.log(`${id}: ${server.name} (${server.server_url})`);

}


```

## Client-side integration

Connected clients receive real-time MCP updates via WebSocket:

* [  JavaScript ](#tab-panel-2468)
* [  TypeScript ](#tab-panel-2469)

JavaScript

```

import { useAgent } from "agents/react";

import { useState } from "react";


function Dashboard() {

  const [tools, setTools] = useState([]);

  const [servers, setServers] = useState({});


  const agent = useAgent({

    agent: "MyAgent",

    onMcpUpdate: (mcpState) => {

      setTools(mcpState.tools);

      setServers(mcpState.servers);

    },

  });


  return (

    <div>

      <h2>Connected Servers</h2>

      {Object.entries(servers).map(([id, server]) => (

        <div key={id}>

          {server.name}: {server.state}

        </div>

      ))}


      <h2>Available Tools ({tools.length})</h2>

      {tools.map((tool) => (

        <div key={`${tool.serverId}-${tool.name}`}>{tool.name}</div>

      ))}

    </div>

  );

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useState } from "react";


function Dashboard() {

  const [tools, setTools] = useState([]);

  const [servers, setServers] = useState({});


  const agent = useAgent({

    agent: "MyAgent",

    onMcpUpdate: (mcpState) => {

      setTools(mcpState.tools);

      setServers(mcpState.servers);

    },

  });


  return (

    <div>

      <h2>Connected Servers</h2>

      {Object.entries(servers).map(([id, server]) => (

        <div key={id}>

          {server.name}: {server.state}

        </div>

      ))}


      <h2>Available Tools ({tools.length})</h2>

      {tools.map((tool) => (

        <div key={`${tool.serverId}-${tool.name}`}>{tool.name}</div>

      ))}

    </div>

  );

}


```

## API reference

### `addMcpServer()`

Add a connection to an MCP server and make its tools available to your agent.

Calling `addMcpServer` is idempotent when both the server name **and** URL match an existing active connection — the existing connection is returned without creating a duplicate. This makes it safe to call in `onStart()` without worrying about duplicate connections on restart.

If you call `addMcpServer` with the same name but a **different** URL, a new connection is created. Both connections remain active and their tools are merged in `getAITools()`. To replace a server, call `removeMcpServer(oldId)` first.

URLs are normalized before comparison (trailing slashes, default ports, and hostname case are handled), so `https://MCP.Example.com` and `https://mcp.example.com/` are treated as the same URL.

TypeScript

```

// HTTP transport (Streamable HTTP, SSE)

async addMcpServer(

  serverName: string,

  url: string,

  options?: {

    callbackHost?: string;

    callbackPath?: string;

    agentsPrefix?: string;

    client?: ClientOptions;

    transport?: {

      headers?: HeadersInit;

      type?: "sse" | "streamable-http" | "auto";

    };

    retry?: RetryOptions;

  }

): Promise<

  | { id: string; state: "authenticating"; authUrl: string }

  | { id: string; state: "ready" }

>


// RPC transport (Durable Object binding — no HTTP overhead)

async addMcpServer(

  serverName: string,

  binding: DurableObjectNamespace,

  options?: {

    props?: Record<string, unknown>;

    client?: ClientOptions;

    retry?: RetryOptions;

  }

): Promise<{ id: string; state: "ready" }>


```

#### Parameters (HTTP transport)

* `serverName` (string, required) — Display name for the MCP server
* `url` (string, required) — URL of the MCP server endpoint
* `options` (object, optional) — Connection configuration:  
   * `callbackHost` — Host for OAuth callback URL. Only needed for OAuth-authenticated servers. If omitted, automatically derived from the incoming request  
   * `callbackPath` — Custom callback URL path that bypasses the default `/agents/{class}/{name}/callback` construction. **Required when `sendIdentityOnConnect` is `false`** to prevent leaking the instance name. When set, the callback URL becomes `{callbackHost}/{callbackPath}`. You must route this path to the agent instance via `getAgentByName`  
   * `agentsPrefix` — URL prefix for OAuth callback path. Default: `"agents"`. Ignored when `callbackPath` is provided  
   * `client` — MCP client configuration options (passed to `@modelcontextprotocol/sdk` Client constructor). By default, includes `CfWorkerJsonSchemaValidator` for validating tool parameters against JSON schemas  
   * `transport` — Transport layer configuration:  
         * `headers` — Custom HTTP headers for authentication  
         * `type` — Transport type: `"auto"` (default), `"streamable-http"`, or `"sse"`  
   * `retry` — Retry options for connection and reconnection attempts. Persisted and used when restoring connections after hibernation or after OAuth completion. Default: 3 attempts, 500ms base delay, 5s max delay. Refer to [Retries](https://developers.cloudflare.com/agents/api-reference/retries/) for details on `RetryOptions`.

#### Parameters (RPC transport)

* `serverName` (string, required) — Display name for the MCP server
* `binding` (`DurableObjectNamespace`, required) — The Durable Object binding for the `McpAgent` class
* `options` (object, optional) — Connection configuration:  
   * `props` — Initialization data passed to the `McpAgent`'s `onStart(props)`. Use this to pass user context, configuration, or other data to the MCP server instance  
   * `client` — MCP client configuration options  
   * `retry` — Retry options for the connection

RPC transport connects your Agent directly to an `McpAgent` via Durable Object bindings without HTTP overhead. Refer to [MCP Transport](https://developers.cloudflare.com/agents/model-context-protocol/transport/) for details on configuring RPC transport.

#### Returns

A Promise that resolves to a discriminated union based on connection state:

* When `state` is `"authenticating"`:  
   * `id` (string) — Unique identifier for this server connection  
   * `state` (`"authenticating"`) — Server is waiting for OAuth authorization  
   * `authUrl` (string) — OAuth authorization URL for user authentication
* When `state` is `"ready"`:  
   * `id` (string) — Unique identifier for this server connection  
   * `state` (`"ready"`) — Server is fully connected and operational

### `removeMcpServer()`

Disconnect from an MCP server and clean up its resources.

TypeScript

```

async removeMcpServer(id: string): Promise<void>


```

#### Parameters

* `id` (string, required) — Server connection ID returned from `addMcpServer()`

### `getMcpServers()`

Get the current state of all MCP server connections.

TypeScript

```

getMcpServers(): MCPServersState


```

#### Returns

TypeScript

```

type MCPServersState = {

  servers: Record<

    string,

    {

      name: string;

      server_url: string;

      auth_url: string | null;

      state:

        | "authenticating"

        | "connecting"

        | "connected"

        | "discovering"

        | "ready"

        | "failed";

      capabilities: ServerCapabilities | null;

      instructions: string | null;

      error: string | null;

    }

  >;

  tools: Array<Tool & { serverId: string }>;

  prompts: Array<Prompt & { serverId: string }>;

  resources: Array<Resource & { serverId: string }>;

  resourceTemplates: Array<ResourceTemplate & { serverId: string }>;

};


```

The `state` field indicates the connection lifecycle:

* `authenticating` — Waiting for OAuth authorization to complete
* `connecting` — Establishing transport connection
* `connected` — Transport connection established
* `discovering` — Discovering server capabilities (tools, resources, prompts)
* `ready` — Fully connected and operational
* `failed` — Connection failed (see `error` field for details)

The `error` field contains an error message when `state` is `"failed"`. Error messages from external OAuth providers are automatically escaped to prevent XSS attacks, making them safe to display directly in your UI.

### `configureOAuthCallback()`

Configure OAuth callback behavior for MCP servers requiring authentication. This method allows you to customize what happens after a user completes OAuth authorization.

TypeScript

```

this.mcp.configureOAuthCallback(options: {

  successRedirect?: string;

  errorRedirect?: string;

  customHandler?: () => Response | Promise<Response>;

}): void


```

#### Parameters

* `options` (object, required) — OAuth callback configuration:  
   * `successRedirect` (string, optional) — URL to redirect to after successful authentication  
   * `errorRedirect` (string, optional) — URL to redirect to after failed authentication. Error message is appended as `?error=<message>` query parameter  
   * `customHandler` (function, optional) — Custom handler for complete control over the callback response. Must return a Response

#### Default behavior

When no configuration is provided:

* **Success**: Redirects to your application origin
* **Failure**: Displays an HTML error page with the error message

If OAuth fails, the connection state becomes `"failed"` and the error message is stored in the `server.error` field for display in your UI.

#### Usage

Configure in `onStart()` before any OAuth flows begin:

* [  JavaScript ](#tab-panel-2460)
* [  TypeScript ](#tab-panel-2461)

JavaScript

```

export class MyAgent extends Agent {

  onStart() {

    // Option 1: Simple redirects

    this.mcp.configureOAuthCallback({

      successRedirect: "/dashboard",

      errorRedirect: "/auth-error",

    });


    // Option 2: Custom handler (e.g., for popup windows)

    this.mcp.configureOAuthCallback({

      customHandler: () => {

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }

}


```

TypeScript

```

export class MyAgent extends Agent {

  onStart() {

    // Option 1: Simple redirects

    this.mcp.configureOAuthCallback({

      successRedirect: "/dashboard",

      errorRedirect: "/auth-error",

    });


    // Option 2: Custom handler (e.g., for popup windows)

    this.mcp.configureOAuthCallback({

      customHandler: () => {

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }

}


```

## Custom OAuth provider

Override the default OAuth provider used when connecting to MCP servers by implementing `createMcpOAuthProvider()` on your Agent class. This enables custom authentication strategies such as pre-registered client credentials or mTLS, beyond the built-in dynamic client registration.

The override is used for both new connections (`addMcpServer`) and restored connections after a Durable Object restart.

* [  JavaScript ](#tab-panel-2466)
* [  TypeScript ](#tab-panel-2467)

JavaScript

```

import { Agent } from "agents";

export class MyAgent extends Agent {

  createMcpOAuthProvider(callbackUrl) {

    const env = this.env;

    return {

      get redirectUrl() {

        return callbackUrl;

      },

      get clientMetadata() {

        return {

          client_id: env.MCP_CLIENT_ID,

          client_secret: env.MCP_CLIENT_SECRET,

          redirect_uris: [callbackUrl],

        };

      },

      clientInformation() {

        return {

          client_id: env.MCP_CLIENT_ID,

          client_secret: env.MCP_CLIENT_SECRET,

        };

      },

    };

  }

}


```

TypeScript

```

import { Agent } from "agents";

import type { AgentMcpOAuthProvider } from "agents";


export class MyAgent extends Agent<Env> {

  createMcpOAuthProvider(callbackUrl: string): AgentMcpOAuthProvider {

    const env = this.env;

    return {

      get redirectUrl() {

        return callbackUrl;

      },

      get clientMetadata() {

        return {

          client_id: env.MCP_CLIENT_ID,

          client_secret: env.MCP_CLIENT_SECRET,

          redirect_uris: [callbackUrl],

        };

      },

      clientInformation() {

        return {

          client_id: env.MCP_CLIENT_ID,

          client_secret: env.MCP_CLIENT_SECRET,

        };

      },

    };

  }

}


```

If you do not override this method, the agent uses the default provider which performs [OAuth 2.0 Dynamic Client Registration ↗](https://datatracker.ietf.org/doc/html/rfc7591) with the MCP server.

### Custom storage backend

To keep the built-in OAuth logic (CSRF state, PKCE, nonce generation, token management) but route token storage to a different backend, import `DurableObjectOAuthClientProvider` and pass your own storage adapter:

* [  JavaScript ](#tab-panel-2456)
* [  TypeScript ](#tab-panel-2457)

JavaScript

```

import { Agent, DurableObjectOAuthClientProvider } from "agents";

export class MyAgent extends Agent {

  createMcpOAuthProvider(callbackUrl) {

    return new DurableObjectOAuthClientProvider(

      myCustomStorage, // any DurableObjectStorage-compatible adapter

      this.name,

      callbackUrl,

    );

  }

}


```

TypeScript

```

import { Agent, DurableObjectOAuthClientProvider } from "agents";

import type { AgentMcpOAuthProvider } from "agents";


export class MyAgent extends Agent {

  createMcpOAuthProvider(callbackUrl: string): AgentMcpOAuthProvider {

    return new DurableObjectOAuthClientProvider(

      myCustomStorage, // any DurableObjectStorage-compatible adapter

      this.name,

      callbackUrl,

    );

  }

}


```

## Advanced: MCPClientManager

For fine-grained control, use `this.mcp` directly:

### Step-by-step connection

* [  JavaScript ](#tab-panel-2470)
* [  TypeScript ](#tab-panel-2471)

JavaScript

```

// 1. Register the server (saves to storage and creates in-memory connection)

const id = "my-server";

await this.mcp.registerServer(id, {

  url: "https://mcp.example.com/mcp",

  name: "My Server",

  callbackUrl: "https://my-worker.workers.dev/agents/my-agent/default/callback",

  transport: { type: "auto" },

});


// 2. Connect (initializes transport, handles OAuth if needed)

const connectResult = await this.mcp.connectToServer(id);


if (connectResult.state === "failed") {

  console.error("Connection failed:", connectResult.error);

  return;

}


if (connectResult.state === "authenticating") {

  console.log("OAuth required:", connectResult.authUrl);

  return;

}


// 3. Discover capabilities (transitions from "connected" to "ready")

if (connectResult.state === "connected") {

  const discoverResult = await this.mcp.discoverIfConnected(id);


  if (!discoverResult?.success) {

    console.error("Discovery failed:", discoverResult?.error);

  }

}


```

TypeScript

```

// 1. Register the server (saves to storage and creates in-memory connection)

const id = "my-server";

await this.mcp.registerServer(id, {

  url: "https://mcp.example.com/mcp",

  name: "My Server",

  callbackUrl: "https://my-worker.workers.dev/agents/my-agent/default/callback",

  transport: { type: "auto" },

});


// 2. Connect (initializes transport, handles OAuth if needed)

const connectResult = await this.mcp.connectToServer(id);


if (connectResult.state === "failed") {

  console.error("Connection failed:", connectResult.error);

  return;

}


if (connectResult.state === "authenticating") {

  console.log("OAuth required:", connectResult.authUrl);

  return;

}


// 3. Discover capabilities (transitions from "connected" to "ready")

if (connectResult.state === "connected") {

  const discoverResult = await this.mcp.discoverIfConnected(id);


  if (!discoverResult?.success) {

    console.error("Discovery failed:", discoverResult?.error);

  }

}


```

### Event subscription

* [  JavaScript ](#tab-panel-2458)
* [  TypeScript ](#tab-panel-2459)

JavaScript

```

// Listen for state changes (onServerStateChanged is an Event<void>)

const disposable = this.mcp.onServerStateChanged(() => {

  console.log("MCP server state changed");

  this.broadcastMcpServers(); // Notify connected clients

});


// Clean up the subscription when no longer needed

// disposable.dispose();


```

TypeScript

```

// Listen for state changes (onServerStateChanged is an Event<void>)

const disposable = this.mcp.onServerStateChanged(() => {

  console.log("MCP server state changed");

  this.broadcastMcpServers(); // Notify connected clients

});


// Clean up the subscription when no longer needed

// disposable.dispose();


```

Note

MCP server list broadcasts (`cf_agent_mcp_servers`) are automatically filtered to exclude connections where [shouldSendProtocolMessages](https://developers.cloudflare.com/agents/api-reference/protocol-messages/) returned `false`.

### Lifecycle methods

#### `this.mcp.registerServer()`

Register a server without immediately connecting.

TypeScript

```

async registerServer(

  id: string,

  options: {

    url: string;

    name: string;

    callbackUrl: string;

    clientOptions?: ClientOptions;

    transportOptions?: TransportOptions;

  }

): Promise<string>


```

#### `this.mcp.connectToServer()`

Establish a connection to a previously registered server.

TypeScript

```

async connectToServer(id: string): Promise<MCPConnectionResult>


type MCPConnectionResult =

  | { state: "failed"; error: string }

  | { state: "authenticating"; authUrl: string }

  | { state: "connected" }


```

#### `this.mcp.discoverIfConnected()`

Check server capabilities if a connection is active.

TypeScript

```

async discoverIfConnected(

  serverId: string,

  options?: { timeoutMs?: number }

): Promise<MCPDiscoverResult | undefined>


type MCPDiscoverResult = {

  success: boolean;

  state: MCPConnectionState;

  error?: string;

}


```

#### `this.mcp.waitForConnections()`

Wait for all in-flight MCP connection and discovery operations to settle. This is useful when you need `this.mcp.getAITools()` to return the full set of tools immediately after the agent wakes from hibernation.

TypeScript

```

// Wait indefinitely

await this.mcp.waitForConnections();


// Wait with a timeout (milliseconds)

await this.mcp.waitForConnections({ timeout: 10_000 });


```

Note

`AIChatAgent` calls this automatically via its [waitForMcpConnections](https://developers.cloudflare.com/agents/api-reference/chat-agents/#waitformcpconnections) property (defaults to `{ timeout: 10_000 }`). You only need `waitForConnections()` directly when using `Agent` with MCP, or when you want finer control inside `onChatMessage`.

#### `this.mcp.closeConnection()`

Close the connection to a specific server while keeping it registered.

TypeScript

```

async closeConnection(id: string): Promise<void>


```

#### `this.mcp.closeAllConnections()`

Close all active server connections while preserving registrations.

TypeScript

```

async closeAllConnections(): Promise<void>


```

#### `this.mcp.getAITools()`

Get all discovered MCP tools in a format compatible with the AI SDK.

TypeScript

```

getAITools(): ToolSet


```

Tools are automatically namespaced by server ID to prevent conflicts when multiple MCP servers expose tools with the same name.

## Error handling

Use error detection utilities to handle connection errors:

* [  JavaScript ](#tab-panel-2464)
* [  TypeScript ](#tab-panel-2465)

JavaScript

```

import { isUnauthorized, isTransportNotImplemented } from "agents";


export class MyAgent extends Agent {

  async onRequest(request) {

    try {

      await this.addMcpServer("Server", "https://mcp.example.com/mcp");

    } catch (error) {

      if (isUnauthorized(error)) {

        return new Response("Authentication required", { status: 401 });

      } else if (isTransportNotImplemented(error)) {

        return new Response("Transport not supported", { status: 400 });

      }

      throw error;

    }

  }

}


```

TypeScript

```

import { isUnauthorized, isTransportNotImplemented } from "agents";


export class MyAgent extends Agent {

  async onRequest(request: Request) {

    try {

      await this.addMcpServer("Server", "https://mcp.example.com/mcp");

    } catch (error) {

      if (isUnauthorized(error)) {

        return new Response("Authentication required", { status: 401 });

      } else if (isTransportNotImplemented(error)) {

        return new Response("Transport not supported", { status: 400 });

      }

      throw error;

    }

  }

}


```

## Next steps

[ Creating MCP servers ](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) Build your own MCP server. 

[ Client SDK ](https://developers.cloudflare.com/agents/api-reference/client-sdk/) Connect from browsers with onMcpUpdate. 

[ Store and sync state ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Learn about agent persistence. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/mcp-client-api/","name":"McpClient"}}]}
```

---

---
title: createMcpHandler
description: The createMcpHandler function creates a fetch handler to serve your MCP server. Use it when you want a stateless MCP server that runs in a plain Worker (no Durable Object). For stateful MCP servers that persist state across requests, use the McpAgent class instead.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/mcp-handler-api.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# createMcpHandler

The `createMcpHandler` function creates a fetch handler to serve your [MCP server](https://developers.cloudflare.com/agents/model-context-protocol/). Use it when you want a stateless MCP server that runs in a plain Worker (no Durable Object). For stateful MCP servers that persist state across requests, use the [McpAgent](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api) class instead.

It uses an implementation of the MCP Transport interface, `WorkerTransport`, built on top of web standards, which conforms to the [streamable-http ↗](https://modelcontextprotocol.io/specification/draft/basic/transports/#streamable-http) transport specification.

TypeScript

```

import { createMcpHandler, type CreateMcpHandlerOptions } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


function createMcpHandler(

  server: McpServer,

  options?: CreateMcpHandlerOptions,

): (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response>;


```

#### Parameters

* **server** — An instance of [McpServer ↗](https://modelcontextprotocol.io/docs/develop/build-server#node) from the `@modelcontextprotocol/sdk` package
* **options** — Optional configuration object (see [CreateMcpHandlerOptions](#createmcphandleroptions))

#### Returns

A Worker fetch handler function with the signature `(request: Request, env: unknown, ctx: ExecutionContext) => Promise<Response>`.

### CreateMcpHandlerOptions

Configuration options for creating an MCP handler.

TypeScript

```

interface CreateMcpHandlerOptions extends WorkerTransportOptions {

  /**

   * The route path that this MCP handler should respond to.

   * If specified, the handler will only process requests that match this route.

   * @default "/mcp"

   */

  route?: string;


  /**

   * An optional auth context to use for handling MCP requests.

   * If not provided, the handler will look for props in the execution context.

   */

  authContext?: McpAuthContext;


  /**

   * An optional transport to use for handling MCP requests.

   * If not provided, a WorkerTransport will be created with the provided WorkerTransportOptions.

   */

  transport?: WorkerTransport;


  // Inherited from WorkerTransportOptions:

  sessionIdGenerator?: () => string;

  enableJsonResponse?: boolean;

  onsessioninitialized?: (sessionId: string) => void;

  corsOptions?: CORSOptions;

  storage?: MCPStorageApi;

}


```

#### Options

##### route

The URL path where the MCP handler responds. Requests to other paths return a 404 response.

**Default:** `"/mcp"`

* [  JavaScript ](#tab-panel-2472)
* [  TypeScript ](#tab-panel-2473)

JavaScript

```

const handler = createMcpHandler(server, {

  route: "/api/mcp", // Only respond to requests at /api/mcp

});


```

TypeScript

```

const handler = createMcpHandler(server, {

  route: "/api/mcp", // Only respond to requests at /api/mcp

});


```

#### authContext

An authentication context object that will be available to MCP tools via [getMcpAuthContext()](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api#authentication-context).

When using the [OAuthProvider](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) from `@cloudflare/workers-oauth-provider`, the authentication context is automatically populated with information from the OAuth flow. You typically don't need to set this manually.

#### transport

A custom `WorkerTransport` instance. If not provided, a new transport is created on every request.

* [  JavaScript ](#tab-panel-2474)
* [  TypeScript ](#tab-panel-2475)

JavaScript

```

import { createMcpHandler, WorkerTransport } from "agents/mcp";


const transport = new WorkerTransport({

  sessionIdGenerator: () => `session-${crypto.randomUUID()}`,

  storage: {

    get: () => myStorage.get("transport-state"),

    set: (state) => myStorage.put("transport-state", state),

  },

});


const handler = createMcpHandler(server, { transport });


```

TypeScript

```

import { createMcpHandler, WorkerTransport } from "agents/mcp";


const transport = new WorkerTransport({

  sessionIdGenerator: () => `session-${crypto.randomUUID()}`,

  storage: {

    get: () => myStorage.get("transport-state"),

    set: (state) => myStorage.put("transport-state", state),

  },

});


const handler = createMcpHandler(server, { transport });


```

## Stateless MCP Servers

Many MCP Servers are stateless, meaning they do not maintain any session state between requests. The `createMcpHandler` function is a lightweight alternative to the `McpAgent` class that can be used to serve an MCP server straight from a Worker. View the [complete example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker).

Breaking change in MCP SDK 1.26.0

**Important:** If you are upgrading from MCP SDK versions before 1.26.0, you must update how you create `McpServer` instances in stateless servers.

MCP SDK 1.26.0 introduces a guard that prevents connecting to a server instance that has already been connected to a transport. This fixes a security vulnerability ([CVE ↗](https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-345p-7cg4-v4c7)) where sharing server or transport instances could leak cross-client response data.

**If your stateless MCP server declares `McpServer` or transport instances in the global scope, you must create new instances per request.**

See the [migration guide](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/#migration-guide-for-mcp-sdk-1260) below for details.

* [  JavaScript ](#tab-panel-2494)
* [  TypeScript ](#tab-panel-2495)

JavaScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({

    name: "Hello MCP Server",

    version: "1.0.0",

  });


  server.tool(

    "hello",

    "Returns a greeting message",

    { name: z.string().optional() },

    async ({ name }) => {

      return {

        content: [

          {

            text: `Hello, ${name ?? "World"}!`,

            type: "text",

          },

        ],

      };

    },

  );


  return server;

}


export default {

  fetch: async (request, env, ctx) => {

    // Create new server instance per request

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

};


```

TypeScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


function createServer() {

  const server = new McpServer({

    name: "Hello MCP Server",

    version: "1.0.0",

  });


  server.tool(

    "hello",

    "Returns a greeting message",

    { name: z.string().optional() },

    async ({ name }) => {

      return {

        content: [

          {

            text: `Hello, ${name ?? "World"}!`,

            type: "text",

          },

        ],

      };

    },

  );


  return server;

}


export default {

  fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {

    // Create new server instance per request

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

} satisfies ExportedHandler<Env>;


```

Each request to this MCP server creates a new session and server instance. The server does not maintain state between requests. This is the simplest way to implement an MCP server.

## Stateful MCP Servers

For stateful MCP servers that need to maintain session state across multiple requests, you can use the `createMcpHandler` function with a `WorkerTransport` instance directly in an `Agent`. This is useful if you want to make use of advanced client features like elicitation and sampling.

Provide a custom `WorkerTransport` with persistent storage. View the [complete example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation).

* [  JavaScript ](#tab-panel-2496)
* [  TypeScript ](#tab-panel-2497)

JavaScript

```

import { Agent } from "agents";

import { createMcpHandler, WorkerTransport } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


const STATE_KEY = "mcp-transport-state";


export class MyStatefulMcpAgent extends Agent {

  server = new McpServer({

    name: "Stateful MCP Server",

    version: "1.0.0",

  });


  transport = new WorkerTransport({

    sessionIdGenerator: () => this.name,

    storage: {

      get: () => {

        return this.ctx.storage.get(STATE_KEY);

      },

      set: (state) => {

        this.ctx.storage.put(STATE_KEY, state);

      },

    },

  });


  async onRequest(request) {

    return createMcpHandler(this.server, {

      transport: this.transport,

    })(request, this.env, this.ctx);

  }

}


```

TypeScript

```

import { Agent } from "agents";

import {

  createMcpHandler,

  WorkerTransport,

  type TransportState,

} from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


const STATE_KEY = "mcp-transport-state";


type State = { counter: number };


export class MyStatefulMcpAgent extends Agent<Env, State> {

  server = new McpServer({

    name: "Stateful MCP Server",

    version: "1.0.0",

  });


  transport = new WorkerTransport({

    sessionIdGenerator: () => this.name,

    storage: {

      get: () => {

        return this.ctx.storage.get<TransportState>(STATE_KEY);

      },

      set: (state: TransportState) => {

        this.ctx.storage.put(STATE_KEY, state);

      },

    },

  });


  async onRequest(request: Request) {

    return createMcpHandler(this.server, {

      transport: this.transport,

    })(request, this.env, this.ctx as unknown as ExecutionContext);

  }

}


```

In this case we are defining the `sessionIdGenerator` to return the Agent name as the session ID. To make sure we route to the correct Agent we can use `getAgentByName` in the Worker handler:

* [  JavaScript ](#tab-panel-2480)
* [  TypeScript ](#tab-panel-2481)

JavaScript

```

import { getAgentByName } from "agents";


export default {

  async fetch(request, env, ctx) {

    // Extract session ID from header or generate a new one

    const sessionId =

      request.headers.get("mcp-session-id") ?? crypto.randomUUID();


    // Get the Agent instance by name/session ID

    const agent = await getAgentByName(env.MyStatefulMcpAgent, sessionId);


    // Route the MCP request to the agent

    return await agent.onRequest(request);

  },

};


```

TypeScript

```

import { getAgentByName } from "agents";


export default {

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    // Extract session ID from header or generate a new one

    const sessionId =

      request.headers.get("mcp-session-id") ?? crypto.randomUUID();


    // Get the Agent instance by name/session ID

    const agent = await getAgentByName(env.MyStatefulMcpAgent, sessionId);


    // Route the MCP request to the agent

    return await agent.onRequest(request);

  },

} satisfies ExportedHandler<Env>;


```

With persistent storage, the transport preserves:

* Session ID across reconnections
* Protocol version negotiation state
* Initialization status

This allows MCP clients to reconnect and resume their session in the event of a connection loss.

## Migration Guide for MCP SDK 1.26.0

The MCP SDK 1.26.0 introduces a breaking change for stateless MCP servers that addresses a critical security vulnerability where responses from one client could leak to another client when using shared server or transport instances.

### Who is affected?

| Server Type                                 | Affected? | Action Required                                |
| ------------------------------------------- | --------- | ---------------------------------------------- |
| Stateful servers using Agent/Durable Object | No        | No changes needed                              |
| Stateless servers using createMcpHandler    | Yes       | Create new McpServer per request               |
| Stateless servers using raw SDK transport   | Yes       | Create new McpServer and transport per request |

### Why is this necessary?

The previous pattern of declaring `McpServer` instances in the global scope allowed responses from one client to leak to another client. This is a security vulnerability. The new SDK version prevents this by throwing an error if you try to connect a server that is already connected.

### Before (broken with SDK 1.26.0)

* [  JavaScript ](#tab-panel-2486)
* [  TypeScript ](#tab-panel-2487)

JavaScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


// INCORRECT: Global server instance

const server = new McpServer({

  name: "Hello MCP Server",

  version: "1.0.0",

});


server.tool("hello", "Returns a greeting", {}, async () => {

  return {

    content: [{ text: "Hello, World!", type: "text" }],

  };

});


export default {

  fetch: async (request, env, ctx) => {

    // This will fail on second request with MCP SDK 1.26.0+

    return createMcpHandler(server)(request, env, ctx);

  },

};


```

TypeScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


// INCORRECT: Global server instance

const server = new McpServer({

  name: "Hello MCP Server",

  version: "1.0.0",

});


server.tool("hello", "Returns a greeting", {}, async () => {

  return {

    content: [{ text: "Hello, World!", type: "text" }],

  };

});


export default {

  fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {

    // This will fail on second request with MCP SDK 1.26.0+

    return createMcpHandler(server)(request, env, ctx);

  },

} satisfies ExportedHandler<Env>;


```

### After (correct)

* [  JavaScript ](#tab-panel-2492)
* [  TypeScript ](#tab-panel-2493)

JavaScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


// CORRECT: Factory function to create server instance

function createServer() {

  const server = new McpServer({

    name: "Hello MCP Server",

    version: "1.0.0",

  });


  server.tool("hello", "Returns a greeting", {}, async () => {

    return {

      content: [{ text: "Hello, World!", type: "text" }],

    };

  });


  return server;

}


export default {

  fetch: async (request, env, ctx) => {

    // Create new server instance per request

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

};


```

TypeScript

```

import { createMcpHandler } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


// CORRECT: Factory function to create server instance

function createServer() {

  const server = new McpServer({

    name: "Hello MCP Server",

    version: "1.0.0",

  });


  server.tool("hello", "Returns a greeting", {}, async () => {

    return {

      content: [{ text: "Hello, World!", type: "text" }],

    };

  });


  return server;

}


export default {

  fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {

    // Create new server instance per request

    const server = createServer();

    return createMcpHandler(server)(request, env, ctx);

  },

} satisfies ExportedHandler<Env>;


```

### For raw SDK transport users

If you are using the raw SDK transport directly (not via `createMcpHandler`), you must also create new transport instances per request:

* [  JavaScript ](#tab-panel-2490)
* [  TypeScript ](#tab-panel-2491)

JavaScript

```

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";


function createServer() {

  const server = new McpServer({

    name: "Hello MCP Server",

    version: "1.0.0",

  });


  // Register tools...


  return server;

}


export default {

  async fetch(request) {

    // Create new transport and server per request

    const transport = new WebStandardStreamableHTTPServerTransport();

    const server = createServer();

    server.connect(transport);

    return transport.handleRequest(request);

  },

};


```

TypeScript

```

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";


function createServer() {

  const server = new McpServer({

    name: "Hello MCP Server",

    version: "1.0.0",

  });


  // Register tools...


  return server;

}


export default {

  async fetch(request: Request) {

    // Create new transport and server per request

    const transport = new WebStandardStreamableHTTPServerTransport();

    const server = createServer();

    server.connect(transport);

    return transport.handleRequest(request);

  },

} satisfies ExportedHandler<Env>;


```

### WorkerTransport

The `WorkerTransport` class implements the MCP Transport interface, handling HTTP request/response cycles, Server-Sent Events (SSE) streaming, session management, and CORS.

TypeScript

```

class WorkerTransport implements Transport {

  sessionId?: string;

  started: boolean;

  onclose?: () => void;

  onerror?: (error: Error) => void;

  onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;


  constructor(options?: WorkerTransportOptions);


  async handleRequest(

    request: Request,

    parsedBody?: unknown,

  ): Promise<Response>;

  async send(

    message: JSONRPCMessage,

    options?: TransportSendOptions,

  ): Promise<void>;

  async start(): Promise<void>;

  async close(): Promise<void>;

}


```

#### Constructor Options

TypeScript

```

interface WorkerTransportOptions {

  /**

   * Function that generates a unique session ID.

   * Called when a new session is initialized.

   */

  sessionIdGenerator?: () => string;


  /**

   * Enable traditional Request/Response mode, disabling streaming.

   * When true, responses are returned as JSON instead of SSE streams.

   * @default false

   */

  enableJsonResponse?: boolean;


  /**

   * Callback invoked when a session is initialized.

   * Receives the generated or restored session ID.

   */

  onsessioninitialized?: (sessionId: string) => void;


  /**

   * CORS configuration for cross-origin requests.

   * Configures Access-Control-* headers.

   */

  corsOptions?: CORSOptions;


  /**

   * Optional storage API for persisting transport state.

   * Use this to store session state in Durable Object/Agent storage

   * so it survives hibernation/restart.

   */

  storage?: MCPStorageApi;

}


```

#### sessionIdGenerator

Provides a custom session identifier. This session identifier is used to identify the session in the MCP Client.

* [  JavaScript ](#tab-panel-2476)
* [  TypeScript ](#tab-panel-2477)

JavaScript

```

const transport = new WorkerTransport({

  sessionIdGenerator: () => `user-${Date.now()}-${Math.random()}`,

});


```

TypeScript

```

const transport = new WorkerTransport({

  sessionIdGenerator: () => `user-${Date.now()}-${Math.random()}`,

});


```

#### enableJsonResponse

Disables SSE streaming and returns responses as standard JSON.

* [  JavaScript ](#tab-panel-2478)
* [  TypeScript ](#tab-panel-2479)

JavaScript

```

const transport = new WorkerTransport({

  enableJsonResponse: true, // Disable streaming, return JSON responses

});


```

TypeScript

```

const transport = new WorkerTransport({

  enableJsonResponse: true, // Disable streaming, return JSON responses

});


```

#### onsessioninitialized

A callback that fires when a session is initialized, either by creating a new session or restoring from storage.

* [  JavaScript ](#tab-panel-2482)
* [  TypeScript ](#tab-panel-2483)

JavaScript

```

const transport = new WorkerTransport({

  onsessioninitialized: (sessionId) => {

    console.log(`MCP session initialized: ${sessionId}`);

  },

});


```

TypeScript

```

const transport = new WorkerTransport({

  onsessioninitialized: (sessionId) => {

    console.log(`MCP session initialized: ${sessionId}`);

  },

});


```

#### corsOptions

Configure CORS headers for cross-origin requests.

TypeScript

```

interface CORSOptions {

  origin?: string;

  methods?: string;

  headers?: string;

  maxAge?: number;

  exposeHeaders?: string;

}


```

* [  JavaScript ](#tab-panel-2484)
* [  TypeScript ](#tab-panel-2485)

JavaScript

```

const transport = new WorkerTransport({

  corsOptions: {

    origin: "https://example.com",

    methods: "GET, POST, OPTIONS",

    headers: "Content-Type, Authorization",

    maxAge: 86400,

  },

});


```

TypeScript

```

const transport = new WorkerTransport({

  corsOptions: {

    origin: "https://example.com",

    methods: "GET, POST, OPTIONS",

    headers: "Content-Type, Authorization",

    maxAge: 86400,

  },

});


```

#### storage

Persist transport state to survive Durable Object hibernation or restarts.

TypeScript

```

interface MCPStorageApi {

  get(): Promise<TransportState | undefined> | TransportState | undefined;

  set(state: TransportState): Promise<void> | void;

}


interface TransportState {

  sessionId?: string;

  initialized: boolean;

  protocolVersion?: ProtocolVersion;

}


```

* [  JavaScript ](#tab-panel-2488)
* [  TypeScript ](#tab-panel-2489)

JavaScript

```

// Inside an Agent or Durable Object class method:

const transport = new WorkerTransport({

  storage: {

    get: async () => {

      return await this.ctx.storage.get("mcp-state");

    },

    set: async (state) => {

      await this.ctx.storage.put("mcp-state", state);

    },

  },

});


```

TypeScript

```

// Inside an Agent or Durable Object class method:

const transport = new WorkerTransport({

  storage: {

    get: async () => {

      return await this.ctx.storage.get<TransportState>("mcp-state");

    },

    set: async (state) => {

      await this.ctx.storage.put("mcp-state", state);

    },

  },

});


```

## Authentication Context

When using [OAuth authentication](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) with `createMcpHandler`, user information is made available to your MCP tools through `getMcpAuthContext()`. Under the hood this uses `AsyncLocalStorage` to pass the request to the tool handler, keeping the authentication context available.

TypeScript

```

interface McpAuthContext {

  props: Record<string, unknown>;

}


```

### getMcpAuthContext

Retrieve the current authentication context within an MCP tool handler. This returns user information that was populated by the OAuth provider. Note that if using `McpAgent`, this information is accessible directly on `this.props` instead.

TypeScript

```

import { getMcpAuthContext } from "agents/mcp";


function getMcpAuthContext(): McpAuthContext | undefined;


```

* [  JavaScript ](#tab-panel-2500)
* [  TypeScript ](#tab-panel-2501)

JavaScript

```

import { getMcpAuthContext } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


function createServer() {

  const server = new McpServer({ name: "Auth Server", version: "1.0.0" });


  server.tool("getProfile", "Get the current user's profile", {}, async () => {

    const auth = getMcpAuthContext();

    const username = auth?.props?.username;

    const email = auth?.props?.email;


    return {

      content: [

        {

          type: "text",

          text: `User: ${username ?? "anonymous"}, Email: ${email ?? "none"}`,

        },

      ],

    };

  });


  return server;

}


```

TypeScript

```

import { getMcpAuthContext } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";


function createServer() {

  const server = new McpServer({ name: "Auth Server", version: "1.0.0" });


  server.tool("getProfile", "Get the current user's profile", {}, async () => {

    const auth = getMcpAuthContext();

    const username = auth?.props?.username as string | undefined;

    const email = auth?.props?.email as string | undefined;


    return {

      content: [

        {

          type: "text",

          text: `User: ${username ?? "anonymous"}, Email: ${email ?? "none"}`,

        },

      ],

    };

  });


  return server;

}


```

Note

For a complete guide on setting up OAuth authentication with MCP servers, see the [MCP Authorization documentation](https://developers.cloudflare.com/agents/model-context-protocol/authorization/). View the [complete authenticated MCP server in a Worker example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/examples/mcp-worker-authenticated).

## Error Handling

The `createMcpHandler` automatically catches errors and returns JSON-RPC error responses with code `-32603` (Internal error).

* [  JavaScript ](#tab-panel-2498)
* [  TypeScript ](#tab-panel-2499)

JavaScript

```

server.tool("riskyOperation", "An operation that might fail", {}, async () => {

  if (Math.random() > 0.5) {

    throw new Error("Random failure occurred");

  }

  return {

    content: [{ type: "text", text: "Success!" }],

  };

});


// Errors are automatically caught and returned as:

// {

//   "jsonrpc": "2.0",

//   "error": {

//     "code": -32603,

//     "message": "Random failure occurred"

//   },

//   "id": <request_id>

// }


```

TypeScript

```

server.tool("riskyOperation", "An operation that might fail", {}, async () => {

  if (Math.random() > 0.5) {

    throw new Error("Random failure occurred");

  }

  return {

    content: [{ type: "text", text: "Success!" }],

  };

});


// Errors are automatically caught and returned as:

// {

//   "jsonrpc": "2.0",

//   "error": {

//     "code": -32603,

//     "message": "Random failure occurred"

//   },

//   "id": <request_id>

// }


```

## Related Resources

[ Building MCP Servers ](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) Build and deploy MCP servers on Cloudflare. 

[ MCP Tools ](https://developers.cloudflare.com/agents/model-context-protocol/tools/) Add tools to your MCP server. 

[ MCP Authorization ](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) Authenticate users with OAuth. 

[ McpAgent API ](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) Build stateful MCP servers. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/mcp-handler-api/","name":"createMcpHandler"}}]}
```

---

---
title: Observability
description: Agents emit structured events for every significant operation — RPC calls, state changes, schedule execution, workflow transitions, MCP connections, and more. These events are published to diagnostics channels and are silent by default (zero overhead when nobody is listening).
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/observability.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Observability

Agents emit structured events for every significant operation — RPC calls, state changes, schedule execution, workflow transitions, MCP connections, and more. These events are published to [diagnostics channels](https://developers.cloudflare.com/workers/runtime-apis/nodejs/diagnostics-channel/) and are silent by default (zero overhead when nobody is listening).

## Event structure

Every event has these fields:

TypeScript

```

{

  type: "rpc",                        // what happened

  agent: "MyAgent",                   // which agent class emitted it

  name: "user-123",                   // which agent instance (Durable Object name)

  payload: { method: "getWeather" },  // details

  timestamp: 1758005142787            // when (ms since epoch)

}


```

`agent` and `name` identify the source agent — `agent` is the class name and `name` is the Durable Object instance name.

## Channels

Events are routed to eight named channels based on their type:

| Channel          | Event types                                                                                                                                      | Description                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- |
| agents:state     | state:update                                                                                                                                     | State sync events                   |
| agents:rpc       | rpc, rpc:error                                                                                                                                   | RPC method calls and failures       |
| agents:message   | message:request, message:response, message:clear, message:cancel, message:error, tool:result, tool:approval                                      | Chat message and tool lifecycle     |
| agents:schedule  | schedule:create, schedule:execute, schedule:cancel, schedule:retry, schedule:error, queue:create, queue:retry, queue:error                       | Scheduled and queued task lifecycle |
| agents:lifecycle | connect, disconnect, destroy                                                                                                                     | Agent connection and teardown       |
| agents:workflow  | workflow:start, workflow:event, workflow:approved, workflow:rejected, workflow:terminated, workflow:paused, workflow:resumed, workflow:restarted | Workflow state transitions          |
| agents:mcp       | mcp:client:preconnect, mcp:client:connect, mcp:client:authorize, mcp:client:discover                                                             | MCP client operations               |
| agents:email     | email:receive, email:reply                                                                                                                       | Email processing                    |

## Subscribing to events

### Typed subscribe helper

The `subscribe()` function from `agents/observability` provides type-safe access to events on a specific channel:

* [  JavaScript ](#tab-panel-2506)
* [  TypeScript ](#tab-panel-2507)

JavaScript

```

import { subscribe } from "agents/observability";


const unsub = subscribe("rpc", (event) => {

  if (event.type === "rpc") {

    console.log(`RPC call: ${event.payload.method}`);

  }

  if (event.type === "rpc:error") {

    console.error(

      `RPC failed: ${event.payload.method} — ${event.payload.error}`,

    );

  }

});


// Clean up when done

unsub();


```

TypeScript

```

import { subscribe } from "agents/observability";


const unsub = subscribe("rpc", (event) => {

  if (event.type === "rpc") {

    console.log(`RPC call: ${event.payload.method}`);

  }

  if (event.type === "rpc:error") {

    console.error(

      `RPC failed: ${event.payload.method} — ${event.payload.error}`,

    );

  }

});


// Clean up when done

unsub();


```

The callback is fully typed — `event` is narrowed to only the event types that flow through that channel.

### Raw diagnostics\_channel

You can also subscribe directly using the Node.js API:

* [  JavaScript ](#tab-panel-2502)
* [  TypeScript ](#tab-panel-2503)

JavaScript

```

import { subscribe } from "node:diagnostics_channel";


subscribe("agents:schedule", (event) => {

  console.log(event);

});


```

TypeScript

```

import { subscribe } from "node:diagnostics_channel";


subscribe("agents:schedule", (event) => {

  console.log(event);

});


```

## Tail Workers (production)

In production, all diagnostics channel messages are automatically forwarded to [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/). No subscription code is needed in the agent itself — attach a Tail Worker and access events via `event.diagnosticsChannelEvents`:

* [  JavaScript ](#tab-panel-2508)
* [  TypeScript ](#tab-panel-2509)

JavaScript

```

export default {

  async tail(events) {

    for (const event of events) {

      for (const msg of event.diagnosticsChannelEvents) {

        // msg.channel is "agents:rpc", "agents:workflow", etc.

        // msg.message is the typed event payload

        console.log(msg.timestamp, msg.channel, msg.message);

      }

    }

  },

};


```

TypeScript

```

export default {

  async tail(events) {

    for (const event of events) {

      for (const msg of event.diagnosticsChannelEvents) {

        // msg.channel is "agents:rpc", "agents:workflow", etc.

        // msg.message is the typed event payload

        console.log(msg.timestamp, msg.channel, msg.message);

      }

    }

  },

};


```

This gives you structured, filterable observability in production with zero overhead in the agent hot path.

## Custom observability

You can override the default implementation by providing your own `Observability` interface:

* [  JavaScript ](#tab-panel-2510)
* [  TypeScript ](#tab-panel-2511)

JavaScript

```

import { Agent } from "agents";

const myObservability = {

  emit(event) {

    // Send to your logging service, filter events, etc.

    if (event.type === "rpc:error") {

      console.error(event.payload.method, event.payload.error);

    }

  },

};


class MyAgent extends Agent {

  observability = myObservability;

}


```

TypeScript

```

import { Agent } from "agents";

import type { Observability } from "agents/observability";


const myObservability: Observability = {

  emit(event) {

    // Send to your logging service, filter events, etc.

    if (event.type === "rpc:error") {

      console.error(event.payload.method, event.payload.error);

    }

  },

};


class MyAgent extends Agent {

  override observability = myObservability;

}


```

Set `observability` to `undefined` to disable all event emission:

* [  JavaScript ](#tab-panel-2504)
* [  TypeScript ](#tab-panel-2505)

JavaScript

```

import { Agent } from "agents";


class MyAgent extends Agent {

  observability = undefined;

}


```

TypeScript

```

import { Agent } from "agents";


class MyAgent extends Agent {

  override observability = undefined;

}


```

## Event reference

### RPC events

| Type      | Payload                | When                          |
| --------- | ---------------------- | ----------------------------- |
| rpc       | { method, streaming? } | A @callable method is invoked |
| rpc:error | { method, error }      | A @callable method throws     |

### State events

| Type         | Payload | When                 |
| ------------ | ------- | -------------------- |
| state:update | {}      | setState() is called |

### Message and tool events (AIChatAgent)

These events are emitted by `AIChatAgent` from `@cloudflare/ai-chat`. They track the chat message lifecycle, including client-side tool interactions.

| Type             | Payload                  | When                                |
| ---------------- | ------------------------ | ----------------------------------- |
| message:request  | {}                       | A chat message is received          |
| message:response | {}                       | A chat response stream completes    |
| message:clear    | {}                       | Chat history is cleared             |
| message:cancel   | { requestId }            | A streaming request is cancelled    |
| message:error    | { error }                | A chat stream fails                 |
| tool:result      | { toolCallId, toolName } | A client tool result is received    |
| tool:approval    | { toolCallId, approved } | A tool call is approved or rejected |

### Schedule and queue events

| Type             | Payload                                | When                                         |
| ---------------- | -------------------------------------- | -------------------------------------------- |
| schedule:create  | { callback, id }                       | A schedule is created                        |
| schedule:execute | { callback, id }                       | A scheduled callback starts                  |
| schedule:cancel  | { callback, id }                       | A schedule is cancelled                      |
| schedule:retry   | { callback, id, attempt, maxAttempts } | A scheduled callback is retried              |
| schedule:error   | { callback, id, error, attempts }      | A scheduled callback fails after all retries |
| queue:create     | { callback, id }                       | A task is enqueued                           |
| queue:retry      | { callback, id, attempt, maxAttempts } | A queued callback is retried                 |
| queue:error      | { callback, id, error, attempts }      | A queued callback fails after all retries    |

### Lifecycle events

| Type       | Payload                        | When                                  |
| ---------- | ------------------------------ | ------------------------------------- |
| connect    | { connectionId }               | A WebSocket connection is established |
| disconnect | { connectionId, code, reason } | A WebSocket connection is closed      |
| destroy    | {}                             | The agent is destroyed                |

### Workflow events

| Type                | Payload                       | When                           |
| ------------------- | ----------------------------- | ------------------------------ |
| workflow:start      | { workflowId, workflowName? } | A workflow instance is started |
| workflow:event      | { workflowId, eventType? }    | An event is sent to a workflow |
| workflow:approved   | { workflowId, reason? }       | A workflow is approved         |
| workflow:rejected   | { workflowId, reason? }       | A workflow is rejected         |
| workflow:terminated | { workflowId, workflowName? } | A workflow is terminated       |
| workflow:paused     | { workflowId, workflowName? } | A workflow is paused           |
| workflow:resumed    | { workflowId, workflowName? } | A workflow is resumed          |
| workflow:restarted  | { workflowId, workflowName? } | A workflow is restarted        |

### MCP events

| Type                  | Payload                               | When                                         |
| --------------------- | ------------------------------------- | -------------------------------------------- |
| mcp:client:preconnect | { serverId }                          | Before connecting to an MCP server           |
| mcp:client:connect    | { url, transport, state, error? }     | An MCP connection attempt completes or fails |
| mcp:client:authorize  | { serverId, authUrl, clientId? }      | An MCP OAuth flow begins                     |
| mcp:client:discover   | { url?, state?, error?, capability? } | MCP capability discovery succeeds or fails   |

### Email events

| Type          | Payload                | When                  |
| ------------- | ---------------------- | --------------------- |
| email:receive | { from, to, subject? } | An email is received  |
| email:reply   | { from, to, subject? } | A reply email is sent |

## Next steps

[ Configuration ](https://developers.cloudflare.com/agents/api-reference/configuration/) wrangler.jsonc setup and deployment. 

[ Tail Workers ](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) Forward diagnostics channel events to a Tail Worker for production monitoring. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/observability/","name":"Observability"}}]}
```

---

---
title: Protocol messages
description: When a WebSocket client connects to an Agent, the framework automatically sends several JSON text frames — identity, state, and MCP server lists. You can suppress these per-connection protocol messages for clients that cannot handle them.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/protocol-messages.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Protocol messages

When a WebSocket client connects to an Agent, the framework automatically sends several JSON text frames — identity, state, and MCP server lists. You can suppress these per-connection protocol messages for clients that cannot handle them.

## Overview

On every new connection, the Agent sends three protocol messages:

| Message type            | Content                   |
| ----------------------- | ------------------------- |
| cf\_agent\_identity     | Agent name and class      |
| cf\_agent\_state        | Current agent state       |
| cf\_agent\_mcp\_servers | Connected MCP server list |

State and MCP messages are also broadcast to all connections whenever they change.

For most web clients this is fine — the [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/) and `useAgent` hook consume these messages automatically. However, some clients cannot handle JSON text frames:

* **Binary-only clients** — MQTT devices, IoT sensors, custom binary protocols
* **Lightweight clients** — Embedded systems with minimal WebSocket stacks
* **Non-browser clients** — Hardware devices connecting via WebSocket

For these connections, you can suppress protocol messages while keeping everything else (RPC, regular messages, broadcasts via `this.broadcast()`) working normally.

## Suppressing protocol messages

Override `shouldSendProtocolMessages` to control which connections receive protocol messages. Return `false` to suppress them.

* [  JavaScript ](#tab-panel-2512)
* [  TypeScript ](#tab-panel-2513)

JavaScript

```

import { Agent } from "agents";


export class IoTAgent extends Agent {

  shouldSendProtocolMessages(connection, ctx) {

    const url = new URL(ctx.request.url);

    return url.searchParams.get("protocol") !== "false";

  }

}


```

TypeScript

```

import { Agent, type Connection, type ConnectionContext } from "agents";


export class IoTAgent extends Agent<Env, State> {

  shouldSendProtocolMessages(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    return url.searchParams.get("protocol") !== "false";

  }

}


```

This hook runs during `onConnect`, before any messages are sent. When it returns `false`:

* No `cf_agent_identity`, `cf_agent_state`, or `cf_agent_mcp_servers` messages are sent on connect
* The connection is excluded from state and MCP broadcasts going forward
* RPC calls, regular `onMessage` handling, and `this.broadcast()` still work normally

### Using WebSocket subprotocol

You can also check the WebSocket subprotocol header, which is the standard way to negotiate protocols over WebSocket:

* [  JavaScript ](#tab-panel-2514)
* [  TypeScript ](#tab-panel-2515)

JavaScript

```

export class MqttAgent extends Agent {

  shouldSendProtocolMessages(connection, ctx) {

    // MQTT-over-WebSocket clients negotiate via subprotocol

    const subprotocol = ctx.request.headers.get("Sec-WebSocket-Protocol");

    return subprotocol !== "mqtt";

  }

}


```

TypeScript

```

export class MqttAgent extends Agent<Env, State> {

  shouldSendProtocolMessages(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    // MQTT-over-WebSocket clients negotiate via subprotocol

    const subprotocol = ctx.request.headers.get("Sec-WebSocket-Protocol");

    return subprotocol !== "mqtt";

  }

}


```

## Checking protocol status

Use `isConnectionProtocolEnabled` to check whether a connection has protocol messages enabled:

* [  JavaScript ](#tab-panel-2516)
* [  TypeScript ](#tab-panel-2517)

JavaScript

```

export class MyAgent extends Agent {

  @callable()

  async getConnectionInfo() {

    const { connection } = getCurrentAgent();

    if (!connection) return null;


    return {

      protocolEnabled: this.isConnectionProtocolEnabled(connection),

      readonly: this.isConnectionReadonly(connection),

    };

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env, State> {

  @callable()

  async getConnectionInfo() {

    const { connection } = getCurrentAgent();

    if (!connection) return null;


    return {

      protocolEnabled: this.isConnectionProtocolEnabled(connection),

      readonly: this.isConnectionReadonly(connection),

    };

  }

}


```

## What is and is not suppressed

The following table shows what still works when protocol messages are suppressed for a connection:

| Action                                                    | Works? |
| --------------------------------------------------------- | ------ |
| Receive cf\_agent\_identity on connect                    | **No** |
| Receive cf\_agent\_state on connect and broadcasts        | **No** |
| Receive cf\_agent\_mcp\_servers on connect and broadcasts | **No** |
| Send and receive regular WebSocket messages               | Yes    |
| Call @callable() RPC methods                              | Yes    |
| Receive this.broadcast() messages                         | Yes    |
| Send binary data                                          | Yes    |
| Mutate agent state via RPC                                | Yes    |

## Combining with readonly

A connection can be both readonly and protocol-suppressed. This is useful for binary devices that should observe but not modify state:

* [  JavaScript ](#tab-panel-2518)
* [  TypeScript ](#tab-panel-2519)

JavaScript

```

export class SensorHub extends Agent {

  shouldSendProtocolMessages(connection, ctx) {

    const url = new URL(ctx.request.url);

    // Binary sensors don't handle JSON protocol frames

    return url.searchParams.get("type") !== "sensor";

  }


  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    // Sensors can only report data via RPC, not modify shared state

    return url.searchParams.get("type") === "sensor";

  }


  @callable()

  async reportReading(sensorId, value) {

    // This RPC still works for readonly+no-protocol connections

    // because it writes to SQL, not agent state

    this

      .sql`INSERT INTO readings (sensor_id, value, ts) VALUES (${sensorId}, ${value}, ${Date.now()})`;

  }

}


```

TypeScript

```

export class SensorHub extends Agent<Env, SensorState> {

  shouldSendProtocolMessages(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    // Binary sensors don't handle JSON protocol frames

    return url.searchParams.get("type") !== "sensor";

  }


  shouldConnectionBeReadonly(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    // Sensors can only report data via RPC, not modify shared state

    return url.searchParams.get("type") === "sensor";

  }


  @callable()

  async reportReading(sensorId: string, value: number) {

    // This RPC still works for readonly+no-protocol connections

    // because it writes to SQL, not agent state

    this

      .sql`INSERT INTO readings (sensor_id, value, ts) VALUES (${sensorId}, ${value}, ${Date.now()})`;

  }

}


```

Both flags are stored in the connection's WebSocket attachment and hidden from `connection.state` — they do not interfere with each other or with user-defined connection state.

## API reference

### `shouldSendProtocolMessages`

An overridable hook that determines if a connection should receive protocol messages when it connects.

| Parameter   | Type              | Description                         |
| ----------- | ----------------- | ----------------------------------- |
| connection  | Connection        | The connecting client               |
| ctx         | ConnectionContext | Contains the upgrade request        |
| **Returns** | boolean           | false to suppress protocol messages |

Default: returns `true` (all connections receive protocol messages).

This hook is evaluated once on connect. The result is persisted in the connection's WebSocket attachment and survives [hibernation](https://developers.cloudflare.com/agents/api-reference/websockets/#hibernation).

### `isConnectionProtocolEnabled`

Check if a connection currently has protocol messages enabled.

| Parameter   | Type       | Description                           |
| ----------- | ---------- | ------------------------------------- |
| connection  | Connection | The connection to check               |
| **Returns** | boolean    | true if protocol messages are enabled |

Safe to call at any time, including after the agent wakes from hibernation.

## How it works

Protocol status is stored as an internal flag in the connection's WebSocket attachment — the same mechanism used by [readonly connections](https://developers.cloudflare.com/agents/api-reference/readonly-connections/). This means:

* **Survives hibernation** — the flag is serialized and restored when the agent wakes up
* **No cleanup needed** — connection state is automatically discarded when the connection closes
* **Zero overhead** — no database tables or queries, just the connection's built-in attachment
* **Safe from user code** — `connection.state` and `connection.setState()` never expose or overwrite the flag

Unlike [readonly](https://developers.cloudflare.com/agents/api-reference/readonly-connections/) which can be toggled dynamically with `setConnectionReadonly()`, protocol status is set once on connect and cannot be changed afterward. To change a connection's protocol status, the client must disconnect and reconnect.

## Related resources

* [Readonly connections](https://developers.cloudflare.com/agents/api-reference/readonly-connections/)
* [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/)
* [Store and sync state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/)
* [MCP Client API](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/protocol-messages/","name":"Protocol messages"}}]}
```

---

---
title: Queue tasks
description: The Agents SDK provides a built-in queue system that allows you to schedule tasks for asynchronous execution. This is useful for background processing, delayed operations, and managing workloads that do not need immediate execution.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/queue-tasks.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Queue tasks

The Agents SDK provides a built-in queue system that allows you to schedule tasks for asynchronous execution. This is useful for background processing, delayed operations, and managing workloads that do not need immediate execution.

## Overview

The queue system is built into the base `Agent` class. Tasks are stored in a SQLite table and processed automatically in FIFO (First In, First Out) order.

## `QueueItem` type

TypeScript

```

type QueueItem<T> = {

  id: string; // Unique identifier for the queued task

  payload: T; // Data to pass to the callback function

  callback: keyof Agent; // Name of the method to call

  created_at: number; // Timestamp when the task was created

};


```

## Core methods

### `queue()`

Adds a task to the queue for future execution.

TypeScript

```

async queue<T>(callback: keyof this, payload: T): Promise<string>


```

**Parameters:**

* `callback` \- The name of the method to call when processing the task
* `payload` \- Data to pass to the callback method

**Returns:** The unique ID of the queued task

**Example:**

* [  JavaScript ](#tab-panel-2530)
* [  TypeScript ](#tab-panel-2531)

JavaScript

```

class MyAgent extends Agent {

  async processEmail(data) {

    // Process the email

    console.log(`Processing email: ${data.subject}`);

  }


  async onMessage(message) {

    // Queue an email processing task

    const taskId = await this.queue("processEmail", {

      email: "user@example.com",

      subject: "Welcome!",

    });


    console.log(`Queued task with ID: ${taskId}`);

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async processEmail(data: { email: string; subject: string }) {

    // Process the email

    console.log(`Processing email: ${data.subject}`);

  }


  async onMessage(message: string) {

    // Queue an email processing task

    const taskId = await this.queue("processEmail", {

      email: "user@example.com",

      subject: "Welcome!",

    });


    console.log(`Queued task with ID: ${taskId}`);

  }

}


```

### `dequeue()`

Removes a specific task from the queue by ID. This method is synchronous.

TypeScript

```

dequeue(id: string): void


```

**Parameters:**

* `id` \- The ID of the task to remove

**Example:**

* [  JavaScript ](#tab-panel-2520)
* [  TypeScript ](#tab-panel-2521)

JavaScript

```

// Remove a specific task

agent.dequeue("abc123def");


```

TypeScript

```

// Remove a specific task

agent.dequeue("abc123def");


```

### `dequeueAll()`

Removes all tasks from the queue. This method is synchronous.

TypeScript

```

dequeueAll(): void


```

**Example:**

* [  JavaScript ](#tab-panel-2522)
* [  TypeScript ](#tab-panel-2523)

JavaScript

```

// Clear the entire queue

agent.dequeueAll();


```

TypeScript

```

// Clear the entire queue

agent.dequeueAll();


```

### `dequeueAllByCallback()`

Removes all tasks that match a specific callback method. This method is synchronous.

TypeScript

```

dequeueAllByCallback(callback: string): void


```

**Parameters:**

* `callback` \- Name of the callback method

**Example:**

* [  JavaScript ](#tab-panel-2524)
* [  TypeScript ](#tab-panel-2525)

JavaScript

```

// Remove all email processing tasks

agent.dequeueAllByCallback("processEmail");


```

TypeScript

```

// Remove all email processing tasks

agent.dequeueAllByCallback("processEmail");


```

### `getQueue()`

Retrieves a specific queued task by ID. This method is synchronous.

TypeScript

```

getQueue<T>(id: string): QueueItem<T> | undefined


```

**Parameters:**

* `id` \- The ID of the task to retrieve

**Returns:** The `QueueItem` with parsed payload or `undefined` if not found

The payload is automatically parsed from JSON before being returned.

**Example:**

* [  JavaScript ](#tab-panel-2528)
* [  TypeScript ](#tab-panel-2529)

JavaScript

```

const task = agent.getQueue("abc123def");

if (task) {

  console.log(`Task callback: ${task.callback}`);

  console.log(`Task payload:`, task.payload);

}


```

TypeScript

```

const task = agent.getQueue("abc123def");

if (task) {

  console.log(`Task callback: ${task.callback}`);

  console.log(`Task payload:`, task.payload);

}


```

### `getQueues()`

Retrieves all queued tasks that match a specific key-value pair in their payload. This method is synchronous.

TypeScript

```

getQueues<T>(key: string, value: string): QueueItem<T>[]


```

**Parameters:**

* `key` \- The key to filter by in the payload
* `value` \- The value to match

**Returns:** Array of matching `QueueItem` objects

This method fetches all queue items and filters them in memory by parsing each payload and checking if the specified key matches the value.

**Example:**

* [  JavaScript ](#tab-panel-2526)
* [  TypeScript ](#tab-panel-2527)

JavaScript

```

// Find all tasks for a specific user

const userTasks = agent.getQueues("userId", "12345");


```

TypeScript

```

// Find all tasks for a specific user

const userTasks = agent.getQueues("userId", "12345");


```

## How queue processing works

1. **Validation**: When calling `queue()`, the method validates that the callback exists as a function on the agent.
2. **Automatic processing**: After queuing, the system automatically attempts to flush the queue.
3. **FIFO order**: Tasks are processed in the order they were created (`created_at` timestamp).
4. **Context preservation**: Each queued task runs with the same agent context (connection, request, email).
5. **Automatic dequeue**: Successfully executed tasks are automatically removed from the queue.
6. **Error handling**: If a callback method does not exist at execution time, an error is logged and the task is skipped.
7. **Persistence**: Tasks are stored in the `cf_agents_queues` SQL table and survive agent restarts.

## Queue callback methods

When defining callback methods for queued tasks, they must follow this signature:

TypeScript

```

async callbackMethod(payload: unknown, queueItem: QueueItem): Promise<void>


```

**Example:**

* [  JavaScript ](#tab-panel-2534)
* [  TypeScript ](#tab-panel-2535)

JavaScript

```

class MyAgent extends Agent {

  async sendNotification(payload, queueItem) {

    console.log(`Processing task ${queueItem.id}`);

    console.log(

      `Sending notification to user ${payload.userId}: ${payload.message}`,

    );


    // Your notification logic here

    await this.notificationService.send(payload.userId, payload.message);

  }


  async onUserSignup(userData) {

    // Queue a welcome notification

    await this.queue("sendNotification", {

      userId: userData.id,

      message: "Welcome to our platform!",

    });

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async sendNotification(

    payload: { userId: string; message: string },

    queueItem: QueueItem<{ userId: string; message: string }>,

  ) {

    console.log(`Processing task ${queueItem.id}`);

    console.log(

      `Sending notification to user ${payload.userId}: ${payload.message}`,

    );


    // Your notification logic here

    await this.notificationService.send(payload.userId, payload.message);

  }


  async onUserSignup(userData: any) {

    // Queue a welcome notification

    await this.queue("sendNotification", {

      userId: userData.id,

      message: "Welcome to our platform!",

    });

  }

}


```

## Use cases

### Background processing

* [  JavaScript ](#tab-panel-2532)
* [  TypeScript ](#tab-panel-2533)

JavaScript

```

class DataProcessor extends Agent {

  async processLargeDataset(data) {

    const results = await this.heavyComputation(data.datasetId);

    await this.notifyUser(data.userId, results);

  }


  async onDataUpload(uploadData) {

    // Queue the processing instead of doing it synchronously

    await this.queue("processLargeDataset", {

      datasetId: uploadData.id,

      userId: uploadData.userId,

    });


    return { message: "Data upload received, processing started" };

  }

}


```

TypeScript

```

class DataProcessor extends Agent {

  async processLargeDataset(data: { datasetId: string; userId: string }) {

    const results = await this.heavyComputation(data.datasetId);

    await this.notifyUser(data.userId, results);

  }


  async onDataUpload(uploadData: any) {

    // Queue the processing instead of doing it synchronously

    await this.queue("processLargeDataset", {

      datasetId: uploadData.id,

      userId: uploadData.userId,

    });


    return { message: "Data upload received, processing started" };

  }

}


```

### Batch operations

* [  JavaScript ](#tab-panel-2538)
* [  TypeScript ](#tab-panel-2539)

JavaScript

```

class BatchProcessor extends Agent {

  async processBatch(data) {

    for (const item of data.items) {

      await this.processItem(item);

    }

    console.log(`Completed batch ${data.batchId}`);

  }


  async onLargeRequest(items) {

    // Split large requests into smaller batches

    const batchSize = 10;

    for (let i = 0; i < items.length; i += batchSize) {

      const batch = items.slice(i, i + batchSize);

      await this.queue("processBatch", {

        items: batch,

        batchId: `batch-${i / batchSize + 1}`,

      });

    }

  }

}


```

TypeScript

```

class BatchProcessor extends Agent {

  async processBatch(data: { items: any[]; batchId: string }) {

    for (const item of data.items) {

      await this.processItem(item);

    }

    console.log(`Completed batch ${data.batchId}`);

  }


  async onLargeRequest(items: any[]) {

    // Split large requests into smaller batches

    const batchSize = 10;

    for (let i = 0; i < items.length; i += batchSize) {

      const batch = items.slice(i, i + batchSize);

      await this.queue("processBatch", {

        items: batch,

        batchId: `batch-${i / batchSize + 1}`,

      });

    }

  }

}


```

## Error handling

* [  JavaScript ](#tab-panel-2536)
* [  TypeScript ](#tab-panel-2537)

JavaScript

```

class RobustAgent extends Agent {

  async reliableTask(payload, queueItem) {

    try {

      await this.doSomethingRisky(payload);

    } catch (error) {

      console.error(`Task ${queueItem.id} failed:`, error);


      // Optionally re-queue with retry logic

      if (payload.retryCount < 3) {

        await this.queue("reliableTask", {

          ...payload,

          retryCount: (payload.retryCount || 0) + 1,

        });

      }

    }

  }

}


```

TypeScript

```

class RobustAgent extends Agent {

  async reliableTask(payload: any, queueItem: QueueItem) {

    try {

      await this.doSomethingRisky(payload);

    } catch (error) {

      console.error(`Task ${queueItem.id} failed:`, error);


      // Optionally re-queue with retry logic

      if (payload.retryCount < 3) {

        await this.queue("reliableTask", {

          ...payload,

          retryCount: (payload.retryCount || 0) + 1,

        });

      }

    }

  }

}


```

## Best practices

1. **Keep payloads small**: Payloads are JSON-serialized and stored in the database.
2. **Idempotent operations**: Design callback methods to be safe to retry.
3. **Error handling**: Include proper error handling in callback methods.
4. **Monitoring**: Use logging to track queue processing.
5. **Cleanup**: Regularly clean up completed or failed tasks if needed.

## Integration with other features

The queue system works with other Agent SDK features:

* **State management**: Access agent state within queued callbacks.
* **Scheduling**: Combine with [schedule()](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) for time-based queue processing.
* **Context**: Queued tasks maintain the original request context.
* **Database**: Uses the same database as other agent data.

## Limitations

* Tasks are processed sequentially, not in parallel.
* No priority system (FIFO only).
* Queue processing happens during agent execution, not as separate background jobs.

Note

Queue tasks support built-in retries with exponential backoff. Pass `{ retry: { maxAttempts, baseDelayMs, maxDelayMs } }` as the third argument to `queue()`. Refer to [Retries](https://developers.cloudflare.com/agents/api-reference/retries/) for details.

## Queue vs Schedule

Use **queue** when you want tasks to execute as soon as possible in order. Use [**schedule**](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) when you need tasks to run at specific times or on a recurring basis.

| Feature          | Queue                    | Schedule                    |
| ---------------- | ------------------------ | --------------------------- |
| Execution timing | Immediate (FIFO)         | Specific time or cron       |
| Use case         | Background processing    | Delayed or recurring tasks  |
| Storage          | cf\_agents\_queues table | cf\_agents\_schedules table |

## Next steps

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Time-based execution with cron and delays. 

[ Run Workflows ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Durable multi-step background processing. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/queue-tasks/","name":"Queue tasks"}}]}
```

---

---
title: Retrieval Augmented Generation
description: Agents can use Retrieval Augmented Generation (RAG) to retrieve relevant information and use it augment calls to AI models. Store a user's chat history to use as context for future conversations, summarize documents to bootstrap an Agent's knowledge base, and/or use data from your Agent's web browsing tasks to enhance your Agent's capabilities.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/rag.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Retrieval Augmented Generation

Agents can use Retrieval Augmented Generation (RAG) to retrieve relevant information and use it augment [calls to AI models](https://developers.cloudflare.com/agents/api-reference/using-ai-models/). Store a user's chat history to use as context for future conversations, summarize documents to bootstrap an Agent's knowledge base, and/or use data from your Agent's [web browsing](https://developers.cloudflare.com/agents/api-reference/browse-the-web/) tasks to enhance your Agent's capabilities.

You can use the Agent's own [SQL database](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state) as the source of truth for your data and store embeddings in [Vectorize](https://developers.cloudflare.com/vectorize/) (or any other vector-enabled database) to allow your Agent to retrieve relevant information.

### Vector search

Note

If you're brand-new to vector databases and Vectorize, visit the [Vectorize tutorial](https://developers.cloudflare.com/vectorize/get-started/intro/) to learn the basics, including how to create an index, insert data, and generate embeddings.

You can query a vector index (or indexes) from any method on your Agent: any Vectorize index you attach is available on `this.env` within your Agent. If you've [associated metadata](https://developers.cloudflare.com/vectorize/best-practices/insert-vectors/#metadata) with your vectors that maps back to data stored in your Agent, you can then look up the data directly within your Agent using `this.sql`.

Here's an example of how to give an Agent retrieval capabilities:

* [  JavaScript ](#tab-panel-2542)
* [  TypeScript ](#tab-panel-2543)

JavaScript

```

import { Agent } from "agents";


export class RAGAgent extends Agent {

  // Other methods on our Agent

  // ...

  //

  async queryKnowledge(userQuery) {

    // Turn a query into an embedding

    const queryVector = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {

      text: [userQuery],

    });


    // Retrieve results from our vector index

    let searchResults = await this.env.VECTOR_DB.query(queryVector.data[0], {

      topK: 10,

      returnMetadata: "all",

    });


    let knowledge = [];

    for (const match of searchResults.matches) {

      console.log(match.metadata);

      knowledge.push(match.metadata);

    }


    // Use the metadata to re-associate the vector search results

    // with data in our Agent's SQL database

    let results = this

      .sql`SELECT * FROM knowledge WHERE id IN (${knowledge.map((k) => k.id)})`;


    // Return them

    return results;

  }

}


```

TypeScript

```

import { Agent } from "agents";


interface Env {

  AI: Ai;

  VECTOR_DB: Vectorize;

}


export class RAGAgent extends Agent {

  // Other methods on our Agent

  // ...

  //

  async queryKnowledge(userQuery: string) {

    // Turn a query into an embedding

    const queryVector = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {

      text: [userQuery],

    });


    // Retrieve results from our vector index

    let searchResults = await this.env.VECTOR_DB.query(queryVector.data[0], {

      topK: 10,

      returnMetadata: "all",

    });


    let knowledge = [];

    for (const match of searchResults.matches) {

      console.log(match.metadata);

      knowledge.push(match.metadata);

    }


    // Use the metadata to re-associate the vector search results

    // with data in our Agent's SQL database

    let results = this

      .sql`SELECT * FROM knowledge WHERE id IN (${knowledge.map((k) => k.id)})`;


    // Return them

    return results;

  }

}


```

You'll also need to connect your Agent to your vector indexes:

* [  wrangler.jsonc ](#tab-panel-2540)
* [  wrangler.toml ](#tab-panel-2541)

```

{

  // ...

  "vectorize": [

    {

      "binding": "VECTOR_DB",

      "index_name": "your-vectorize-index-name",

    },

  ],

  // ...

}


```

```

[[vectorize]]

binding = "VECTOR_DB"

index_name = "your-vectorize-index-name"


```

If you have multiple indexes you want to make available, you can provide an array of `vectorize` bindings.

#### Next steps

* Learn more on how to [combine Vectorize and Workers AI](https://developers.cloudflare.com/vectorize/get-started/embeddings/)
* Review the [Vectorize query API](https://developers.cloudflare.com/vectorize/reference/client-api/)
* Use [metadata filtering](https://developers.cloudflare.com/vectorize/reference/metadata-filtering/) to add context to your results

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/rag/","name":"Retrieval Augmented Generation"}}]}
```

---

---
title: Readonly connections
description: Readonly connections restrict certain WebSocket clients from modifying agent state while still letting them receive state updates and call non-mutating RPC methods.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/readonly-connections.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Readonly connections

Readonly connections restrict certain WebSocket clients from modifying agent state while still letting them receive state updates and call non-mutating RPC methods.

## Overview

When a connection is marked as readonly:

* It **receives** state updates from the server
* It **can call** RPC methods that do not modify state
* It **cannot** call `this.setState()` — neither via client-side `setState()` nor via a `@callable()` method that calls `this.setState()` internally

This is useful for scenarios like:

* **View-only modes**: Users who should only observe but not modify
* **Role-based access**: Restricting state modifications based on user roles
* **Multi-tenant scenarios**: Some tenants have read-only access
* **Audit and monitoring connections**: Observers that should not affect the system

* [  JavaScript ](#tab-panel-2544)
* [  TypeScript ](#tab-panel-2545)

JavaScript

```

import { Agent } from "agents";


export class DocAgent extends Agent {

  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    return url.searchParams.get("mode") === "view";

  }

}


```

TypeScript

```

import { Agent, type Connection, type ConnectionContext } from "agents";


export class DocAgent extends Agent<Env, DocState> {

  shouldConnectionBeReadonly(connection: Connection, ctx: ConnectionContext) {

    const url = new URL(ctx.request.url);

    return url.searchParams.get("mode") === "view";

  }

}


```

* [  JavaScript ](#tab-panel-2546)
* [  TypeScript ](#tab-panel-2547)

JavaScript

```

// Client - view-only mode

const agent = useAgent({

  agent: "DocAgent",

  name: "doc-123",

  query: { mode: "view" },

  onStateUpdateError: (error) => {

    toast.error("You're in view-only mode");

  },

});


```

TypeScript

```

// Client - view-only mode

const agent = useAgent({

  agent: "DocAgent",

  name: "doc-123",

  query: { mode: "view" },

  onStateUpdateError: (error) => {

    toast.error("You're in view-only mode");

  },

});


```

## Marking connections as readonly

### On connect

Override `shouldConnectionBeReadonly` to evaluate each connection when it first connects. Return `true` to mark it readonly.

* [  JavaScript ](#tab-panel-2550)
* [  TypeScript ](#tab-panel-2551)

JavaScript

```

export class MyAgent extends Agent {

  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    const role = url.searchParams.get("role");

    return role === "viewer" || role === "guest";

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env, State> {

  shouldConnectionBeReadonly(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    const role = url.searchParams.get("role");

    return role === "viewer" || role === "guest";

  }

}


```

This hook runs before the initial state is sent to the client, so the connection is readonly from the very first message.

### At any time

Use `setConnectionReadonly` to change a connection's readonly status dynamically:

* [  JavaScript ](#tab-panel-2556)
* [  TypeScript ](#tab-panel-2557)

JavaScript

```

export class GameAgent extends Agent {

  @callable()

  async startSpectating() {

    const { connection } = getCurrentAgent();

    if (connection) {

      this.setConnectionReadonly(connection, true);

    }

  }


  @callable()

  async joinAsPlayer() {

    const { connection } = getCurrentAgent();

    if (connection) {

      this.setConnectionReadonly(connection, false);

    }

  }

}


```

TypeScript

```

export class GameAgent extends Agent<Env, GameState> {

  @callable()

  async startSpectating() {

    const { connection } = getCurrentAgent();

    if (connection) {

      this.setConnectionReadonly(connection, true);

    }

  }


  @callable()

  async joinAsPlayer() {

    const { connection } = getCurrentAgent();

    if (connection) {

      this.setConnectionReadonly(connection, false);

    }

  }

}


```

### Letting a connection toggle its own status

A connection can toggle its own readonly status via a callable. This is useful for lock/unlock UIs where viewers can opt into editing mode:

* [  JavaScript ](#tab-panel-2552)
* [  TypeScript ](#tab-panel-2553)

JavaScript

```

import { Agent, callable, getCurrentAgent } from "agents";


export class CollabAgent extends Agent {

  @callable()

  async setMyReadonly(readonly) {

    const { connection } = getCurrentAgent();

    if (connection) {

      this.setConnectionReadonly(connection, readonly);

    }

  }

}


```

TypeScript

```

import { Agent, callable, getCurrentAgent } from "agents";


export class CollabAgent extends Agent<Env, State> {

  @callable()

  async setMyReadonly(readonly: boolean) {

    const { connection } = getCurrentAgent();

    if (connection) {

      this.setConnectionReadonly(connection, readonly);

    }

  }

}


```

On the client:

* [  JavaScript ](#tab-panel-2548)
* [  TypeScript ](#tab-panel-2549)

JavaScript

```

// Toggle between readonly and writable

await agent.call("setMyReadonly", [true]); // lock

await agent.call("setMyReadonly", [false]); // unlock


```

TypeScript

```

// Toggle between readonly and writable

await agent.call("setMyReadonly", [true]); // lock

await agent.call("setMyReadonly", [false]); // unlock


```

### Checking status

Use `isConnectionReadonly` to check a connection's current status:

* [  JavaScript ](#tab-panel-2554)
* [  TypeScript ](#tab-panel-2555)

JavaScript

```

export class MyAgent extends Agent {

  @callable()

  async getPermissions() {

    const { connection } = getCurrentAgent();

    if (connection) {

      return { canEdit: !this.isConnectionReadonly(connection) };

    }

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env, State> {

  @callable()

  async getPermissions() {

    const { connection } = getCurrentAgent();

    if (connection) {

      return { canEdit: !this.isConnectionReadonly(connection) };

    }

  }

}


```

## Handling errors on the client

Errors surface in two ways depending on how the write was attempted:

* **Client-side `setState()`** — the server sends a `cf_agent_state_error` message. Handle it with the `onStateUpdateError` callback.
* **`@callable()` methods** — the RPC call rejects with an error. Handle it with a `try`/`catch` around `agent.call()`.

Note

`onStateUpdateError` also fires when `validateStateChange` rejects a client-originated state update (with the message `"State update rejected"`). This makes the callback useful for handling any rejected state write, not just readonly errors.

* [  JavaScript ](#tab-panel-2558)
* [  TypeScript ](#tab-panel-2559)

JavaScript

```

const agent = useAgent({

  agent: "MyAgent",

  name: "instance",

  // Fires when client-side setState() is blocked

  onStateUpdateError: (error) => {

    setError(error);

  },

});


// Fires when a callable that writes state is blocked

try {

  await agent.call("updateSettings", [newSettings]);

} catch (e) {

  setError(e instanceof Error ? e.message : String(e)); // "Connection is readonly"

}


```

TypeScript

```

const agent = useAgent({

  agent: "MyAgent",

  name: "instance",

  // Fires when client-side setState() is blocked

  onStateUpdateError: (error) => {

    setError(error);

  },

});


// Fires when a callable that writes state is blocked

try {

  await agent.call("updateSettings", [newSettings]);

} catch (e) {

  setError(e instanceof Error ? e.message : String(e)); // "Connection is readonly"

}


```

To avoid showing errors in the first place, check permissions before rendering edit controls:

```

function Editor() {

  const [canEdit, setCanEdit] = useState(false);

  const agent = useAgent({ agent: "MyAgent", name: "instance" });


  useEffect(() => {

    agent.call("getPermissions").then((p) => setCanEdit(p.canEdit));

  }, []);


  return <button disabled={!canEdit}>{canEdit ? "Edit" : "View Only"}</button>;

}


```

## API reference

### `shouldConnectionBeReadonly`

An overridable hook that determines if a connection should be marked as readonly when it connects.

| Parameter   | Type              | Description                  |
| ----------- | ----------------- | ---------------------------- |
| connection  | Connection        | The connecting client        |
| ctx         | ConnectionContext | Contains the upgrade request |
| **Returns** | boolean           | true to mark as readonly     |

Default: returns `false` (all connections are writable).

### `setConnectionReadonly`

Mark or unmark a connection as readonly. Can be called at any time.

| Parameter  | Type       | Description                           |
| ---------- | ---------- | ------------------------------------- |
| connection | Connection | The connection to update              |
| readonly   | boolean    | true to make readonly (default: true) |

### `isConnectionReadonly`

Check if a connection is currently readonly.

| Parameter   | Type       | Description             |
| ----------- | ---------- | ----------------------- |
| connection  | Connection | The connection to check |
| **Returns** | boolean    | true if readonly        |

### `onStateUpdateError` (client)

Callback on `AgentClient` and `useAgent` options. Called when the server rejects a state update.

| Parameter | Type   | Description                   |
| --------- | ------ | ----------------------------- |
| error     | string | Error message from the server |

## Examples

### Query parameter based access

* [  JavaScript ](#tab-panel-2562)
* [  TypeScript ](#tab-panel-2563)

JavaScript

```

export class DocumentAgent extends Agent {

  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    const mode = url.searchParams.get("mode");

    return mode === "view";

  }

}


// Client connects with readonly mode

const agent = useAgent({

  agent: "DocumentAgent",

  name: "doc-123",

  query: { mode: "view" },

  onStateUpdateError: (error) => {

    toast.error("Document is in view-only mode");

  },

});


```

TypeScript

```

export class DocumentAgent extends Agent<Env, DocumentState> {

  shouldConnectionBeReadonly(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    const mode = url.searchParams.get("mode");

    return mode === "view";

  }

}


// Client connects with readonly mode

const agent = useAgent({

  agent: "DocumentAgent",

  name: "doc-123",

  query: { mode: "view" },

  onStateUpdateError: (error) => {

    toast.error("Document is in view-only mode");

  },

});


```

### Role-based access control

* [  JavaScript ](#tab-panel-2572)
* [  TypeScript ](#tab-panel-2573)

JavaScript

```

export class CollaborativeAgent extends Agent {

  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    const role = url.searchParams.get("role");

    return role === "viewer" || role === "guest";

  }


  onConnect(connection, ctx) {

    const url = new URL(ctx.request.url);

    const userId = url.searchParams.get("userId");


    console.log(

      `User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})`,

    );

  }


  @callable()

  async upgradeToEditor() {

    const { connection } = getCurrentAgent();

    if (!connection) return;


    // Check permissions (pseudo-code)

    const canUpgrade = await checkUserPermissions();

    if (canUpgrade) {

      this.setConnectionReadonly(connection, false);

      return { success: true };

    }


    throw new Error("Insufficient permissions");

  }

}


```

TypeScript

```

export class CollaborativeAgent extends Agent<Env, CollabState> {

  shouldConnectionBeReadonly(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    const role = url.searchParams.get("role");

    return role === "viewer" || role === "guest";

  }


  onConnect(connection: Connection, ctx: ConnectionContext) {

    const url = new URL(ctx.request.url);

    const userId = url.searchParams.get("userId");


    console.log(

      `User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})`,

    );

  }


  @callable()

  async upgradeToEditor() {

    const { connection } = getCurrentAgent();

    if (!connection) return;


    // Check permissions (pseudo-code)

    const canUpgrade = await checkUserPermissions();

    if (canUpgrade) {

      this.setConnectionReadonly(connection, false);

      return { success: true };

    }


    throw new Error("Insufficient permissions");

  }

}


```

### Admin dashboard

* [  JavaScript ](#tab-panel-2574)
* [  TypeScript ](#tab-panel-2575)

JavaScript

```

export class MonitoringAgent extends Agent {

  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    // Only admins can modify state

    return url.searchParams.get("admin") !== "true";

  }


  onStateChanged(state, source) {

    if (source !== "server") {

      // Log who modified the state

      console.log(`State modified by connection ${source.id}`);

    }

  }

}


// Admin client (can modify)

const adminAgent = useAgent({

  agent: "MonitoringAgent",

  name: "system",

  query: { admin: "true" },

});


// Viewer client (readonly)

const viewerAgent = useAgent({

  agent: "MonitoringAgent",

  name: "system",

  query: { admin: "false" },

  onStateUpdateError: (error) => {

    console.log("Viewer cannot modify state");

  },

});


```

TypeScript

```

export class MonitoringAgent extends Agent<Env, SystemState> {

  shouldConnectionBeReadonly(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    // Only admins can modify state

    return url.searchParams.get("admin") !== "true";

  }


  onStateChanged(state: SystemState, source: Connection | "server") {

    if (source !== "server") {

      // Log who modified the state

      console.log(`State modified by connection ${source.id}`);

    }

  }

}


// Admin client (can modify)

const adminAgent = useAgent({

  agent: "MonitoringAgent",

  name: "system",

  query: { admin: "true" },

});


// Viewer client (readonly)

const viewerAgent = useAgent({

  agent: "MonitoringAgent",

  name: "system",

  query: { admin: "false" },

  onStateUpdateError: (error) => {

    console.log("Viewer cannot modify state");

  },

});


```

### Dynamic permission changes

* [  JavaScript ](#tab-panel-2576)
* [  TypeScript ](#tab-panel-2577)

JavaScript

```

export class GameAgent extends Agent {

  @callable()

  async startSpectatorMode() {

    const { connection } = getCurrentAgent();

    if (!connection) return;


    this.setConnectionReadonly(connection, true);

    return { mode: "spectator" };

  }


  @callable()

  async joinAsPlayer() {

    const { connection } = getCurrentAgent();

    if (!connection) return;


    const canJoin = this.state.players.length < 4;

    if (canJoin) {

      this.setConnectionReadonly(connection, false);

      return { mode: "player" };

    }


    throw new Error("Game is full");

  }


  @callable()

  async getMyPermissions() {

    const { connection } = getCurrentAgent();

    if (!connection) return null;


    return {

      canEdit: !this.isConnectionReadonly(connection),

      connectionId: connection.id,

    };

  }

}


```

TypeScript

```

export class GameAgent extends Agent<Env, GameState> {

  @callable()

  async startSpectatorMode() {

    const { connection } = getCurrentAgent();

    if (!connection) return;


    this.setConnectionReadonly(connection, true);

    return { mode: "spectator" };

  }


  @callable()

  async joinAsPlayer() {

    const { connection } = getCurrentAgent();

    if (!connection) return;


    const canJoin = this.state.players.length < 4;

    if (canJoin) {

      this.setConnectionReadonly(connection, false);

      return { mode: "player" };

    }


    throw new Error("Game is full");

  }


  @callable()

  async getMyPermissions() {

    const { connection } = getCurrentAgent();

    if (!connection) return null;


    return {

      canEdit: !this.isConnectionReadonly(connection),

      connectionId: connection.id,

    };

  }

}


```

Client-side React component:

```

function GameComponent() {

  const [canEdit, setCanEdit] = useState(false);


  const agent = useAgent({

    agent: "GameAgent",

    name: "game-123",

    onStateUpdateError: (error) => {

      toast.error("Cannot modify game state in spectator mode");

    },

  });


  useEffect(() => {

    agent.call("getMyPermissions").then((perms) => {

      setCanEdit(perms?.canEdit ?? false);

    });

  }, [agent]);


  return (

    <div>

      <button onClick={() => agent.call("joinAsPlayer")} disabled={canEdit}>

        Join as Player

      </button>


      <button

        onClick={() => agent.call("startSpectatorMode")}

        disabled={!canEdit}

      >

        Switch to Spectator

      </button>


      <div>{canEdit ? "You can modify the game" : "You are spectating"}</div>

    </div>

  );

}


```

## How it works

Readonly status is stored in the connection's WebSocket attachment, which persists through the WebSocket Hibernation API. The flag is namespaced internally so it cannot be accidentally overwritten by `connection.setState()`. The same mechanism is used by [protocol message control](https://developers.cloudflare.com/agents/api-reference/protocol-messages/) — both flag coexist safely in the attachment. This means:

* **Survives hibernation** — the flag is serialized and restored when the agent wakes up
* **No cleanup needed** — connection state is automatically discarded when the connection closes
* **Zero overhead** — no database tables or queries, just the connection's built-in attachment
* **Safe from user code** — `connection.state` and `connection.setState()` never expose or overwrite the readonly flag

When a readonly connection tries to modify state, the server blocks it — regardless of whether the write comes from client-side `setState()` or from a `@callable()` method:

```

Client (readonly)                     Agent

       │                                │

       │  setState({ count: 1 })        │

       │ ─────────────────────────────▶ │  Check readonly → blocked

       │  ◀───────────────────────────  │

       │  cf_agent_state_error          │

       │                                │

       │  call("increment")             │

       │ ─────────────────────────────▶ │  increment() calls this.setState()

       │                                │  Check readonly → throw

       │  ◀───────────────────────────  │

       │  RPC error: "Connection is     │

       │              readonly"         │

       │                                │

       │  call("getPermissions")        │

       │ ─────────────────────────────▶ │  getPermissions() — no setState()

       │  ◀───────────────────────────  │

       │  RPC result: { canEdit: false }│


```

### What readonly does and does not restrict

| Action                                             | Allowed? |
| -------------------------------------------------- | -------- |
| Receive state broadcasts                           | Yes      |
| Call @callable() methods that do not write state   | Yes      |
| Call @callable() methods that call this.setState() | **No**   |
| Send state updates via client-side setState()      | **No**   |

The enforcement happens inside `setState()` itself. When a `@callable()` method tries to call `this.setState()` and the current connection context is readonly, the framework throws an `Error("Connection is readonly")`. This means you do not need manual permission checks in your RPC methods — any callable that writes state is automatically blocked for readonly connections.

## Caveats

### Side effects in callables still run

The readonly check happens inside `this.setState()`, not at the start of the callable. If your method has side effects before the state write, those will still execute:

* [  JavaScript ](#tab-panel-2560)
* [  TypeScript ](#tab-panel-2561)

JavaScript

```

export class MyAgent extends Agent {

  @callable()

  async processOrder(orderId) {

    await sendConfirmationEmail(orderId); // runs even for readonly connections

    await chargePayment(orderId); // runs too

    this.setState({ ...this.state, orders: [...this.state.orders, orderId] }); // throws

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env, State> {

  @callable()

  async processOrder(orderId: string) {

    await sendConfirmationEmail(orderId); // runs even for readonly connections

    await chargePayment(orderId); // runs too

    this.setState({ ...this.state, orders: [...this.state.orders, orderId] }); // throws

  }

}


```

To avoid this, either check permissions before side effects or structure your code so the state write comes first:

* [  JavaScript ](#tab-panel-2564)
* [  TypeScript ](#tab-panel-2565)

JavaScript

```

export class MyAgent extends Agent {

  @callable()

  async processOrder(orderId) {

    // Write state first — throws immediately for readonly connections

    this.setState({ ...this.state, orders: [...this.state.orders, orderId] });

    // Side effects only run if setState succeeded

    await sendConfirmationEmail(orderId);

    await chargePayment(orderId);

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env, State> {

  @callable()

  async processOrder(orderId: string) {

    // Write state first — throws immediately for readonly connections

    this.setState({ ...this.state, orders: [...this.state.orders, orderId] });

    // Side effects only run if setState succeeded

    await sendConfirmationEmail(orderId);

    await chargePayment(orderId);

  }

}


```

## Best practices

### Combine with authentication

* [  JavaScript ](#tab-panel-2568)
* [  TypeScript ](#tab-panel-2569)

JavaScript

```

export class SecureAgent extends Agent {

  shouldConnectionBeReadonly(connection, ctx) {

    const url = new URL(ctx.request.url);

    const token = url.searchParams.get("token");


    // Verify token and get permissions

    const permissions = this.verifyToken(token);

    return !permissions.canWrite;

  }

}


```

TypeScript

```

export class SecureAgent extends Agent<Env, State> {

  shouldConnectionBeReadonly(

    connection: Connection,

    ctx: ConnectionContext,

  ): boolean {

    const url = new URL(ctx.request.url);

    const token = url.searchParams.get("token");


    // Verify token and get permissions

    const permissions = this.verifyToken(token);

    return !permissions.canWrite;

  }

}


```

### Provide clear user feedback

* [  JavaScript ](#tab-panel-2566)
* [  TypeScript ](#tab-panel-2567)

JavaScript

```

const agent = useAgent({

  agent: "MyAgent",

  name: "instance",

  onStateUpdateError: (error) => {

    // User-friendly messages

    if (error.includes("readonly")) {

      showToast("You are in view-only mode. Upgrade to edit.");

    }

  },

});


```

TypeScript

```

const agent = useAgent({

  agent: "MyAgent",

  name: "instance",

  onStateUpdateError: (error) => {

    // User-friendly messages

    if (error.includes("readonly")) {

      showToast("You are in view-only mode. Upgrade to edit.");

    }

  },

});


```

### Check permissions before UI actions

```

function EditButton() {

  const [canEdit, setCanEdit] = useState(false);

  const agent = useAgent({

    /* ... */

  });


  useEffect(() => {

    agent.call("checkPermissions").then((perms) => {

      setCanEdit(perms.canEdit);

    });

  }, []);


  return <button disabled={!canEdit}>{canEdit ? "Edit" : "View Only"}</button>;

}


```

### Log access attempts

* [  JavaScript ](#tab-panel-2570)
* [  TypeScript ](#tab-panel-2571)

JavaScript

```

export class AuditedAgent extends Agent {

  onStateChanged(state, source) {

    if (source !== "server") {

      this.audit({

        action: "state_update",

        connectionId: source.id,

        readonly: this.isConnectionReadonly(source),

        timestamp: Date.now(),

      });

    }

  }

}


```

TypeScript

```

export class AuditedAgent extends Agent<Env, State> {

  onStateChanged(state: State, source: Connection | "server") {

    if (source !== "server") {

      this.audit({

        action: "state_update",

        connectionId: source.id,

        readonly: this.isConnectionReadonly(source),

        timestamp: Date.now(),

      });

    }

  }

}


```

## Limitations

* Readonly status only applies to state updates using `setState()`
* RPC methods can still be called (implement your own checks if needed)
* Readonly is a per-connection flag, not tied to user identity

## Related resources

* [Store and sync state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/)
* [Protocol messages](https://developers.cloudflare.com/agents/api-reference/protocol-messages/) — suppress JSON protocol frames for binary-only clients (can be combined with readonly)
* [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/)
* [Callable methods](https://developers.cloudflare.com/agents/api-reference/callable-methods/)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/readonly-connections/","name":"Readonly connections"}}]}
```

---

---
title: Retries
description: Retry failed operations with exponential backoff and jitter. The Agents SDK provides built-in retry support for scheduled tasks, queued tasks, and a general-purpose this.retry() method for your own code.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/retries.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Retries

Retry failed operations with exponential backoff and jitter. The Agents SDK provides built-in retry support for scheduled tasks, queued tasks, and a general-purpose `this.retry()` method for your own code.

## Overview

Transient failures are common when calling external APIs, interacting with other services, or running background tasks. The retry system handles these automatically:

* **Exponential backoff** — each retry waits longer than the last
* **Jitter** — randomized delays prevent thundering herd problems
* **Configurable** — tune attempts, delays, and caps per call site
* **Built-in** — schedule, queue, and workflow operations retry automatically

## Quick start

Use `this.retry()` to retry any async operation:

* [  JavaScript ](#tab-panel-2582)
* [  TypeScript ](#tab-panel-2583)

JavaScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async fetchWithRetry(url) {

    const response = await this.retry(async () => {

      const res = await fetch(url);

      if (!res.ok) throw new Error(`HTTP ${res.status}`);

      return res.json();

    });


    return response;

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async fetchWithRetry(url: string) {

    const response = await this.retry(async () => {

      const res = await fetch(url);

      if (!res.ok) throw new Error(`HTTP ${res.status}`);

      return res.json();

    });


    return response;

  }

}


```

By default, `this.retry()` retries up to three times with jittered exponential backoff.

## `this.retry()`

The `retry()` method is available on every `Agent` instance. It retries the provided function on any thrown error by default.

TypeScript

```

async retry<T>(

  fn: (attempt: number) => Promise<T>,

  options?: RetryOptions & {

    shouldRetry?: (err: unknown, nextAttempt: number) => boolean;

  }

): Promise<T>


```

**Parameters:**

* `fn` — the async function to retry. Receives the current attempt number (1-indexed).
* `options` — optional retry configuration (refer to [RetryOptions](#retryoptions) below). Options are validated eagerly — invalid values throw immediately.
* `options.shouldRetry` — optional predicate called with the thrown error and the next attempt number. Return `false` to stop retrying immediately. If not provided, all errors are retried.

**Returns:** the result of `fn` on success.

**Throws:** the last error if all attempts fail or `shouldRetry` returns `false`.

### Examples

**Basic retry:**

* [  JavaScript ](#tab-panel-2578)
* [  TypeScript ](#tab-panel-2579)

JavaScript

```

const data = await this.retry(() => fetch("https://api.example.com/data"));


```

TypeScript

```

const data = await this.retry(() => fetch("https://api.example.com/data"));


```

**Custom retry options:**

* [  JavaScript ](#tab-panel-2584)
* [  TypeScript ](#tab-panel-2585)

JavaScript

```

const data = await this.retry(

  async () => {

    const res = await fetch("https://slow-api.example.com/data");

    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    return res.json();

  },

  {

    maxAttempts: 5,

    baseDelayMs: 500,

    maxDelayMs: 10000,

  },

);


```

TypeScript

```

const data = await this.retry(

  async () => {

    const res = await fetch("https://slow-api.example.com/data");

    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    return res.json();

  },

  {

    maxAttempts: 5,

    baseDelayMs: 500,

    maxDelayMs: 10000,

  },

);


```

**Using the attempt number:**

* [  JavaScript ](#tab-panel-2580)
* [  TypeScript ](#tab-panel-2581)

JavaScript

```

const result = await this.retry(async (attempt) => {

  console.log(`Attempt ${attempt}...`);

  return await this.callExternalService();

});


```

TypeScript

```

const result = await this.retry(async (attempt) => {

  console.log(`Attempt ${attempt}...`);

  return await this.callExternalService();

});


```

**Selective retry with `shouldRetry`:**

Use `shouldRetry` to stop retrying on specific errors. The predicate receives both the error and the next attempt number:

* [  JavaScript ](#tab-panel-2590)
* [  TypeScript ](#tab-panel-2591)

JavaScript

```

const data = await this.retry(

  async () => {

    const res = await fetch("https://api.example.com/data");

    if (!res.ok) throw new HttpError(res.status, await res.text());

    return res.json();

  },

  {

    maxAttempts: 5,

    shouldRetry: (err, nextAttempt) => {

      // Do not retry 4xx client errors — our request is wrong

      if (err instanceof HttpError && err.status >= 400 && err.status < 500) {

        return false;

      }

      return true; // retry everything else (5xx, network errors, etc.)

    },

  },

);


```

TypeScript

```

const data = await this.retry(

  async () => {

    const res = await fetch("https://api.example.com/data");

    if (!res.ok) throw new HttpError(res.status, await res.text());

    return res.json();

  },

  {

    maxAttempts: 5,

    shouldRetry: (err, nextAttempt) => {

      // Do not retry 4xx client errors — our request is wrong

      if (err instanceof HttpError && err.status >= 400 && err.status < 500) {

        return false;

      }

      return true; // retry everything else (5xx, network errors, etc.)

    },

  },

);


```

## Retries in schedules

Pass retry options when creating a schedule:

* [  JavaScript ](#tab-panel-2606)
* [  TypeScript ](#tab-panel-2607)

JavaScript

```

// Retry up to 5 times if the callback fails

await this.schedule(

  "processTask",

  60,

  { taskId: "123" },

  {

    retry: { maxAttempts: 5 },

  },

);


// Retry with custom backoff

await this.schedule(

  new Date("2026-03-01T09:00:00Z"),

  "sendReport",

  {},

  {

    retry: {

      maxAttempts: 3,

      baseDelayMs: 1000,

      maxDelayMs: 30000,

    },

  },

);


// Cron with retries

await this.schedule(

  "0 8 * * *",

  "dailyDigest",

  {},

  {

    retry: { maxAttempts: 3 },

  },

);


// Interval with retries

await this.scheduleEvery(

  30,

  "poll",

  { source: "api" },

  {

    retry: { maxAttempts: 5, baseDelayMs: 200 },

  },

);


```

TypeScript

```

// Retry up to 5 times if the callback fails

await this.schedule(

  "processTask",

  60,

  { taskId: "123" },

  {

    retry: { maxAttempts: 5 },

  },

);


// Retry with custom backoff

await this.schedule(

  new Date("2026-03-01T09:00:00Z"),

  "sendReport",

  {},

  {

    retry: {

      maxAttempts: 3,

      baseDelayMs: 1000,

      maxDelayMs: 30000,

    },

  },

);


// Cron with retries

await this.schedule(

  "0 8 * * *",

  "dailyDigest",

  {},

  {

    retry: { maxAttempts: 3 },

  },

);


// Interval with retries

await this.scheduleEvery(

  30,

  "poll",

  { source: "api" },

  {

    retry: { maxAttempts: 5, baseDelayMs: 200 },

  },

);


```

If the callback throws, it is retried according to the retry options. If all attempts fail, the error is logged and routed through `onError()`. The schedule is still removed (for one-time schedules) or rescheduled (for cron/interval) regardless of success or failure.

## Retries in queues

Pass retry options when adding a task to the queue:

* [  JavaScript ](#tab-panel-2594)
* [  TypeScript ](#tab-panel-2595)

JavaScript

```

await this.queue(

  "sendEmail",

  { to: "user@example.com" },

  {

    retry: { maxAttempts: 5 },

  },

);


await this.queue("processWebhook", webhookData, {

  retry: {

    maxAttempts: 3,

    baseDelayMs: 500,

    maxDelayMs: 5000,

  },

});


```

TypeScript

```

await this.queue(

  "sendEmail",

  { to: "user@example.com" },

  {

    retry: { maxAttempts: 5 },

  },

);


await this.queue("processWebhook", webhookData, {

  retry: {

    maxAttempts: 3,

    baseDelayMs: 500,

    maxDelayMs: 5000,

  },

});


```

If the callback throws, it is retried before the task is dequeued. After all attempts are exhausted, the task is dequeued and the error is logged.

## Validation

Retry options are validated eagerly when you call `this.retry()`, `queue()`, `schedule()`, or `scheduleEvery()`. Invalid options throw immediately instead of failing later at execution time:

* [  JavaScript ](#tab-panel-2600)
* [  TypeScript ](#tab-panel-2601)

JavaScript

```

// Throws immediately: "retry.maxAttempts must be >= 1"

await this.queue("sendEmail", data, {

  retry: { maxAttempts: 0 },

});


// Throws immediately: "retry.baseDelayMs must be > 0"

await this.schedule(

  60,

  "process",

  {},

  {

    retry: { baseDelayMs: -100 },

  },

);


// Throws immediately: "retry.maxAttempts must be an integer"

await this.retry(() => fetch(url), { maxAttempts: 2.5 });


// Throws immediately: "retry.baseDelayMs must be <= retry.maxDelayMs"

// because baseDelayMs: 5000 exceeds the default maxDelayMs: 3000

await this.queue("sendEmail", data, {

  retry: { baseDelayMs: 5000 },

});


```

TypeScript

```

// Throws immediately: "retry.maxAttempts must be >= 1"

await this.queue("sendEmail", data, {

  retry: { maxAttempts: 0 },

});


// Throws immediately: "retry.baseDelayMs must be > 0"

await this.schedule(

  60,

  "process",

  {},

  {

    retry: { baseDelayMs: -100 },

  },

);


// Throws immediately: "retry.maxAttempts must be an integer"

await this.retry(() => fetch(url), { maxAttempts: 2.5 });


// Throws immediately: "retry.baseDelayMs must be <= retry.maxDelayMs"

// because baseDelayMs: 5000 exceeds the default maxDelayMs: 3000

await this.queue("sendEmail", data, {

  retry: { baseDelayMs: 5000 },

});


```

Validation resolves partial options against class-level or built-in defaults before checking cross-field constraints. This means `{ baseDelayMs: 5000 }` is caught immediately when the resolved `maxDelayMs` is 3000, rather than failing later at execution time.

## Default behavior

Even without explicit retry options, scheduled and queued callbacks are retried with sensible defaults:

| Setting     | Default |
| ----------- | ------- |
| maxAttempts | 3       |
| baseDelayMs | 100     |
| maxDelayMs  | 3000    |

These defaults apply to `this.retry()`, `queue()`, `schedule()`, and `scheduleEvery()`. Per-call-site options override them.

### Class-level defaults

Override the defaults for your entire agent via `static options`:

* [  JavaScript ](#tab-panel-2586)
* [  TypeScript ](#tab-panel-2587)

JavaScript

```

class MyAgent extends Agent {

  static options = {

    retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 5000 },

  };

}


```

TypeScript

```

class MyAgent extends Agent {

  static options = {

    retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 5000 },

  };

}


```

You only need to specify the fields you want to change — unset fields fall back to the built-in defaults:

* [  JavaScript ](#tab-panel-2588)
* [  TypeScript ](#tab-panel-2589)

JavaScript

```

class MyAgent extends Agent {

  // Only override maxAttempts; baseDelayMs (100) and maxDelayMs (3000) stay default

  static options = {

    retry: { maxAttempts: 10 },

  };

}


```

TypeScript

```

class MyAgent extends Agent {

  // Only override maxAttempts; baseDelayMs (100) and maxDelayMs (3000) stay default

  static options = {

    retry: { maxAttempts: 10 },

  };

}


```

Class-level defaults are used as fallbacks when a call site does not specify retry options. Per-call-site options always take priority:

* [  JavaScript ](#tab-panel-2592)
* [  TypeScript ](#tab-panel-2593)

JavaScript

```

// Uses class-level defaults (10 attempts)

await this.retry(() => fetch(url));


// Overrides to 2 attempts for this specific call

await this.retry(() => fetch(url), { maxAttempts: 2 });


```

TypeScript

```

// Uses class-level defaults (10 attempts)

await this.retry(() => fetch(url));


// Overrides to 2 attempts for this specific call

await this.retry(() => fetch(url), { maxAttempts: 2 });


```

To disable retries for a specific task, set `maxAttempts: 1`:

* [  JavaScript ](#tab-panel-2598)
* [  TypeScript ](#tab-panel-2599)

JavaScript

```

await this.schedule(

  60,

  "oneShot",

  {},

  {

    retry: { maxAttempts: 1 },

  },

);


```

TypeScript

```

await this.schedule(

  60,

  "oneShot",

  {},

  {

    retry: { maxAttempts: 1 },

  },

);


```

## RetryOptions

TypeScript

```

interface RetryOptions {

  /** Maximum number of attempts (including the first). Must be an integer >= 1. Default: 3 */

  maxAttempts?: number;

  /** Base delay in milliseconds for exponential backoff. Must be > 0 and <= maxDelayMs. Default: 100 */

  baseDelayMs?: number;

  /** Maximum delay cap in milliseconds. Must be > 0. Default: 3000 */

  maxDelayMs?: number;

}


```

The delay between retries uses **full jitter exponential backoff**:

```

delay = random(0, min(2^attempt * baseDelayMs, maxDelayMs))


```

This means early retries are fast (often under 200ms), and later retries back off to avoid overwhelming a failing service. The randomization (jitter) prevents multiple agents from retrying at the exact same moment.

## How it works

### Backoff strategy

The retry system uses the "Full Jitter" strategy from the [AWS Architecture Blog ↗](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/). Given 3 attempts with default settings:

| Attempt | Upper Bound                   | Actual Delay     |
| ------- | ----------------------------- | ---------------- |
| 1       | min(2^1 \* 100, 3000) = 200ms | random(0, 200ms) |
| 2       | min(2^2 \* 100, 3000) = 400ms | random(0, 400ms) |
| 3       | (no retry — final attempt)    | —                |

With `maxAttempts: 5` and `baseDelayMs: 500`:

| Attempt | Upper Bound                   | Actual Delay      |
| ------- | ----------------------------- | ----------------- |
| 1       | min(2 \* 500, 3000) = 1000ms  | random(0, 1000ms) |
| 2       | min(4 \* 500, 3000) = 2000ms  | random(0, 2000ms) |
| 3       | min(8 \* 500, 3000) = 3000ms  | random(0, 3000ms) |
| 4       | min(16 \* 500, 3000) = 3000ms | random(0, 3000ms) |
| 5       | (no retry — final attempt)    | —                 |

### MCP server retries

When adding an MCP server, you can configure retry options for connection and reconnection attempts:

* [  JavaScript ](#tab-panel-2596)
* [  TypeScript ](#tab-panel-2597)

JavaScript

```

await this.addMcpServer("github", "https://mcp.github.com", {

  retry: { maxAttempts: 5, baseDelayMs: 1000, maxDelayMs: 10000 },

});


```

TypeScript

```

await this.addMcpServer("github", "https://mcp.github.com", {

  retry: { maxAttempts: 5, baseDelayMs: 1000, maxDelayMs: 10000 },

});


```

These options are persisted and used when:

* Restoring server connections after hibernation
* Establishing connections after OAuth completion

Default: 3 attempts, 500ms base delay, 5s max delay.

## Patterns

### Retry with logging

* [  JavaScript ](#tab-panel-2604)
* [  TypeScript ](#tab-panel-2605)

JavaScript

```

class MyAgent extends Agent {

  async resilientTask(payload) {

    try {

      const result = await this.retry(

        async (attempt) => {

          if (attempt > 1) {

            console.log(`Retrying ${payload.url} (attempt ${attempt})...`);

          }

          const res = await fetch(payload.url);

          if (!res.ok) throw new Error(`HTTP ${res.status}`);

          return res.json();

        },

        { maxAttempts: 5 },

      );

      console.log("Success:", result);

    } catch (e) {

      console.error("All retries failed:", e);

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async resilientTask(payload: { url: string }) {

    try {

      const result = await this.retry(

        async (attempt) => {

          if (attempt > 1) {

            console.log(`Retrying ${payload.url} (attempt ${attempt})...`);

          }

          const res = await fetch(payload.url);

          if (!res.ok) throw new Error(`HTTP ${res.status}`);

          return res.json();

        },

        { maxAttempts: 5 },

      );

      console.log("Success:", result);

    } catch (e) {

      console.error("All retries failed:", e);

    }

  }

}


```

### Retry with fallback

* [  JavaScript ](#tab-panel-2602)
* [  TypeScript ](#tab-panel-2603)

JavaScript

```

class MyAgent extends Agent {

  async fetchData() {

    try {

      return await this.retry(

        () => fetch("https://primary-api.example.com/data"),

        { maxAttempts: 3, baseDelayMs: 200 },

      );

    } catch {

      // Primary failed, try fallback

      return await this.retry(

        () => fetch("https://fallback-api.example.com/data"),

        { maxAttempts: 2 },

      );

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async fetchData() {

    try {

      return await this.retry(

        () => fetch("https://primary-api.example.com/data"),

        { maxAttempts: 3, baseDelayMs: 200 },

      );

    } catch {

      // Primary failed, try fallback

      return await this.retry(

        () => fetch("https://fallback-api.example.com/data"),

        { maxAttempts: 2 },

      );

    }

  }

}


```

### Combining retries with scheduling

For operations that might take a long time to recover (minutes or hours), combine `this.retry()` for immediate retries with `this.schedule()` for delayed retries:

* [  JavaScript ](#tab-panel-2608)
* [  TypeScript ](#tab-panel-2609)

JavaScript

```

class MyAgent extends Agent {

  async syncData(payload) {

    const attempt = payload.attempt ?? 1;


    try {

      // Immediate retries for transient failures (seconds)

      await this.retry(() => this.fetchAndProcess(payload.source), {

        maxAttempts: 3,

        baseDelayMs: 1000,

      });

    } catch (e) {

      if (attempt >= 5) {

        console.error("Giving up after 5 scheduled attempts");

        return;

      }


      // Schedule a retry in 5 minutes for longer outages

      const delaySeconds = 300 * attempt;

      await this.schedule(delaySeconds, "syncData", {

        source: payload.source,

        attempt: attempt + 1,

      });

      console.log(`Scheduled retry ${attempt + 1} in ${delaySeconds}s`);

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async syncData(payload: { source: string; attempt?: number }) {

    const attempt = payload.attempt ?? 1;


    try {

      // Immediate retries for transient failures (seconds)

      await this.retry(() => this.fetchAndProcess(payload.source), {

        maxAttempts: 3,

        baseDelayMs: 1000,

      });

    } catch (e) {

      if (attempt >= 5) {

        console.error("Giving up after 5 scheduled attempts");

        return;

      }


      // Schedule a retry in 5 minutes for longer outages

      const delaySeconds = 300 * attempt;

      await this.schedule(delaySeconds, "syncData", {

        source: payload.source,

        attempt: attempt + 1,

      });

      console.log(`Scheduled retry ${attempt + 1} in ${delaySeconds}s`);

    }

  }

}


```

## Limitations

* **No dead-letter queue.** If a queued or scheduled task fails all retry attempts, it is removed. Implement your own persistence if you need to track failed tasks.
* **Retry delays block the agent.** During the backoff delay, the Durable Object is awake but idle. For short delays (under 3 seconds) this is fine. For longer recovery times, use `this.schedule()` instead.
* **Queue retries are head-of-line blocking.** Queue items are processed sequentially. If one item is being retried with long delays, it blocks all subsequent items. If you need independent retry behavior, use `this.retry()` inside the callback rather than per-task retry options on `queue()`.
* **No circuit breaker.** The retry system does not track failure rates across calls. If a service is persistently down, each task will exhaust its retry budget independently.
* **`shouldRetry` is only available on `this.retry()`.** The `shouldRetry` predicate cannot be used with `schedule()` or `queue()` because functions cannot be serialized to the database. For scheduled/queued tasks, handle non-retryable errors inside the callback itself.

## Next steps

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Schedule tasks for future execution. 

[ Queue tasks ](https://developers.cloudflare.com/agents/api-reference/queue-tasks/) Background task queue for immediate processing. 

[ Run Workflows ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Durable multi-step processing with automatic retries. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/retries/","name":"Retries"}}]}
```

---

---
title: Routing
description: This guide explains how requests are routed to agents, how naming works, and patterns for organizing your agents.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/routing.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Routing

This guide explains how requests are routed to agents, how naming works, and patterns for organizing your agents.

## How routing works

When a request comes in, `routeAgentRequest()` examines the URL and routes it to the appropriate agent instance:

```

https://your-worker.dev/agents/{agent-name}/{instance-name}

                               └────┬────┘   └─────┬─────┘

                               Class name     Unique instance ID

                              (kebab-case)


```

**Example URLs:**

| URL                      | Agent Class | Instance |
| ------------------------ | ----------- | -------- |
| /agents/counter/user-123 | Counter     | user-123 |
| /agents/chat-room/lobby  | ChatRoom    | lobby    |
| /agents/my-agent/default | MyAgent     | default  |

## Name resolution

Agent class names are automatically converted to kebab-case for URLs:

| Class Name  | URL Path                 |
| ----------- | ------------------------ |
| Counter     | /agents/counter/...      |
| MyAgent     | /agents/my-agent/...     |
| ChatRoom    | /agents/chat-room/...    |
| AIAssistant | /agents/ai-assistant/... |

The router matches both the original name and kebab-case version, so you can use either:

* `useAgent({ agent: "Counter" })` → `/agents/counter/...`
* `useAgent({ agent: "counter" })` → `/agents/counter/...`

## Using routeAgentRequest()

The `routeAgentRequest()` function is the main entry point for agent routing:

* [  JavaScript ](#tab-panel-2618)
* [  TypeScript ](#tab-panel-2619)

JavaScript

```

import { routeAgentRequest } from "agents";


export default {

  async fetch(request, env, ctx) {

    // Route to agents - returns Response or undefined

    const agentResponse = await routeAgentRequest(request, env);


    if (agentResponse) {

      return agentResponse;

    }


    // No agent matched - handle other routes

    return new Response("Not found", { status: 404 });

  },

};


```

TypeScript

```

import { routeAgentRequest } from "agents";


export default {

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    // Route to agents - returns Response or undefined

    const agentResponse = await routeAgentRequest(request, env);


    if (agentResponse) {

      return agentResponse;

    }


    // No agent matched - handle other routes

    return new Response("Not found", { status: 404 });

  },

} satisfies ExportedHandler<Env>;


```

## Instance naming patterns

The instance name (the last part of the URL) determines which agent instance handles the request. Each unique name gets its own isolated agent with its own state.

### Per-user agents

Each user gets their own agent instance:

* [  JavaScript ](#tab-panel-2612)
* [  TypeScript ](#tab-panel-2613)

JavaScript

```

// Client

const agent = useAgent({

  agent: "UserProfile",

  name: `user-${userId}`, // e.g., "user-abc123"

});


```

TypeScript

```

// Client

const agent = useAgent({

  agent: "UserProfile",

  name: `user-${userId}`, // e.g., "user-abc123"

});


```

```

/agents/user-profile/user-abc123 → User abc123's agent

/agents/user-profile/user-xyz789 → User xyz789's agent (separate instance)


```

### Shared rooms

Multiple users share the same agent instance:

* [  JavaScript ](#tab-panel-2614)
* [  TypeScript ](#tab-panel-2615)

JavaScript

```

// Client

const agent = useAgent({

  agent: "ChatRoom",

  name: roomId, // e.g., "general" or "room-42"

});


```

TypeScript

```

// Client

const agent = useAgent({

  agent: "ChatRoom",

  name: roomId, // e.g., "general" or "room-42"

});


```

```

/agents/chat-room/general → All users in "general" share this agent


```

### Global singleton

A single instance for the entire application:

* [  JavaScript ](#tab-panel-2616)
* [  TypeScript ](#tab-panel-2617)

JavaScript

```

// Client

const agent = useAgent({

  agent: "AppConfig",

  name: "default", // Or any consistent name

});


```

TypeScript

```

// Client

const agent = useAgent({

  agent: "AppConfig",

  name: "default", // Or any consistent name

});


```

### Dynamic naming

Generate instance names based on context:

* [  JavaScript ](#tab-panel-2622)
* [  TypeScript ](#tab-panel-2623)

JavaScript

```

// Per-session

const agent = useAgent({

  agent: "Session",

  name: sessionId,

});


// Per-document

const agent = useAgent({

  agent: "Document",

  name: `doc-${documentId}`,

});


// Per-game

const agent = useAgent({

  agent: "Game",

  name: `game-${gameId}-${Date.now()}`,

});


```

TypeScript

```

// Per-session

const agent = useAgent({

  agent: "Session",

  name: sessionId,

});


// Per-document

const agent = useAgent({

  agent: "Document",

  name: `doc-${documentId}`,

});


// Per-game

const agent = useAgent({

  agent: "Game",

  name: `game-${gameId}-${Date.now()}`,

});


```

## Custom URL routing

For advanced use cases where you need control over the URL structure, you can bypass the default `/agents/{agent}/{name}` pattern.

### Using basePath (client-side)

The `basePath` option lets clients connect to any URL path:

* [  JavaScript ](#tab-panel-2620)
* [  TypeScript ](#tab-panel-2621)

JavaScript

```

// Client connects to /user instead of /agents/user-agent/...

const agent = useAgent({

  agent: "UserAgent", // Required but ignored when basePath is set

  basePath: "user", // → connects to /user

});


```

TypeScript

```

// Client connects to /user instead of /agents/user-agent/...

const agent = useAgent({

  agent: "UserAgent", // Required but ignored when basePath is set

  basePath: "user", // → connects to /user

});


```

This is useful when:

* You want clean URLs without the `/agents/` prefix
* The instance name is determined server-side (for example, from auth/session)
* You are integrating with an existing URL structure

### Server-side instance selection

When using `basePath`, the server must handle routing. Use `getAgentByName()` to get the agent instance, then forward the request with `fetch()`:

* [  JavaScript ](#tab-panel-2632)
* [  TypeScript ](#tab-panel-2633)

JavaScript

```

export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    // Custom routing - server determines instance from session

    if (url.pathname.startsWith("/user/")) {

      const session = await getSession(request);

      const agent = await getAgentByName(env.UserAgent, session.userId);

      return agent.fetch(request); // Forward request directly to agent

    }


    // Default routing for standard /agents/... paths

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

export default {

  async fetch(request: Request, env: Env) {

    const url = new URL(request.url);


    // Custom routing - server determines instance from session

    if (url.pathname.startsWith("/user/")) {

      const session = await getSession(request);

      const agent = await getAgentByName(env.UserAgent, session.userId);

      return agent.fetch(request); // Forward request directly to agent

    }


    // Default routing for standard /agents/... paths

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

### Custom path with dynamic instance

Route different paths to different instances:

* [  JavaScript ](#tab-panel-2626)
* [  TypeScript ](#tab-panel-2627)

JavaScript

```

// Route /chat/{room} to ChatRoom agent

if (url.pathname.startsWith("/chat/")) {

  const roomId = url.pathname.replace("/chat/", "");

  const agent = await getAgentByName(env.ChatRoom, roomId);

  return agent.fetch(request);

}


// Route /doc/{id} to Document agent

if (url.pathname.startsWith("/doc/")) {

  const docId = url.pathname.replace("/doc/", "");

  const agent = await getAgentByName(env.Document, docId);

  return agent.fetch(request);

}


```

TypeScript

```

// Route /chat/{room} to ChatRoom agent

if (url.pathname.startsWith("/chat/")) {

  const roomId = url.pathname.replace("/chat/", "");

  const agent = await getAgentByName(env.ChatRoom, roomId);

  return agent.fetch(request);

}


// Route /doc/{id} to Document agent

if (url.pathname.startsWith("/doc/")) {

  const docId = url.pathname.replace("/doc/", "");

  const agent = await getAgentByName(env.Document, docId);

  return agent.fetch(request);

}


```

### Receiving the instance identity (client-side)

When using `basePath`, the client does not know which instance it connected to until the server returns this information. The agent automatically sends its identity on connection:

* [  JavaScript ](#tab-panel-2634)
* [  TypeScript ](#tab-panel-2635)

JavaScript

```

const agent = useAgent({

  agent: "UserAgent",

  basePath: "user",

  onIdentity: (name, agentType) => {

    console.log(`Connected to ${agentType} instance: ${name}`);

    // e.g., "Connected to user-agent instance: user-123"

  },

});


// Reactive state - re-renders when identity is received

return (

  <div>

    {agent.identified ? `Connected to: ${agent.name}` : "Connecting..."}

  </div>

);


```

TypeScript

```

const agent = useAgent({

  agent: "UserAgent",

  basePath: "user",

  onIdentity: (name, agentType) => {

    console.log(`Connected to ${agentType} instance: ${name}`);

    // e.g., "Connected to user-agent instance: user-123"

  },

});


// Reactive state - re-renders when identity is received

return (

  <div>

    {agent.identified ? `Connected to: ${agent.name}` : "Connecting..."}

  </div>

);


```

For `AgentClient`:

* [  JavaScript ](#tab-panel-2636)
* [  TypeScript ](#tab-panel-2637)

JavaScript

```

const agent = new AgentClient({

  agent: "UserAgent",

  basePath: "user",

  host: "example.com",

  onIdentity: (name, agentType) => {

    // Update UI with actual instance name

    setInstanceName(name);

  },

});


// Wait for identity before proceeding

await agent.ready;

console.log(agent.name); // Now has the server-determined name


```

TypeScript

```

const agent = new AgentClient({

  agent: "UserAgent",

  basePath: "user",

  host: "example.com",

  onIdentity: (name, agentType) => {

    // Update UI with actual instance name

    setInstanceName(name);

  },

});


// Wait for identity before proceeding

await agent.ready;

console.log(agent.name); // Now has the server-determined name


```

### Handling identity changes on reconnect

If the identity changes on reconnect (for example, session expired and user logs in as someone else), you can handle it with `onIdentityChange`:

* [  JavaScript ](#tab-panel-2630)
* [  TypeScript ](#tab-panel-2631)

JavaScript

```

const agent = useAgent({

  agent: "UserAgent",

  basePath: "user",

  onIdentityChange: (oldName, newName, oldAgent, newAgent) => {

    console.log(`Session changed: ${oldName} → ${newName}`);

    // Refresh state, show notification, etc.

  },

});


```

TypeScript

```

const agent = useAgent({

  agent: "UserAgent",

  basePath: "user",

  onIdentityChange: (oldName, newName, oldAgent, newAgent) => {

    console.log(`Session changed: ${oldName} → ${newName}`);

    // Refresh state, show notification, etc.

  },

});


```

If `onIdentityChange` is not provided and identity changes, a warning is logged to help catch unexpected session changes.

### Disabling identity for security

If your instance names contain sensitive data (session IDs, internal user IDs), you can disable identity sending:

* [  JavaScript ](#tab-panel-2624)
* [  TypeScript ](#tab-panel-2625)

JavaScript

```

class SecureAgent extends Agent {

  // Do not expose instance names to clients

  static options = { sendIdentityOnConnect: false };

}


```

TypeScript

```

class SecureAgent extends Agent {

  // Do not expose instance names to clients

  static options = { sendIdentityOnConnect: false };

}


```

When identity is disabled:

* `agent.identified` stays `false`
* `agent.ready` never resolves (use state updates instead)
* `onIdentity` and `onIdentityChange` are never called

### When to use custom routing

| Scenario                        | Approach                            |
| ------------------------------- | ----------------------------------- |
| Standard agent access           | Default /agents/{agent}/{name}      |
| Instance from auth/session      | basePath \+ getAgentByName \+ fetch |
| Clean URLs (no /agents/ prefix) | basePath \+ custom routing          |
| Legacy URL structure            | basePath \+ custom routing          |
| Complex routing logic           | Custom routing in Worker            |

## Routing options

Both `routeAgentRequest()` and `getAgentByName()` accept options for customizing routing behavior.

### CORS

For cross-origin requests (common when your frontend is on a different domain):

* [  JavaScript ](#tab-panel-2628)
* [  TypeScript ](#tab-panel-2629)

JavaScript

```

const response = await routeAgentRequest(request, env, {

  cors: true, // Enable default CORS headers

});


```

TypeScript

```

const response = await routeAgentRequest(request, env, {

  cors: true, // Enable default CORS headers

});


```

Or with custom CORS headers:

* [  JavaScript ](#tab-panel-2638)
* [  TypeScript ](#tab-panel-2639)

JavaScript

```

const response = await routeAgentRequest(request, env, {

  cors: {

    "Access-Control-Allow-Origin": "https://myapp.com",

    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",

    "Access-Control-Allow-Headers": "Content-Type, Authorization",

  },

});


```

TypeScript

```

const response = await routeAgentRequest(request, env, {

  cors: {

    "Access-Control-Allow-Origin": "https://myapp.com",

    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",

    "Access-Control-Allow-Headers": "Content-Type, Authorization",

  },

});


```

### Location hints

For latency-sensitive applications, hint where the agent should run:

* [  JavaScript ](#tab-panel-2640)
* [  TypeScript ](#tab-panel-2641)

JavaScript

```

// With getAgentByName

const agent = await getAgentByName(env.MyAgent, "instance-name", {

  locationHint: "enam", // Eastern North America

});


// With routeAgentRequest (applies to all matched agents)

const response = await routeAgentRequest(request, env, {

  locationHint: "enam",

});


```

TypeScript

```

// With getAgentByName

const agent = await getAgentByName(env.MyAgent, "instance-name", {

  locationHint: "enam", // Eastern North America

});


// With routeAgentRequest (applies to all matched agents)

const response = await routeAgentRequest(request, env, {

  locationHint: "enam",

});


```

Available location hints: `wnam`, `enam`, `sam`, `weur`, `eeur`, `apac`, `oc`, `afr`, `me`

### Jurisdiction

For data residency requirements:

* [  JavaScript ](#tab-panel-2644)
* [  TypeScript ](#tab-panel-2645)

JavaScript

```

// With getAgentByName

const agent = await getAgentByName(env.MyAgent, "instance-name", {

  jurisdiction: "eu", // EU jurisdiction

});


// With routeAgentRequest (applies to all matched agents)

const response = await routeAgentRequest(request, env, {

  jurisdiction: "eu",

});


```

TypeScript

```

// With getAgentByName

const agent = await getAgentByName(env.MyAgent, "instance-name", {

  jurisdiction: "eu", // EU jurisdiction

});


// With routeAgentRequest (applies to all matched agents)

const response = await routeAgentRequest(request, env, {

  jurisdiction: "eu",

});


```

### Props

Since agents are instantiated by the runtime rather than constructed directly, `props` provides a way to pass initialization arguments:

* [  JavaScript ](#tab-panel-2642)
* [  TypeScript ](#tab-panel-2643)

JavaScript

```

const agent = await getAgentByName(env.MyAgent, "instance-name", {

  props: {

    userId: session.userId,

    config: { maxRetries: 3 },

  },

});


```

TypeScript

```

const agent = await getAgentByName(env.MyAgent, "instance-name", {

  props: {

    userId: session.userId,

    config: { maxRetries: 3 },

  },

});


```

Props are passed to the agent's `onStart` lifecycle method:

* [  JavaScript ](#tab-panel-2646)
* [  TypeScript ](#tab-panel-2647)

JavaScript

```

class MyAgent extends Agent {

  userId;

  config;


  async onStart(props) {

    this.userId = props?.userId;

    this.config = props?.config;

  }

}


```

TypeScript

```

class MyAgent extends Agent<Env, State> {

  private userId?: string;

  private config?: { maxRetries: number };


  async onStart(props?: { userId: string; config: { maxRetries: number } }) {

    this.userId = props?.userId;

    this.config = props?.config;

  }

}


```

When using `props` with `routeAgentRequest`, the same props are passed to whichever agent matches the URL. This works well for universal context like authentication:

* [  JavaScript ](#tab-panel-2648)
* [  TypeScript ](#tab-panel-2649)

JavaScript

```

export default {

  async fetch(request, env) {

    const session = await getSession(request);

    return routeAgentRequest(request, env, {

      props: { userId: session.userId, role: session.role },

    });

  },

};


```

TypeScript

```

export default {

  async fetch(request, env) {

    const session = await getSession(request);

    return routeAgentRequest(request, env, {

      props: { userId: session.userId, role: session.role },

    });

  },

} satisfies ExportedHandler<Env>;


```

For agent-specific initialization, use `getAgentByName` instead where you control exactly which agent receives the props.

Note

For `McpAgent`, props are automatically stored and accessible via `this.props`. Refer to [MCP servers](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) for details.

### Hooks

`routeAgentRequest` supports hooks for intercepting requests before they reach agents:

* [  JavaScript ](#tab-panel-2650)
* [  TypeScript ](#tab-panel-2651)

JavaScript

```

const response = await routeAgentRequest(request, env, {

  onBeforeConnect: (req, lobby) => {

    // Called before WebSocket connections

    // Return a Response to reject, Request to modify, or void to continue

  },

  onBeforeRequest: (req, lobby) => {

    // Called before HTTP requests

    // Return a Response to reject, Request to modify, or void to continue

  },

});


```

TypeScript

```

const response = await routeAgentRequest(request, env, {

  onBeforeConnect: (req, lobby) => {

    // Called before WebSocket connections

    // Return a Response to reject, Request to modify, or void to continue

  },

  onBeforeRequest: (req, lobby) => {

    // Called before HTTP requests

    // Return a Response to reject, Request to modify, or void to continue

  },

});


```

These hooks are useful for authentication and validation. Refer to [Cross-domain authentication](https://developers.cloudflare.com/agents/guides/cross-domain-authentication/) for detailed examples.

## Server-side agent access

You can access agents from your Worker code using `getAgentByName()` for RPC calls:

* [  JavaScript ](#tab-panel-2656)
* [  TypeScript ](#tab-panel-2657)

JavaScript

```

import { getAgentByName, routeAgentRequest } from "agents";


export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    // API endpoint that interacts with an agent

    if (url.pathname === "/api/increment") {

      const counter = await getAgentByName(env.Counter, "global-counter");

      const newCount = await counter.increment();

      return Response.json({ count: newCount });

    }


    // Regular agent routing

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { getAgentByName, routeAgentRequest } from "agents";


export default {

  async fetch(request: Request, env: Env) {

    const url = new URL(request.url);


    // API endpoint that interacts with an agent

    if (url.pathname === "/api/increment") {

      const counter = await getAgentByName(env.Counter, "global-counter");

      const newCount = await counter.increment();

      return Response.json({ count: newCount });

    }


    // Regular agent routing

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

For options like `locationHint`, `jurisdiction`, and `props`, refer to [Routing options](#routing-options).

## Sub-paths and HTTP methods

Requests can include sub-paths after the instance name. These are passed to your agent's `onRequest()` handler:

```

/agents/api/v1/users     → agent: "api", instance: "v1", path: "/users"

/agents/api/v1/users/123 → agent: "api", instance: "v1", path: "/users/123"


```

Handle sub-paths in your agent:

* [  JavaScript ](#tab-panel-2658)
* [  TypeScript ](#tab-panel-2659)

JavaScript

```

export class API extends Agent {

  async onRequest(request) {

    const url = new URL(request.url);


    // url.pathname contains the full path including /agents/api/v1/...

    // Extract the sub-path after your agent's base path

    const path = url.pathname.replace(/^\/agents\/api\/[^/]+/, "");


    if (request.method === "GET" && path === "/users") {

      return Response.json(await this.getUsers());

    }


    if (request.method === "POST" && path === "/users") {

      const data = await request.json();

      return Response.json(await this.createUser(data));

    }


    return new Response("Not found", { status: 404 });

  }

}


```

TypeScript

```

export class API extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const url = new URL(request.url);


    // url.pathname contains the full path including /agents/api/v1/...

    // Extract the sub-path after your agent's base path

    const path = url.pathname.replace(/^\/agents\/api\/[^/]+/, "");


    if (request.method === "GET" && path === "/users") {

      return Response.json(await this.getUsers());

    }


    if (request.method === "POST" && path === "/users") {

      const data = await request.json();

      return Response.json(await this.createUser(data));

    }


    return new Response("Not found", { status: 404 });

  }

}


```

## Multiple agents

You can have multiple agent classes in one project. Each gets its own namespace:

* [  JavaScript ](#tab-panel-2654)
* [  TypeScript ](#tab-panel-2655)

JavaScript

```

// server.ts

export { Counter } from "./agents/counter";

export { ChatRoom } from "./agents/chat-room";

export { UserProfile } from "./agents/user-profile";


export default {

  async fetch(request, env) {

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

// server.ts

export { Counter } from "./agents/counter";

export { ChatRoom } from "./agents/chat-room";

export { UserProfile } from "./agents/user-profile";


export default {

  async fetch(request: Request, env: Env) {

    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

* [  wrangler.jsonc ](#tab-panel-2610)
* [  wrangler.toml ](#tab-panel-2611)

```

{

  "durable_objects": {

    "bindings": [

      { "name": "Counter", "class_name": "Counter" },

      { "name": "ChatRoom", "class_name": "ChatRoom" },

      { "name": "UserProfile", "class_name": "UserProfile" },

    ],

  },

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["Counter", "ChatRoom", "UserProfile"],

    },

  ],

}


```

```

[[durable_objects.bindings]]

name = "Counter"

class_name = "Counter"


[[durable_objects.bindings]]

name = "ChatRoom"

class_name = "ChatRoom"


[[durable_objects.bindings]]

name = "UserProfile"

class_name = "UserProfile"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "Counter", "ChatRoom", "UserProfile" ]


```

Each agent is accessed via its own path:

```

/agents/counter/...

/agents/chat-room/...

/agents/user-profile/...


```

## Request flow

Here is how a request flows through the system:

flowchart TD
    A["HTTP Request<br/>or WebSocket"] --> B["routeAgentRequest<br/>Parse URL path"]
    B --> C["Find binding in<br/>env by name"]
    C --> D["Get/create DO<br/>by instance ID"]
    D --> E["Agent Instance"]
    E --> F{"Protocol?"}
    F -->|WebSocket| G["onConnect(), onMessage"]
    F -->|HTTP| H["onRequest()"]

## Routing with authentication

There are several ways to authenticate requests before they reach your agent.

### Using authentication hooks

The `routeAgentRequest()` function provides `onBeforeConnect` and `onBeforeRequest` hooks for authentication:

* [  JavaScript ](#tab-panel-2664)
* [  TypeScript ](#tab-panel-2665)

JavaScript

```

import { Agent, routeAgentRequest } from "agents";


export default {

  async fetch(request, env) {

    return (

      (await routeAgentRequest(request, env, {

        // Run before WebSocket connections

        onBeforeConnect: async (request) => {

          const token = new URL(request.url).searchParams.get("token");

          if (!(await verifyToken(token, env))) {

            // Return a response to reject the connection

            return new Response("Unauthorized", { status: 401 });

          }

          // Return nothing to allow the connection

        },

        // Run before HTTP requests

        onBeforeRequest: async (request) => {

          const auth = request.headers.get("Authorization");

          if (!auth || !(await verifyAuth(auth, env))) {

            return new Response("Unauthorized", { status: 401 });

          }

        },

        // Optional: prepend a prefix to agent instance names

        prefix: "user-",

      })) ?? new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { Agent, routeAgentRequest } from "agents";


export default {

  async fetch(request: Request, env: Env) {

    return (

      (await routeAgentRequest(request, env, {

        // Run before WebSocket connections

        onBeforeConnect: async (request) => {

          const token = new URL(request.url).searchParams.get("token");

          if (!(await verifyToken(token, env))) {

            // Return a response to reject the connection

            return new Response("Unauthorized", { status: 401 });

          }

          // Return nothing to allow the connection

        },

        // Run before HTTP requests

        onBeforeRequest: async (request) => {

          const auth = request.headers.get("Authorization");

          if (!auth || !(await verifyAuth(auth, env))) {

            return new Response("Unauthorized", { status: 401 });

          }

        },

        // Optional: prepend a prefix to agent instance names

        prefix: "user-",

      })) ?? new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

### Manual authentication

Check authentication before calling `routeAgentRequest()`:

* [  JavaScript ](#tab-panel-2660)
* [  TypeScript ](#tab-panel-2661)

JavaScript

```

export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    // Protect agent routes

    if (url.pathname.startsWith("/agents/")) {

      const user = await authenticate(request, env);

      if (!user) {

        return new Response("Unauthorized", { status: 401 });

      }


      // Optionally, enforce that users can only access their own agents

      const instanceName = url.pathname.split("/")[3];

      if (instanceName !== `user-${user.id}`) {

        return new Response("Forbidden", { status: 403 });

      }

    }


    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

export default {

  async fetch(request: Request, env: Env) {

    const url = new URL(request.url);


    // Protect agent routes

    if (url.pathname.startsWith("/agents/")) {

      const user = await authenticate(request, env);

      if (!user) {

        return new Response("Unauthorized", { status: 401 });

      }


      // Optionally, enforce that users can only access their own agents

      const instanceName = url.pathname.split("/")[3];

      if (instanceName !== `user-${user.id}`) {

        return new Response("Forbidden", { status: 403 });

      }

    }


    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

### Using a framework (Hono)

If you are using a framework like [Hono ↗](https://hono.dev/), authenticate in middleware before calling the agent:

* [  JavaScript ](#tab-panel-2662)
* [  TypeScript ](#tab-panel-2663)

JavaScript

```

import { Agent, getAgentByName } from "agents";

import { Hono } from "hono";


const app = new Hono();


// Authentication middleware

app.use("/agents/*", async (c, next) => {

  const token = c.req.header("Authorization")?.replace("Bearer ", "");

  if (!token || !(await verifyToken(token, c.env))) {

    return c.json({ error: "Unauthorized" }, 401);

  }

  await next();

});


// Route to a specific agent

app.all("/agents/code-review/:id/*", async (c) => {

  const id = c.req.param("id");

  const agent = await getAgentByName(c.env.CodeReviewAgent, id);

  return agent.fetch(c.req.raw);

});


export default app;


```

TypeScript

```

import { Agent, getAgentByName } from "agents";

import { Hono } from "hono";


const app = new Hono<{ Bindings: Env }>();


// Authentication middleware

app.use("/agents/*", async (c, next) => {

  const token = c.req.header("Authorization")?.replace("Bearer ", "");

  if (!token || !(await verifyToken(token, c.env))) {

    return c.json({ error: "Unauthorized" }, 401);

  }

  await next();

});


// Route to a specific agent

app.all("/agents/code-review/:id/*", async (c) => {

  const id = c.req.param("id");

  const agent = await getAgentByName(c.env.CodeReviewAgent, id);

  return agent.fetch(c.req.raw);

});


export default app;


```

For WebSocket authentication patterns (tokens in URLs, JWT refresh), refer to [Cross-domain authentication](https://developers.cloudflare.com/agents/guides/cross-domain-authentication/).

## Troubleshooting

### Agent namespace not found

The error message lists available agents. Check:

1. Agent class is exported from your entry point.
2. Class name in code matches `class_name` in `wrangler.jsonc`.
3. URL uses correct kebab-case name.

### Request returns 404

1. Verify the URL pattern: `/agents/{agent-name}/{instance-name}`.
2. Check that `routeAgentRequest()` is called before your 404 handler.
3. Ensure the response from `routeAgentRequest()` is returned (not just called).

### WebSocket connection fails

1. Do not modify the response from `routeAgentRequest()` for WebSocket upgrades.
2. Ensure CORS is enabled if connecting from a different origin.
3. Check browser dev tools for the actual error.

### `basePath` not working

1. Ensure your Worker handles the custom path and forwards to the agent.
2. Use `getAgentByName()` \+ `agent.fetch(request)` to forward requests.
3. The `agent` parameter is still required but ignored when `basePath` is set.
4. Check that the server-side route matches the client's `basePath`.

## API reference

### `routeAgentRequest(request, env, options?)`

Routes a request to the appropriate agent.

| Parameter               | Type                    | Description                                     |
| ----------------------- | ----------------------- | ----------------------------------------------- |
| request                 | Request                 | The incoming request                            |
| env                     | Env                     | Environment with agent bindings                 |
| options.cors            | boolean \| HeadersInit  | Enable CORS headers                             |
| options.props           | Record<string, unknown> | Props passed to whichever agent handles request |
| options.locationHint    | string                  | Preferred location for agent instances          |
| options.jurisdiction    | string                  | Data jurisdiction for agent instances           |
| options.onBeforeConnect | Function                | Callback before WebSocket connections           |
| options.onBeforeRequest | Function                | Callback before HTTP requests                   |

**Returns:** `Promise<Response | undefined>` \- Response if matched, undefined if no agent route.

### `getAgentByName(namespace, name, options?)`

Get an agent instance by name for server-side RPC or request forwarding.

| Parameter            | Type                      | Description                           |
| -------------------- | ------------------------- | ------------------------------------- |
| namespace            | DurableObjectNamespace<T> | Agent binding from env                |
| name                 | string                    | Instance name                         |
| options.locationHint | string                    | Preferred location                    |
| options.jurisdiction | string                    | Data jurisdiction                     |
| options.props        | Record<string, unknown>   | Initialization properties for onStart |

**Returns:** `Promise<DurableObjectStub<T>>` \- Typed stub for calling agent methods or forwarding requests.

### `useAgent(options)` / `AgentClient` options

Client connection options for custom routing:

| Option           | Type                                           | Description                                          |
| ---------------- | ---------------------------------------------- | ---------------------------------------------------- |
| agent            | string                                         | Agent class name (required)                          |
| name             | string                                         | Instance name (default: "default")                   |
| basePath         | string                                         | Full URL path - bypasses agent/name URL construction |
| path             | string                                         | Additional path to append to the URL                 |
| onIdentity       | (name, agent) => void                          | Called when server sends identity                    |
| onIdentityChange | (oldName, newName, oldAgent, newAgent) => void | Called when identity changes on reconnect            |

**Return value properties (React hook):**

| Property   | Type          | Description                                   |
| ---------- | ------------- | --------------------------------------------- |
| name       | string        | Current instance name (reactive)              |
| agent      | string        | Current agent class name (reactive)           |
| identified | boolean       | Whether identity has been received (reactive) |
| ready      | Promise<void> | Resolves when identity is received            |

### `Agent.options` (server)

Static options for agent configuration:

| Option                     | Type    | Default | Description                                          |
| -------------------------- | ------- | ------- | ---------------------------------------------------- |
| hibernate                  | boolean | true    | Whether the agent should hibernate when inactive     |
| sendIdentityOnConnect      | boolean | true    | Whether to send identity to clients on connect       |
| hungScheduleTimeoutSeconds | number  | 30      | Timeout before a running schedule is considered hung |

* [  JavaScript ](#tab-panel-2652)
* [  TypeScript ](#tab-panel-2653)

JavaScript

```

class SecureAgent extends Agent {

  static options = { sendIdentityOnConnect: false };

}


```

TypeScript

```

class SecureAgent extends Agent {

  static options = { sendIdentityOnConnect: false };

}


```

## Next steps

[ Client SDK ](https://developers.cloudflare.com/agents/api-reference/client-sdk/) Connect from browsers with useAgent and AgentClient. 

[ Cross-domain authentication ](https://developers.cloudflare.com/agents/guides/cross-domain-authentication/) WebSocket authentication patterns. 

[ Callable methods ](https://developers.cloudflare.com/agents/api-reference/callable-methods/) RPC from clients over WebSocket. 

[ Configuration ](https://developers.cloudflare.com/agents/api-reference/configuration/) Set up agent bindings in wrangler.jsonc. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/routing/","name":"Routing"}}]}
```

---

---
title: Run Workflows
description: Integrate Cloudflare Workflows with Agents for durable, multi-step background processing while Agents handle real-time communication.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/run-workflows.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Run Workflows

Integrate [Cloudflare Workflows](https://developers.cloudflare.com/workflows/) with Agents for durable, multi-step background processing while Agents handle real-time communication.

Agents vs. Workflows

Agents excel at real-time communication and state management. Workflows excel at durable execution with automatic retries, failure recovery, and waiting for external events.

Use Agents alone for chat, messaging, and quick API calls. Use Agent + Workflow for long-running tasks (over 30 seconds), multi-step pipelines, and human approval flows.

## Quick start

### 1\. Define a Workflow

Extend `AgentWorkflow` for typed access to the originating Agent:

* [  JavaScript ](#tab-panel-2694)
* [  TypeScript ](#tab-panel-2695)

JavaScript

```

import { AgentWorkflow } from "agents/workflows";

export class ProcessingWorkflow extends AgentWorkflow {

  async run(event, step) {

    const params = event.payload;


    const result = await step.do("process-data", async () => {

      return processData(params.data);

    });


    // Non-durable: progress reporting (may repeat on retry)

    await this.reportProgress({

      step: "process",

      status: "complete",

      percent: 0.5,

    });


    // Broadcast to connected WebSocket clients

    this.broadcastToClients({ type: "update", taskId: params.taskId });


    await step.do("save-results", async () => {

      // Call Agent methods via RPC

      await this.agent.saveResult(params.taskId, result);

    });


    // Durable: idempotent, won't repeat on retry

    await step.reportComplete(result);

    return result;

  }

}


```

TypeScript

```

import { AgentWorkflow } from "agents/workflows";

import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";

import type { MyAgent } from "./agent";


type TaskParams = { taskId: string; data: string };


export class ProcessingWorkflow extends AgentWorkflow<MyAgent, TaskParams> {

  async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {

    const params = event.payload;


    const result = await step.do("process-data", async () => {

      return processData(params.data);

    });


    // Non-durable: progress reporting (may repeat on retry)

    await this.reportProgress({

      step: "process",

      status: "complete",

      percent: 0.5,

    });


    // Broadcast to connected WebSocket clients

    this.broadcastToClients({ type: "update", taskId: params.taskId });


    await step.do("save-results", async () => {

      // Call Agent methods via RPC

      await this.agent.saveResult(params.taskId, result);

    });


    // Durable: idempotent, won't repeat on retry

    await step.reportComplete(result);

    return result;

  }

}


```

### 2\. Start a Workflow from an Agent

Use `runWorkflow()` to start and track workflows:

* [  JavaScript ](#tab-panel-2696)
* [  TypeScript ](#tab-panel-2697)

JavaScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async startTask(taskId, data) {

    const instanceId = await this.runWorkflow("PROCESSING_WORKFLOW", {

      taskId,

      data,

    });

    return { instanceId };

  }


  async onWorkflowProgress(workflowName, instanceId, progress) {

    this.broadcast(JSON.stringify({ type: "workflow-progress", progress }));

  }


  async onWorkflowComplete(workflowName, instanceId, result) {

    console.log(`Workflow completed:`, result);

  }


  async saveResult(taskId, result) {

    this

      .sql`INSERT INTO results (task_id, data) VALUES (${taskId}, ${JSON.stringify(result)})`;

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async startTask(taskId: string, data: string) {

    const instanceId = await this.runWorkflow("PROCESSING_WORKFLOW", {

      taskId,

      data,

    });

    return { instanceId };

  }


  async onWorkflowProgress(

    workflowName: string,

    instanceId: string,

    progress: unknown,

  ) {

    this.broadcast(JSON.stringify({ type: "workflow-progress", progress }));

  }


  async onWorkflowComplete(

    workflowName: string,

    instanceId: string,

    result?: unknown,

  ) {

    console.log(`Workflow completed:`, result);

  }


  async saveResult(taskId: string, result: unknown) {

    this

      .sql`INSERT INTO results (task_id, data) VALUES (${taskId}, ${JSON.stringify(result)})`;

  }

}


```

### 3\. Configure Wrangler

* [  wrangler.jsonc ](#tab-panel-2666)
* [  wrangler.toml ](#tab-panel-2667)

```

{

  "name": "my-app",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "durable_objects": {

    "bindings": [{ "name": "MY_AGENT", "class_name": "MyAgent" }],

  },

  "workflows": [

    {

      "name": "processing-workflow",

      "binding": "PROCESSING_WORKFLOW",

      "class_name": "ProcessingWorkflow",

    },

  ],

  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],

}


```

```

name = "my-app"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"


[[durable_objects.bindings]]

name = "MY_AGENT"

class_name = "MyAgent"


[[workflows]]

name = "processing-workflow"

binding = "PROCESSING_WORKFLOW"

class_name = "ProcessingWorkflow"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "MyAgent" ]


```

## AgentWorkflow class

Base class for Workflows that integrate with Agents.

### Type parameters

| Parameter    | Description                                               |
| ------------ | --------------------------------------------------------- |
| AgentType    | The Agent class type for typed RPC                        |
| Params       | Parameters passed to the workflow                         |
| ProgressType | Type for progress reporting (defaults to DefaultProgress) |
| Env          | Environment type (defaults to Cloudflare.Env)             |

### Properties

| Property     | Type   | Description                          |
| ------------ | ------ | ------------------------------------ |
| agent        | Stub   | Typed stub for calling Agent methods |
| instanceId   | string | The workflow instance ID             |
| workflowName | string | The workflow binding name            |
| env          | Env    | Environment bindings                 |

### Instance methods (non-durable)

These methods may repeat on retry. Use for lightweight, frequent updates.

#### reportProgress(progress)

Report progress to the Agent. Triggers `onWorkflowProgress` callback.

* [  JavaScript ](#tab-panel-2670)
* [  TypeScript ](#tab-panel-2671)

JavaScript

```

await this.reportProgress({

  step: "processing",

  status: "running",

  percent: 0.5,

});


```

TypeScript

```

await this.reportProgress({

  step: "processing",

  status: "running",

  percent: 0.5,

});


```

#### broadcastToClients(message)

Broadcast a message to all WebSocket clients connected to the Agent.

* [  JavaScript ](#tab-panel-2668)
* [  TypeScript ](#tab-panel-2669)

JavaScript

```

this.broadcastToClients({ type: "update", data: result });


```

TypeScript

```

this.broadcastToClients({ type: "update", data: result });


```

#### waitForApproval(step, options?)

Wait for an approval event. Throws `WorkflowRejectedError` if rejected.

* [  JavaScript ](#tab-panel-2672)
* [  TypeScript ](#tab-panel-2673)

JavaScript

```

const approval = await this.waitForApproval(step, {

  timeout: "7 days",

});


```

TypeScript

```

const approval = await this.waitForApproval<{ approvedBy: string }>(step, {

  timeout: "7 days",

});


```

### Step methods (durable)

These methods are idempotent and will not repeat on retry. Use for state changes that must persist.

| Method                        | Description                                    |
| ----------------------------- | ---------------------------------------------- |
| step.reportComplete(result?)  | Report successful completion                   |
| step.reportError(error)       | Report an error                                |
| step.sendEvent(event)         | Send a custom event to the Agent               |
| step.updateAgentState(state)  | Replace Agent state (broadcasts to clients)    |
| step.mergeAgentState(partial) | Merge into Agent state (broadcasts to clients) |
| step.resetAgentState()        | Reset Agent state to initialState              |

### DefaultProgress type

TypeScript

```

type DefaultProgress = {

  step?: string;

  status?: "pending" | "running" | "complete" | "error";

  message?: string;

  percent?: number;

  [key: string]: unknown;

};


```

## Agent workflow methods

Methods available on the `Agent` class for Workflow management.

### runWorkflow(workflowName, params, options?)

Start a workflow instance and track it in the Agent database.

**Parameters:**

| Parameter            | Type   | Description                                           |
| -------------------- | ------ | ----------------------------------------------------- |
| workflowName         | string | Workflow binding name from env                        |
| params               | object | Parameters to pass to the workflow                    |
| options.id           | string | Custom workflow ID (auto-generated if not provided)   |
| options.metadata     | object | Metadata stored for querying (not passed to workflow) |
| options.agentBinding | string | Agent binding name (auto-detected if not provided)    |

**Returns:** `Promise<string>` \- Workflow instance ID

* [  JavaScript ](#tab-panel-2678)
* [  TypeScript ](#tab-panel-2679)

JavaScript

```

const instanceId = await this.runWorkflow(

  "MY_WORKFLOW",

  { taskId: "123" },

  {

    metadata: { userId: "user-456", priority: "high" },

  },

);


```

TypeScript

```

const instanceId = await this.runWorkflow(

  "MY_WORKFLOW",

  { taskId: "123" },

  {

    metadata: { userId: "user-456", priority: "high" },

  },

);


```

### sendWorkflowEvent(workflowName, instanceId, event)

Send an event to a running workflow.

* [  JavaScript ](#tab-panel-2674)
* [  TypeScript ](#tab-panel-2675)

JavaScript

```

await this.sendWorkflowEvent("MY_WORKFLOW", instanceId, {

  type: "custom-event",

  payload: { action: "proceed" },

});


```

TypeScript

```

await this.sendWorkflowEvent("MY_WORKFLOW", instanceId, {

  type: "custom-event",

  payload: { action: "proceed" },

});


```

### getWorkflowStatus(workflowName, instanceId)

Get the status of a workflow and update the tracking record.

* [  JavaScript ](#tab-panel-2676)
* [  TypeScript ](#tab-panel-2677)

JavaScript

```

const status = await this.getWorkflowStatus("MY_WORKFLOW", instanceId);

// { status: 'running', output: null, error: null }


```

TypeScript

```

const status = await this.getWorkflowStatus("MY_WORKFLOW", instanceId);

// { status: 'running', output: null, error: null }


```

### getWorkflow(instanceId)

Get a tracked workflow by ID.

* [  JavaScript ](#tab-panel-2680)
* [  TypeScript ](#tab-panel-2681)

JavaScript

```

const workflow = this.getWorkflow(instanceId);

// { instanceId, workflowName, status, metadata, error, createdAt, ... }


```

TypeScript

```

const workflow = this.getWorkflow(instanceId);

// { instanceId, workflowName, status, metadata, error, createdAt, ... }


```

### getWorkflows(criteria?)

Query tracked workflows with cursor-based pagination. Returns a `WorkflowPage` with workflows, total count, and cursor for the next page.

* [  JavaScript ](#tab-panel-2702)
* [  TypeScript ](#tab-panel-2703)

JavaScript

```

// Get running workflows (default limit is 50, max is 100)

const { workflows, total } = this.getWorkflows({ status: "running" });


// Filter by metadata

const { workflows: userWorkflows } = this.getWorkflows({

  metadata: { userId: "user-456" },

});


// Pagination with cursor

const page1 = this.getWorkflows({

  status: ["complete", "errored"],

  limit: 20,

  orderBy: "desc",

});


console.log(`Showing ${page1.workflows.length} of ${page1.total} workflows`);


// Get next page using cursor

if (page1.nextCursor) {

  const page2 = this.getWorkflows({

    status: ["complete", "errored"],

    limit: 20,

    orderBy: "desc",

    cursor: page1.nextCursor,

  });

}


```

TypeScript

```

// Get running workflows (default limit is 50, max is 100)

const { workflows, total } = this.getWorkflows({ status: "running" });


// Filter by metadata

const { workflows: userWorkflows } = this.getWorkflows({

  metadata: { userId: "user-456" },

});


// Pagination with cursor

const page1 = this.getWorkflows({

  status: ["complete", "errored"],

  limit: 20,

  orderBy: "desc",

});


console.log(`Showing ${page1.workflows.length} of ${page1.total} workflows`);


// Get next page using cursor

if (page1.nextCursor) {

  const page2 = this.getWorkflows({

    status: ["complete", "errored"],

    limit: 20,

    orderBy: "desc",

    cursor: page1.nextCursor,

  });

}


```

The `WorkflowPage` type:

TypeScript

```

type WorkflowPage = {

  workflows: WorkflowInfo[];

  total: number; // Total matching workflows

  nextCursor: string | null; // null when no more pages

};


```

### deleteWorkflow(instanceId)

Delete a single workflow instance tracking record. Returns `true` if deleted, `false` if not found.

### deleteWorkflows(criteria?)

Delete workflow instance tracking records matching criteria.

* [  JavaScript ](#tab-panel-2688)
* [  TypeScript ](#tab-panel-2689)

JavaScript

```

// Delete completed workflow instances older than 7 days

this.deleteWorkflows({

  status: "complete",

  createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),

});


// Delete all errored and terminated workflows

this.deleteWorkflows({

  status: ["errored", "terminated"],

});


```

TypeScript

```

// Delete completed workflow instances older than 7 days

this.deleteWorkflows({

  status: "complete",

  createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),

});


// Delete all errored and terminated workflows

this.deleteWorkflows({

  status: ["errored", "terminated"],

});


```

### terminateWorkflow(instanceId)

Terminate a running workflow immediately. Sets status to `"terminated"`.

* [  JavaScript ](#tab-panel-2682)
* [  TypeScript ](#tab-panel-2683)

JavaScript

```

await this.terminateWorkflow(instanceId);


```

TypeScript

```

await this.terminateWorkflow(instanceId);


```

Note

`terminate()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare.

### pauseWorkflow(instanceId)

Pause a running workflow. The workflow can be resumed later with `resumeWorkflow()`.

* [  JavaScript ](#tab-panel-2684)
* [  TypeScript ](#tab-panel-2685)

JavaScript

```

await this.pauseWorkflow(instanceId);


```

TypeScript

```

await this.pauseWorkflow(instanceId);


```

Note

`pause()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare.

### resumeWorkflow(instanceId)

Resume a paused workflow.

* [  JavaScript ](#tab-panel-2686)
* [  TypeScript ](#tab-panel-2687)

JavaScript

```

await this.resumeWorkflow(instanceId);


```

TypeScript

```

await this.resumeWorkflow(instanceId);


```

Note

`resume()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare.

### restartWorkflow(instanceId, options?)

Restart a workflow instance from the beginning with the same ID.

* [  JavaScript ](#tab-panel-2690)
* [  TypeScript ](#tab-panel-2691)

JavaScript

```

// Reset tracking (default) - clears timestamps and error fields

await this.restartWorkflow(instanceId);


// Preserve original timestamps

await this.restartWorkflow(instanceId, { resetTracking: false });


```

TypeScript

```

// Reset tracking (default) - clears timestamps and error fields

await this.restartWorkflow(instanceId);


// Preserve original timestamps

await this.restartWorkflow(instanceId, { resetTracking: false });


```

Note

`restart()` is not yet supported in local development with `wrangler dev`. It works when deployed to Cloudflare.

### approveWorkflow(instanceId, options?)

Approve a waiting workflow. Use with `waitForApproval()` in the workflow.

* [  JavaScript ](#tab-panel-2698)
* [  TypeScript ](#tab-panel-2699)

JavaScript

```

await this.approveWorkflow(instanceId, {

  reason: "Approved by admin",

  metadata: { approvedBy: userId },

});


```

TypeScript

```

await this.approveWorkflow(instanceId, {

  reason: "Approved by admin",

  metadata: { approvedBy: userId },

});


```

### rejectWorkflow(instanceId, options?)

Reject a waiting workflow. Causes `waitForApproval()` to throw `WorkflowRejectedError`.

* [  JavaScript ](#tab-panel-2692)
* [  TypeScript ](#tab-panel-2693)

JavaScript

```

await this.rejectWorkflow(instanceId, { reason: "Request denied" });


```

TypeScript

```

await this.rejectWorkflow(instanceId, { reason: "Request denied" });


```

### migrateWorkflowBinding(oldName, newName)

Migrate tracked workflows after renaming a workflow binding.

* [  JavaScript ](#tab-panel-2700)
* [  TypeScript ](#tab-panel-2701)

JavaScript

```

class MyAgent extends Agent {

  async onStart() {

    this.migrateWorkflowBinding("OLD_WORKFLOW", "NEW_WORKFLOW");

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onStart() {

    this.migrateWorkflowBinding("OLD_WORKFLOW", "NEW_WORKFLOW");

  }

}


```

## Lifecycle callbacks

Override these methods in your Agent to handle workflow events:

| Callback           | Parameters                         | Description                           |
| ------------------ | ---------------------------------- | ------------------------------------- |
| onWorkflowProgress | workflowName, instanceId, progress | Called when workflow reports progress |
| onWorkflowComplete | workflowName, instanceId, result?  | Called when workflow completes        |
| onWorkflowError    | workflowName, instanceId, error    | Called when workflow errors           |
| onWorkflowEvent    | workflowName, instanceId, event    | Called when workflow sends an event   |
| onWorkflowCallback | callback: WorkflowCallback         | Called for all callback types         |

* [  JavaScript ](#tab-panel-2704)
* [  TypeScript ](#tab-panel-2705)

JavaScript

```

class MyAgent extends Agent {

  async onWorkflowProgress(workflowName, instanceId, progress) {

    this.broadcast(

      JSON.stringify({ type: "progress", workflowName, instanceId, progress }),

    );

  }


  async onWorkflowComplete(workflowName, instanceId, result) {

    console.log(`${workflowName}/${instanceId} completed`);

  }


  async onWorkflowError(workflowName, instanceId, error) {

    console.error(`${workflowName}/${instanceId} failed:`, error);

  }

}


```

TypeScript

```

class MyAgent extends Agent {

  async onWorkflowProgress(

    workflowName: string,

    instanceId: string,

    progress: unknown,

  ) {

    this.broadcast(

      JSON.stringify({ type: "progress", workflowName, instanceId, progress }),

    );

  }


  async onWorkflowComplete(

    workflowName: string,

    instanceId: string,

    result?: unknown,

  ) {

    console.log(`${workflowName}/${instanceId} completed`);

  }


  async onWorkflowError(

    workflowName: string,

    instanceId: string,

    error: string,

  ) {

    console.error(`${workflowName}/${instanceId} failed:`, error);

  }

}


```

## Workflow tracking

Workflows started with `runWorkflow()` are automatically tracked in the Agent's internal database. You can query, filter, and manage workflows using the methods described above (`getWorkflow()`, `getWorkflows()`, `deleteWorkflow()`, etc.).

### Status values

| Status     | Description           |
| ---------- | --------------------- |
| queued     | Waiting to start      |
| running    | Currently executing   |
| paused     | Paused by user        |
| waiting    | Waiting for event     |
| complete   | Finished successfully |
| errored    | Failed with error     |
| terminated | Manually terminated   |

Use the `metadata` option in `runWorkflow()` to store queryable information (like user IDs or task types) that you can filter on later with `getWorkflows()`.

## Examples

### Human-in-the-loop approval

* [  JavaScript ](#tab-panel-2716)
* [  TypeScript ](#tab-panel-2717)

JavaScript

```

import { AgentWorkflow } from "agents/workflows";

export class ApprovalWorkflow extends AgentWorkflow {

  async run(event, step) {

    const request = await step.do("prepare", async () => {

      return { ...event.payload, preparedAt: Date.now() };

    });


    await this.reportProgress({

      step: "approval",

      status: "pending",

      message: "Awaiting approval",

    });


    // Throws WorkflowRejectedError if rejected

    const approval = await this.waitForApproval(step, {

      timeout: "7 days",

    });


    console.log("Approved by:", approval?.approvedBy);


    const result = await step.do("execute", async () => {

      return executeRequest(request);

    });


    await step.reportComplete(result);

    return result;

  }

}


class MyAgent extends Agent {

  async handleApproval(instanceId, userId) {

    await this.approveWorkflow(instanceId, {

      reason: "Approved by admin",

      metadata: { approvedBy: userId },

    });

  }


  async handleRejection(instanceId, reason) {

    await this.rejectWorkflow(instanceId, { reason });

  }

}


```

TypeScript

```

import { AgentWorkflow } from "agents/workflows";

import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";


export class ApprovalWorkflow extends AgentWorkflow<MyAgent, RequestParams> {

  async run(event: AgentWorkflowEvent<RequestParams>, step: AgentWorkflowStep) {

    const request = await step.do("prepare", async () => {

      return { ...event.payload, preparedAt: Date.now() };

    });


    await this.reportProgress({

      step: "approval",

      status: "pending",

      message: "Awaiting approval",

    });


    // Throws WorkflowRejectedError if rejected

    const approval = await this.waitForApproval<{ approvedBy: string }>(step, {

      timeout: "7 days",

    });


    console.log("Approved by:", approval?.approvedBy);


    const result = await step.do("execute", async () => {

      return executeRequest(request);

    });


    await step.reportComplete(result);

    return result;

  }

}


class MyAgent extends Agent {

  async handleApproval(instanceId: string, userId: string) {

    await this.approveWorkflow(instanceId, {

      reason: "Approved by admin",

      metadata: { approvedBy: userId },

    });

  }


  async handleRejection(instanceId: string, reason: string) {

    await this.rejectWorkflow(instanceId, { reason });

  }

}


```

### Retry with backoff

* [  JavaScript ](#tab-panel-2710)
* [  TypeScript ](#tab-panel-2711)

JavaScript

```

import { AgentWorkflow } from "agents/workflows";

export class ResilientWorkflow extends AgentWorkflow {

  async run(event, step) {

    const result = await step.do(

      "call-api",

      {

        retries: { limit: 5, delay: "10 seconds", backoff: "exponential" },

        timeout: "5 minutes",

      },

      async () => {

        const response = await fetch("https://api.example.com/process", {

          method: "POST",

          body: JSON.stringify(event.payload),

        });

        if (!response.ok) throw new Error(`API error: ${response.status}`);

        return response.json();

      },

    );


    await step.reportComplete(result);

    return result;

  }

}


```

TypeScript

```

import { AgentWorkflow } from "agents/workflows";

import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";


export class ResilientWorkflow extends AgentWorkflow<MyAgent, TaskParams> {

  async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {

    const result = await step.do(

      "call-api",

      {

        retries: { limit: 5, delay: "10 seconds", backoff: "exponential" },

        timeout: "5 minutes",

      },

      async () => {

        const response = await fetch("https://api.example.com/process", {

          method: "POST",

          body: JSON.stringify(event.payload),

        });

        if (!response.ok) throw new Error(`API error: ${response.status}`);

        return response.json();

      },

    );


    await step.reportComplete(result);

    return result;

  }

}


```

### State synchronization

Workflows can update Agent state durably via `step`, which automatically broadcasts to all connected clients:

* [  JavaScript ](#tab-panel-2714)
* [  TypeScript ](#tab-panel-2715)

JavaScript

```

import { AgentWorkflow } from "agents/workflows";

export class StatefulWorkflow extends AgentWorkflow {

  async run(event, step) {

    // Replace entire state (durable, broadcasts to clients)

    await step.updateAgentState({

      currentTask: {

        id: event.payload.taskId,

        status: "processing",

        startedAt: Date.now(),

      },

    });


    const result = await step.do("process", async () =>

      processTask(event.payload),

    );


    // Merge partial state (durable, keeps existing fields)

    await step.mergeAgentState({

      currentTask: { status: "complete", result, completedAt: Date.now() },

    });


    await step.reportComplete(result);

    return result;

  }

}


```

TypeScript

```

import { AgentWorkflow } from "agents/workflows";

import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";


export class StatefulWorkflow extends AgentWorkflow<MyAgent, TaskParams> {

  async run(event: AgentWorkflowEvent<TaskParams>, step: AgentWorkflowStep) {

    // Replace entire state (durable, broadcasts to clients)

    await step.updateAgentState({

      currentTask: {

        id: event.payload.taskId,

        status: "processing",

        startedAt: Date.now(),

      },

    });


    const result = await step.do("process", async () =>

      processTask(event.payload),

    );


    // Merge partial state (durable, keeps existing fields)

    await step.mergeAgentState({

      currentTask: { status: "complete", result, completedAt: Date.now() },

    });


    await step.reportComplete(result);

    return result;

  }

}


```

### Custom progress types

Define custom progress types for domain-specific reporting:

* [  JavaScript ](#tab-panel-2718)
* [  TypeScript ](#tab-panel-2719)

JavaScript

```

import { AgentWorkflow } from "agents/workflows";

// Custom progress type for data pipeline

// Workflow with custom progress type (3rd type parameter)

export class ETLWorkflow extends AgentWorkflow {

  async run(event, step) {

    await this.reportProgress({

      stage: "extract",

      recordsProcessed: 0,

      totalRecords: 1000,

      currentTable: "users",

    });


    // ... processing

  }

}


// Agent receives typed progress

class MyAgent extends Agent {

  async onWorkflowProgress(workflowName, instanceId, progress) {

    const p = progress;

    console.log(`Stage: ${p.stage}, ${p.recordsProcessed}/${p.totalRecords}`);

  }

}


```

TypeScript

```

import { AgentWorkflow } from "agents/workflows";

import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";


// Custom progress type for data pipeline

type PipelineProgress = {

  stage: "extract" | "transform" | "load";

  recordsProcessed: number;

  totalRecords: number;

  currentTable?: string;

};


// Workflow with custom progress type (3rd type parameter)

export class ETLWorkflow extends AgentWorkflow<

  MyAgent,

  ETLParams,

  PipelineProgress

> {

  async run(event: AgentWorkflowEvent<ETLParams>, step: AgentWorkflowStep) {

    await this.reportProgress({

      stage: "extract",

      recordsProcessed: 0,

      totalRecords: 1000,

      currentTable: "users",

    });


    // ... processing

  }

}


// Agent receives typed progress

class MyAgent extends Agent {

  async onWorkflowProgress(

    workflowName: string,

    instanceId: string,

    progress: unknown,

  ) {

    const p = progress as PipelineProgress;

    console.log(`Stage: ${p.stage}, ${p.recordsProcessed}/${p.totalRecords}`);

  }

}


```

### Cleanup strategy

The internal `cf_agents_workflows` table can grow unbounded, so implement a retention policy:

* [  JavaScript ](#tab-panel-2712)
* [  TypeScript ](#tab-panel-2713)

JavaScript

```

class MyAgent extends Agent {

  // Option 1: Delete on completion

  async onWorkflowComplete(workflowName, instanceId, result) {

    // Process result first, then delete

    this.deleteWorkflow(instanceId);

  }


  // Option 2: Scheduled cleanup (keep recent history)

  async cleanupOldWorkflows() {

    this.deleteWorkflows({

      status: ["complete", "errored"],

      createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),

    });

  }


  // Option 3: Keep all history for compliance/auditing

  // Don't call deleteWorkflows() - query historical data as needed

}


```

TypeScript

```

class MyAgent extends Agent {

  // Option 1: Delete on completion

  async onWorkflowComplete(

    workflowName: string,

    instanceId: string,

    result?: unknown,

  ) {

    // Process result first, then delete

    this.deleteWorkflow(instanceId);

  }


  // Option 2: Scheduled cleanup (keep recent history)

  async cleanupOldWorkflows() {

    this.deleteWorkflows({

      status: ["complete", "errored"],

      createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),

    });

  }


  // Option 3: Keep all history for compliance/auditing

  // Don't call deleteWorkflows() - query historical data as needed

}


```

## Bidirectional communication

### Workflow to Agent

* [  JavaScript ](#tab-panel-2708)
* [  TypeScript ](#tab-panel-2709)

JavaScript

```

// Direct RPC call (typed)

await this.agent.updateTaskStatus(taskId, "processing");

const data = await this.agent.getData(taskId);


// Non-durable callbacks (may repeat on retry, use for frequent updates)

await this.reportProgress({ step: "process", percent: 0.5 });

this.broadcastToClients({ type: "update", data });


// Durable callbacks via step (idempotent, won't repeat on retry)

await step.reportComplete(result);

await step.reportError("Something went wrong");

await step.sendEvent({ type: "custom", data: {} });


// Durable state synchronization via step (broadcasts to clients)

await step.updateAgentState({ status: "processing" });

await step.mergeAgentState({ progress: 0.5 });


```

TypeScript

```

// Direct RPC call (typed)

await this.agent.updateTaskStatus(taskId, "processing");

const data = await this.agent.getData(taskId);


// Non-durable callbacks (may repeat on retry, use for frequent updates)

await this.reportProgress({ step: "process", percent: 0.5 });

this.broadcastToClients({ type: "update", data });


// Durable callbacks via step (idempotent, won't repeat on retry)

await step.reportComplete(result);

await step.reportError("Something went wrong");

await step.sendEvent({ type: "custom", data: {} });


// Durable state synchronization via step (broadcasts to clients)

await step.updateAgentState({ status: "processing" });

await step.mergeAgentState({ progress: 0.5 });


```

### Agent to Workflow

* [  JavaScript ](#tab-panel-2706)
* [  TypeScript ](#tab-panel-2707)

JavaScript

```

// Send event to waiting workflow

await this.sendWorkflowEvent("MY_WORKFLOW", instanceId, {

  type: "custom-event",

  payload: { action: "proceed" },

});


// Approve/reject workflows using convenience methods

await this.approveWorkflow(instanceId, {

  reason: "Approved by admin",

  metadata: { approvedBy: userId },

});


await this.rejectWorkflow(instanceId, { reason: "Request denied" });


```

TypeScript

```

// Send event to waiting workflow

await this.sendWorkflowEvent("MY_WORKFLOW", instanceId, {

  type: "custom-event",

  payload: { action: "proceed" },

});


// Approve/reject workflows using convenience methods

await this.approveWorkflow(instanceId, {

  reason: "Approved by admin",

  metadata: { approvedBy: userId },

});


await this.rejectWorkflow(instanceId, { reason: "Request denied" });


```

## Best practices

1. **Keep workflows focused** — One workflow per logical task
2. **Use meaningful step names** — Helps with debugging and observability
3. **Report progress regularly** — Keeps users informed
4. **Handle errors gracefully** — Use `reportError()` before throwing
5. **Clean up completed workflows** — Implement a retention policy for the tracking table
6. **Handle workflow binding renames** — Use `migrateWorkflowBinding()` when renaming workflow bindings in `wrangler.jsonc`

## Limitations

| Constraint          | Limit                                                     |
| ------------------- | --------------------------------------------------------- |
| Maximum steps       | 10,000 per workflow (default) / configurable up to 25,000 |
| State size          | 10 MB per workflow                                        |
| Event wait time     | 1 year maximum                                            |
| Step execution time | 30 minutes per step                                       |

Workflows cannot open WebSocket connections directly. Use `broadcastToClients()` to communicate with connected clients through the Agent.

## Related resources

[ Workflows documentation ](https://developers.cloudflare.com/workflows/) Learn about Cloudflare Workflows fundamentals. 

[ Store and sync state ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Persist and synchronize agent state. 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Time-based task execution. 

[ Human-in-the-loop ](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) Approval flows and manual intervention patterns. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/run-workflows/","name":"Run Workflows"}}]}
```

---

---
title: Schedule tasks
description: Schedule tasks to run in the future — whether that is seconds from now, at a specific date/time, or on a recurring cron schedule. Scheduled tasks survive agent restarts and are persisted to SQLite.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/schedule-tasks.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Schedule tasks

Schedule tasks to run in the future — whether that is seconds from now, at a specific date/time, or on a recurring cron schedule. Scheduled tasks survive agent restarts and are persisted to SQLite.

Scheduled tasks can do anything a request or message from a user can: make requests, query databases, send emails, read and write state. Scheduled tasks can invoke any regular method on your Agent.

## Overview

The scheduling system supports four modes:

| Mode          | Syntax                             | Use case                  |
| ------------- | ---------------------------------- | ------------------------- |
| **Delayed**   | this.schedule(60, ...)             | Run in 60 seconds         |
| **Scheduled** | this.schedule(new Date(...), ...)  | Run at specific time      |
| **Cron**      | this.schedule("0 8 \* \* \*", ...) | Run on recurring schedule |
| **Interval**  | this.scheduleEvery(30, ...)        | Run every 30 seconds      |

Under the hood, scheduling uses [Durable Object alarms](https://developers.cloudflare.com/durable-objects/api/alarms/) to wake the agent at the right time. Tasks are stored in a SQLite table and executed in order.

## Quick start

* [  JavaScript ](#tab-panel-2738)
* [  TypeScript ](#tab-panel-2739)

JavaScript

```

import { Agent } from "agents";


export class ReminderAgent extends Agent {

  async onRequest(request) {

    const url = new URL(request.url);


    // Schedule in 30 seconds

    await this.schedule(30, "sendReminder", {

      message: "Check your email",

    });


    // Schedule at specific time

    await this.schedule(new Date("2025-02-01T09:00:00Z"), "sendReminder", {

      message: "Monthly report due",

    });


    // Schedule recurring (every day at 8am)

    await this.schedule("0 8 * * *", "dailyDigest", {

      userId: url.searchParams.get("userId"),

    });


    return new Response("Scheduled!");

  }


  async sendReminder(payload) {

    console.log(`Reminder: ${payload.message}`);

    // Send notification, email, etc.

  }


  async dailyDigest(payload) {

    console.log(`Sending daily digest to ${payload.userId}`);

    // Generate and send digest

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class ReminderAgent extends Agent {

  async onRequest(request: Request) {

    const url = new URL(request.url);


    // Schedule in 30 seconds

    await this.schedule(30, "sendReminder", {

      message: "Check your email",

    });


    // Schedule at specific time

    await this.schedule(new Date("2025-02-01T09:00:00Z"), "sendReminder", {

      message: "Monthly report due",

    });


    // Schedule recurring (every day at 8am)

    await this.schedule("0 8 * * *", "dailyDigest", {

      userId: url.searchParams.get("userId"),

    });


    return new Response("Scheduled!");

  }


  async sendReminder(payload: { message: string }) {

    console.log(`Reminder: ${payload.message}`);

    // Send notification, email, etc.

  }


  async dailyDigest(payload: { userId: string }) {

    console.log(`Sending daily digest to ${payload.userId}`);

    // Generate and send digest

  }

}


```

## Scheduling modes

### Delayed execution

Pass a number to schedule a task to run after a delay in **seconds**:

* [  JavaScript ](#tab-panel-2720)
* [  TypeScript ](#tab-panel-2721)

JavaScript

```

// Run in 10 seconds

await this.schedule(10, "processTask", { taskId: "123" });


// Run in 5 minutes (300 seconds)

await this.schedule(300, "sendFollowUp", { email: "user@example.com" });


// Run in 1 hour

await this.schedule(3600, "checkStatus", { orderId: "abc" });


```

TypeScript

```

// Run in 10 seconds

await this.schedule(10, "processTask", { taskId: "123" });


// Run in 5 minutes (300 seconds)

await this.schedule(300, "sendFollowUp", { email: "user@example.com" });


// Run in 1 hour

await this.schedule(3600, "checkStatus", { orderId: "abc" });


```

**Use cases:**

* Debouncing rapid events
* Delayed notifications ("You left items in your cart")
* Retry with backoff
* Rate limiting

### Scheduled execution

Pass a `Date` object to schedule a task at a specific time:

* [  JavaScript ](#tab-panel-2724)
* [  TypeScript ](#tab-panel-2725)

JavaScript

```

// Run tomorrow at noon

const tomorrow = new Date();

tomorrow.setDate(tomorrow.getDate() + 1);

tomorrow.setHours(12, 0, 0, 0);

await this.schedule(tomorrow, "sendReminder", { message: "Meeting time!" });


// Run at a specific timestamp

await this.schedule(new Date("2025-06-15T14:30:00Z"), "triggerEvent", {

  eventId: "conference-2025",

});


// Run in 2 hours using Date math

const twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000);

await this.schedule(twoHoursFromNow, "checkIn", {});


```

TypeScript

```

// Run tomorrow at noon

const tomorrow = new Date();

tomorrow.setDate(tomorrow.getDate() + 1);

tomorrow.setHours(12, 0, 0, 0);

await this.schedule(tomorrow, "sendReminder", { message: "Meeting time!" });


// Run at a specific timestamp

await this.schedule(new Date("2025-06-15T14:30:00Z"), "triggerEvent", {

  eventId: "conference-2025",

});


// Run in 2 hours using Date math

const twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000);

await this.schedule(twoHoursFromNow, "checkIn", {});


```

**Use cases:**

* Appointment reminders
* Deadline notifications
* Scheduled content publishing
* Time-based triggers

### Recurring (cron)

Pass a cron expression string for recurring schedules:

* [  JavaScript ](#tab-panel-2728)
* [  TypeScript ](#tab-panel-2729)

JavaScript

```

// Every day at 8:00 AM

await this.schedule("0 8 * * *", "dailyReport", {});


// Every hour

await this.schedule("0 * * * *", "hourlyCheck", {});


// Every Monday at 9:00 AM

await this.schedule("0 9 * * 1", "weeklySync", {});


// Every 15 minutes

await this.schedule("*/15 * * * *", "pollForUpdates", {});


// First day of every month at midnight

await this.schedule("0 0 1 * *", "monthlyCleanup", {});


```

TypeScript

```

// Every day at 8:00 AM

await this.schedule("0 8 * * *", "dailyReport", {});


// Every hour

await this.schedule("0 * * * *", "hourlyCheck", {});


// Every Monday at 9:00 AM

await this.schedule("0 9 * * 1", "weeklySync", {});


// Every 15 minutes

await this.schedule("*/15 * * * *", "pollForUpdates", {});


// First day of every month at midnight

await this.schedule("0 0 1 * *", "monthlyCleanup", {});


```

**Cron syntax:** `minute hour day month weekday`

| Field        | Values         | Special characters |
| ------------ | -------------- | ------------------ |
| Minute       | 0-59           | \* , \- /          |
| Hour         | 0-23           | \* , \- /          |
| Day of Month | 1-31           | \* , \- /          |
| Month        | 1-12           | \* , \- /          |
| Day of Week  | 0-6 (0=Sunday) | \* , \- /          |

**Common patterns:**

* [  JavaScript ](#tab-panel-2722)
* [  TypeScript ](#tab-panel-2723)

JavaScript

```

"* * * * *"; // Every minute

"*/5 * * * *"; // Every 5 minutes

"0 * * * *"; // Every hour (on the hour)

"0 0 * * *"; // Every day at midnight

"0 8 * * 1-5"; // Weekdays at 8am

"0 0 * * 0"; // Every Sunday at midnight

"0 0 1 * *"; // First of every month


```

TypeScript

```

"* * * * *"; // Every minute

"*/5 * * * *"; // Every 5 minutes

"0 * * * *"; // Every hour (on the hour)

"0 0 * * *"; // Every day at midnight

"0 8 * * 1-5"; // Weekdays at 8am

"0 0 * * 0"; // Every Sunday at midnight

"0 0 1 * *"; // First of every month


```

**Use cases:**

* Daily/weekly reports
* Periodic cleanup jobs
* Polling external services
* Health checks
* Subscription renewals

### Interval

Use `scheduleEvery()` to run a task at fixed intervals (in seconds). Unlike cron, intervals support sub-minute precision and arbitrary durations:

* [  JavaScript ](#tab-panel-2726)
* [  TypeScript ](#tab-panel-2727)

JavaScript

```

// Poll every 30 seconds

await this.scheduleEvery(30, "poll", { source: "api" });


// Health check every 45 seconds

await this.scheduleEvery(45, "healthCheck", {});


// Sync every 90 seconds (1.5 minutes - cannot be expressed in cron)

await this.scheduleEvery(90, "syncData", { destination: "warehouse" });


```

TypeScript

```

// Poll every 30 seconds

await this.scheduleEvery(30, "poll", { source: "api" });


// Health check every 45 seconds

await this.scheduleEvery(45, "healthCheck", {});


// Sync every 90 seconds (1.5 minutes - cannot be expressed in cron)

await this.scheduleEvery(90, "syncData", { destination: "warehouse" });


```

**Key differences from cron:**

| Feature             | Cron                                  | Interval               |
| ------------------- | ------------------------------------- | ---------------------- |
| Minimum granularity | 1 minute                              | 1 second               |
| Arbitrary intervals | No (must fit cron pattern)            | Yes                    |
| Fixed schedule      | Yes (for example, "every day at 8am") | No (relative to start) |
| Overlap prevention  | No                                    | Yes (built-in)         |

**Overlap prevention:**

If a callback takes longer than the interval, the next execution is skipped (not queued). This prevents runaway resource usage:

* [  JavaScript ](#tab-panel-2732)
* [  TypeScript ](#tab-panel-2733)

JavaScript

```

class PollingAgent extends Agent {

  async poll() {

    // If this takes 45 seconds and interval is 30 seconds,

    // the next poll is skipped (with a warning logged)

    const data = await slowExternalApi();

    await this.processData(data);

  }

}


// Set up 30-second interval

await this.scheduleEvery(30, "poll", {});


```

TypeScript

```

class PollingAgent extends Agent {

  async poll() {

    // If this takes 45 seconds and interval is 30 seconds,

    // the next poll is skipped (with a warning logged)

    const data = await slowExternalApi();

    await this.processData(data);

  }

}


// Set up 30-second interval

await this.scheduleEvery(30, "poll", {});


```

When a skip occurs, you will see a warning in logs:

```

Skipping interval schedule abc123: previous execution still running


```

**Error resilience:**

If the callback throws an error, the interval continues — only that execution fails:

* [  JavaScript ](#tab-panel-2730)
* [  TypeScript ](#tab-panel-2731)

JavaScript

```

class SyncAgent extends Agent {

  async syncData() {

    // Even if this throws, the interval keeps running

    const response = await fetch("https://api.example.com/data");

    if (!response.ok) throw new Error("Sync failed");

    // ...

  }

}


```

TypeScript

```

class SyncAgent extends Agent {

  async syncData() {

    // Even if this throws, the interval keeps running

    const response = await fetch("https://api.example.com/data");

    if (!response.ok) throw new Error("Sync failed");

    // ...

  }

}


```

**Use cases:**

* Sub-minute polling (every 10, 30, 45 seconds)
* Intervals that do not map to cron (every 90 seconds, every 7 minutes)
* Rate-limited API polling with precise control
* Real-time data synchronization

## Managing scheduled tasks

### Get a schedule

Retrieve a scheduled task by its ID:

* [  JavaScript ](#tab-panel-2734)
* [  TypeScript ](#tab-panel-2735)

JavaScript

```

const schedule = this.getSchedule(scheduleId);


if (schedule) {

  console.log(

    `Task ${schedule.id} will run at ${new Date(schedule.time * 1000)}`,

  );

  console.log(`Callback: ${schedule.callback}`);

  console.log(`Type: ${schedule.type}`); // "scheduled" | "delayed" | "cron" | "interval"

} else {

  console.log("Schedule not found");

}


```

TypeScript

```

const schedule = this.getSchedule(scheduleId);


if (schedule) {

  console.log(

    `Task ${schedule.id} will run at ${new Date(schedule.time * 1000)}`,

  );

  console.log(`Callback: ${schedule.callback}`);

  console.log(`Type: ${schedule.type}`); // "scheduled" | "delayed" | "cron" | "interval"

} else {

  console.log("Schedule not found");

}


```

### List schedules

Query scheduled tasks with optional filters:

* [  JavaScript ](#tab-panel-2744)
* [  TypeScript ](#tab-panel-2745)

JavaScript

```

// Get all scheduled tasks

const allSchedules = this.getSchedules();


// Get only cron jobs

const cronJobs = this.getSchedules({ type: "cron" });


// Get tasks in the next hour

const upcoming = this.getSchedules({

  timeRange: {

    start: new Date(),

    end: new Date(Date.now() + 60 * 60 * 1000),

  },

});


// Get a specific task by ID

const specific = this.getSchedules({ id: "abc123" });


// Combine filters

const upcomingCronJobs = this.getSchedules({

  type: "cron",

  timeRange: {

    start: new Date(),

    end: new Date(Date.now() + 24 * 60 * 60 * 1000),

  },

});


```

TypeScript

```

// Get all scheduled tasks

const allSchedules = this.getSchedules();


// Get only cron jobs

const cronJobs = this.getSchedules({ type: "cron" });


// Get tasks in the next hour

const upcoming = this.getSchedules({

  timeRange: {

    start: new Date(),

    end: new Date(Date.now() + 60 * 60 * 1000),

  },

});


// Get a specific task by ID

const specific = this.getSchedules({ id: "abc123" });


// Combine filters

const upcomingCronJobs = this.getSchedules({

  type: "cron",

  timeRange: {

    start: new Date(),

    end: new Date(Date.now() + 24 * 60 * 60 * 1000),

  },

});


```

### Cancel a schedule

Remove a scheduled task before it executes:

* [  JavaScript ](#tab-panel-2736)
* [  TypeScript ](#tab-panel-2737)

JavaScript

```

const cancelled = await this.cancelSchedule(scheduleId);


if (cancelled) {

  console.log("Schedule cancelled successfully");

} else {

  console.log("Schedule not found (may have already executed)");

}


```

TypeScript

```

const cancelled = await this.cancelSchedule(scheduleId);


if (cancelled) {

  console.log("Schedule cancelled successfully");

} else {

  console.log("Schedule not found (may have already executed)");

}


```

**Example: Cancellable reminders**

* [  JavaScript ](#tab-panel-2756)
* [  TypeScript ](#tab-panel-2757)

JavaScript

```

class ReminderAgent extends Agent {

  async setReminder(userId, message, delaySeconds) {

    const schedule = await this.schedule(delaySeconds, "sendReminder", {

      userId,

      message,

    });


    // Store the schedule ID so user can cancel later

    this.sql`

      INSERT INTO user_reminders (user_id, schedule_id, message)

      VALUES (${userId}, ${schedule.id}, ${message})

    `;


    return schedule.id;

  }


  async cancelReminder(scheduleId) {

    const cancelled = await this.cancelSchedule(scheduleId);


    if (cancelled) {

      this.sql`DELETE FROM user_reminders WHERE schedule_id = ${scheduleId}`;

    }


    return cancelled;

  }


  async sendReminder(payload) {

    // Send the reminder...


    // Clean up the record

    this.sql`DELETE FROM user_reminders WHERE user_id = ${payload.userId}`;

  }

}


```

TypeScript

```

class ReminderAgent extends Agent {

  async setReminder(userId: string, message: string, delaySeconds: number) {

    const schedule = await this.schedule(delaySeconds, "sendReminder", {

      userId,

      message,

    });


    // Store the schedule ID so user can cancel later

    this.sql`

      INSERT INTO user_reminders (user_id, schedule_id, message)

      VALUES (${userId}, ${schedule.id}, ${message})

    `;


    return schedule.id;

  }


  async cancelReminder(scheduleId: string) {

    const cancelled = await this.cancelSchedule(scheduleId);


    if (cancelled) {

      this.sql`DELETE FROM user_reminders WHERE schedule_id = ${scheduleId}`;

    }


    return cancelled;

  }


  async sendReminder(payload: { userId: string; message: string }) {

    // Send the reminder...


    // Clean up the record

    this.sql`DELETE FROM user_reminders WHERE user_id = ${payload.userId}`;

  }

}


```

## The Schedule object

When you create or retrieve a schedule, you get a `Schedule` object:

TypeScript

```

type Schedule<T> = {

  id: string; // Unique identifier

  callback: string; // Method name to call

  payload: T; // Data passed to the callback

  time: number; // Unix timestamp (seconds) of next execution

} & (

  | { type: "scheduled" } // One-time at specific date

  | { type: "delayed"; delayInSeconds: number } // One-time after delay

  | { type: "cron"; cron: string } // Recurring (cron expression)

  | { type: "interval"; intervalSeconds: number } // Recurring (fixed interval)

);


```

**Example:**

* [  JavaScript ](#tab-panel-2740)
* [  TypeScript ](#tab-panel-2741)

JavaScript

```

const schedule = await this.schedule(60, "myTask", { foo: "bar" });


console.log(schedule);

// {

//   id: "abc123xyz",

//   callback: "myTask",

//   payload: { foo: "bar" },

//   time: 1706745600,

//   type: "delayed",

//   delayInSeconds: 60

// }


```

TypeScript

```

const schedule = await this.schedule(60, "myTask", { foo: "bar" });


console.log(schedule);

// {

//   id: "abc123xyz",

//   callback: "myTask",

//   payload: { foo: "bar" },

//   time: 1706745600,

//   type: "delayed",

//   delayInSeconds: 60

// }


```

## Patterns

### Rescheduling from callbacks

For dynamic recurring schedules, schedule the next run from within the callback:

* [  JavaScript ](#tab-panel-2754)
* [  TypeScript ](#tab-panel-2755)

JavaScript

```

class PollingAgent extends Agent {

  async startPolling(intervalSeconds) {

    await this.schedule(intervalSeconds, "poll", { interval: intervalSeconds });

  }


  async poll(payload) {

    try {

      const data = await fetch("https://api.example.com/updates");

      await this.processUpdates(await data.json());

    } catch (error) {

      console.error("Polling failed:", error);

    }


    // Schedule the next poll (regardless of success/failure)

    await this.schedule(payload.interval, "poll", payload);

  }


  async stopPolling() {

    // Cancel all polling schedules

    const schedules = this.getSchedules({ type: "delayed" });

    for (const schedule of schedules) {

      if (schedule.callback === "poll") {

        await this.cancelSchedule(schedule.id);

      }

    }

  }

}


```

TypeScript

```

class PollingAgent extends Agent {

  async startPolling(intervalSeconds: number) {

    await this.schedule(intervalSeconds, "poll", { interval: intervalSeconds });

  }


  async poll(payload: { interval: number }) {

    try {

      const data = await fetch("https://api.example.com/updates");

      await this.processUpdates(await data.json());

    } catch (error) {

      console.error("Polling failed:", error);

    }


    // Schedule the next poll (regardless of success/failure)

    await this.schedule(payload.interval, "poll", payload);

  }


  async stopPolling() {

    // Cancel all polling schedules

    const schedules = this.getSchedules({ type: "delayed" });

    for (const schedule of schedules) {

      if (schedule.callback === "poll") {

        await this.cancelSchedule(schedule.id);

      }

    }

  }

}


```

### Exponential backoff retry

* [  JavaScript ](#tab-panel-2758)
* [  TypeScript ](#tab-panel-2759)

JavaScript

```

class RetryAgent extends Agent {

  async attemptTask(payload) {

    try {

      await this.doWork(payload.taskId);

      console.log(

        `Task ${payload.taskId} succeeded on attempt ${payload.attempt}`,

      );

    } catch (error) {

      if (payload.attempt >= payload.maxAttempts) {

        console.error(

          `Task ${payload.taskId} failed after ${payload.maxAttempts} attempts`,

        );

        return;

      }


      // Exponential backoff: 2^attempt seconds (2s, 4s, 8s, 16s...)

      const delaySeconds = Math.pow(2, payload.attempt);


      await this.schedule(delaySeconds, "attemptTask", {

        ...payload,

        attempt: payload.attempt + 1,

      });


      console.log(`Retrying task ${payload.taskId} in ${delaySeconds}s`);

    }

  }


  async doWork(taskId) {

    // Your actual work here

  }

}


```

TypeScript

```

class RetryAgent extends Agent {

  async attemptTask(payload: {

    taskId: string;

    attempt: number;

    maxAttempts: number;

  }) {

    try {

      await this.doWork(payload.taskId);

      console.log(

        `Task ${payload.taskId} succeeded on attempt ${payload.attempt}`,

      );

    } catch (error) {

      if (payload.attempt >= payload.maxAttempts) {

        console.error(

          `Task ${payload.taskId} failed after ${payload.maxAttempts} attempts`,

        );

        return;

      }


      // Exponential backoff: 2^attempt seconds (2s, 4s, 8s, 16s...)

      const delaySeconds = Math.pow(2, payload.attempt);


      await this.schedule(delaySeconds, "attemptTask", {

        ...payload,

        attempt: payload.attempt + 1,

      });


      console.log(`Retrying task ${payload.taskId} in ${delaySeconds}s`);

    }

  }


  async doWork(taskId: string) {

    // Your actual work here

  }

}


```

### Self-destructing agents

You can safely call `this.destroy()` from within a scheduled callback:

* [  JavaScript ](#tab-panel-2746)
* [  TypeScript ](#tab-panel-2747)

JavaScript

```

class TemporaryAgent extends Agent {

  async onStart() {

    // Self-destruct in 24 hours

    await this.schedule(24 * 60 * 60, "cleanup", {});

  }


  async cleanup() {

    // Perform final cleanup

    console.log("Agent lifetime expired, cleaning up...");


    // This is safe to call from a scheduled callback

    await this.destroy();

  }

}


```

TypeScript

```

class TemporaryAgent extends Agent {

  async onStart() {

    // Self-destruct in 24 hours

    await this.schedule(24 * 60 * 60, "cleanup", {});

  }


  async cleanup() {

    // Perform final cleanup

    console.log("Agent lifetime expired, cleaning up...");


    // This is safe to call from a scheduled callback

    await this.destroy();

  }

}


```

Note

When `destroy()` is called from within a scheduled task, the Agent SDK defers the destruction to ensure the scheduled callback completes successfully. The Agent instance will be evicted immediately after the callback finishes executing.

## AI-assisted scheduling

The SDK includes utilities for parsing natural language scheduling requests with AI.

### `getSchedulePrompt()`

Returns a system prompt for parsing natural language into scheduling parameters:

* [  JavaScript ](#tab-panel-2760)
* [  TypeScript ](#tab-panel-2761)

JavaScript

```

import { getSchedulePrompt, scheduleSchema } from "agents";

import { generateObject } from "ai";

import { openai } from "@ai-sdk/openai";


class SmartScheduler extends Agent {

  async parseScheduleRequest(userInput) {

    const result = await generateObject({

      model: openai("gpt-4o"),

      system: getSchedulePrompt({ date: new Date() }),

      prompt: userInput,

      schema: scheduleSchema,

    });


    return result.object;

  }


  async handleUserRequest(input) {

    // Parse: "remind me to call mom tomorrow at 3pm"

    const parsed = await this.parseScheduleRequest(input);


    // parsed = {

    //   description: "call mom",

    //   when: {

    //     type: "scheduled",

    //     date: "2025-01-30T15:00:00Z"

    //   }

    // }


    if (parsed.when.type === "scheduled" && parsed.when.date) {

      await this.schedule(new Date(parsed.when.date), "sendReminder", {

        message: parsed.description,

      });

    } else if (parsed.when.type === "delayed" && parsed.when.delayInSeconds) {

      await this.schedule(parsed.when.delayInSeconds, "sendReminder", {

        message: parsed.description,

      });

    } else if (parsed.when.type === "cron" && parsed.when.cron) {

      await this.schedule(parsed.when.cron, "sendReminder", {

        message: parsed.description,

      });

    }

  }


  async sendReminder(payload) {

    console.log(`Reminder: ${payload.message}`);

  }

}


```

TypeScript

```

import { getSchedulePrompt, scheduleSchema } from "agents";

import { generateObject } from "ai";

import { openai } from "@ai-sdk/openai";


class SmartScheduler extends Agent {

  async parseScheduleRequest(userInput: string) {

    const result = await generateObject({

      model: openai("gpt-4o"),

      system: getSchedulePrompt({ date: new Date() }),

      prompt: userInput,

      schema: scheduleSchema,

    });


    return result.object;

  }


  async handleUserRequest(input: string) {

    // Parse: "remind me to call mom tomorrow at 3pm"

    const parsed = await this.parseScheduleRequest(input);


    // parsed = {

    //   description: "call mom",

    //   when: {

    //     type: "scheduled",

    //     date: "2025-01-30T15:00:00Z"

    //   }

    // }


    if (parsed.when.type === "scheduled" && parsed.when.date) {

      await this.schedule(new Date(parsed.when.date), "sendReminder", {

        message: parsed.description,

      });

    } else if (parsed.when.type === "delayed" && parsed.when.delayInSeconds) {

      await this.schedule(parsed.when.delayInSeconds, "sendReminder", {

        message: parsed.description,

      });

    } else if (parsed.when.type === "cron" && parsed.when.cron) {

      await this.schedule(parsed.when.cron, "sendReminder", {

        message: parsed.description,

      });

    }

  }


  async sendReminder(payload: { message: string }) {

    console.log(`Reminder: ${payload.message}`);

  }

}


```

### `scheduleSchema`

A Zod schema for validating parsed scheduling data. Uses a discriminated union on `when.type` so each variant only contains the fields it needs:

* [  JavaScript ](#tab-panel-2750)
* [  TypeScript ](#tab-panel-2751)

JavaScript

```

import { scheduleSchema } from "agents";


// The schema is a discriminated union:

// {

//   description: string,

//   when:

//     | { type: "scheduled", date: string }       // ISO 8601 date string

//     | { type: "delayed", delayInSeconds: number }

//     | { type: "cron", cron: string }

//     | { type: "no-schedule" }

// }


```

TypeScript

```

import { scheduleSchema } from "agents";


// The schema is a discriminated union:

// {

//   description: string,

//   when:

//     | { type: "scheduled", date: string }       // ISO 8601 date string

//     | { type: "delayed", delayInSeconds: number }

//     | { type: "cron", cron: string }

//     | { type: "no-schedule" }

// }


```

Note

Dates are returned as ISO 8601 strings (not `Date` objects) for compatibility with both Zod v3 and v4 JSON schema generation.

## Scheduling vs Queue vs Workflows

| Feature            | Queue              | Scheduling        | Workflows           |
| ------------------ | ------------------ | ----------------- | ------------------- |
| **When**           | Immediately (FIFO) | Future time       | Future time         |
| **Execution**      | Sequential         | At scheduled time | Multi-step          |
| **Retries**        | Built-in           | Built-in          | Automatic           |
| **Persistence**    | SQLite             | SQLite            | Workflow engine     |
| **Recurring**      | No                 | Yes (cron)        | No (use scheduling) |
| **Complex logic**  | No                 | No                | Yes                 |
| **Human approval** | No                 | No                | Yes                 |

Use Queue when:

* You need background processing without blocking the response
* Tasks should run ASAP but do not need to block
* Order matters (FIFO)

Use Scheduling when:

* Tasks need to run at a specific time
* You need recurring jobs (cron)
* Delayed execution (debouncing, retries)

Use Workflows when:

* Multi-step processes with dependencies
* Automatic retries with backoff
* Human-in-the-loop approvals
* Long-running tasks (minutes to hours)

## API reference

### `schedule()`

TypeScript

```

async schedule<T>(

  when: Date | string | number,

  callback: keyof this,

  payload?: T,

  options?: { retry?: RetryOptions }

): Promise<Schedule<T>>


```

Schedule a task for future execution.

**Parameters:**

* `when` \- When to execute: `number` (seconds delay), `Date` (specific time), or `string` (cron expression)
* `callback` \- Name of the method to call
* `payload` \- Data to pass to the callback (must be JSON-serializable)
* `options.retry` \- Optional retry configuration. Refer to [Retries](https://developers.cloudflare.com/agents/api-reference/retries/) for details.

**Returns:** A `Schedule` object with the task details

Warning

Tasks that set a callback for a method that does not exist will throw an exception. Ensure that the method named in the `callback` argument exists on your `Agent` class.

### `scheduleEvery()`

TypeScript

```

async scheduleEvery<T>(

  intervalSeconds: number,

  callback: keyof this,

  payload?: T,

  options?: { retry?: RetryOptions }

): Promise<Schedule<T>>


```

Schedule a task to run repeatedly at a fixed interval.

**Parameters:**

* `intervalSeconds` \- Number of seconds between executions (must be greater than 0)
* `callback` \- Name of the method to call
* `payload` \- Data to pass to the callback (must be JSON-serializable)
* `options.retry` \- Optional retry configuration. Refer to [Retries](https://developers.cloudflare.com/agents/api-reference/retries/) for details.

**Returns:** A `Schedule` object with `type: "interval"`

**Behavior:**

* First execution occurs after `intervalSeconds` (not immediately)
* If callback is still running when next execution is due, it is skipped (overlap prevention)
* If callback throws an error, the interval continues
* Cancel with `cancelSchedule(id)` to stop the entire interval

### `getSchedule()`

TypeScript

```

getSchedule<T>(id: string): Schedule<T> | undefined


```

Get a scheduled task by ID. Returns `undefined` if not found. This method is synchronous.

### `getSchedules()`

TypeScript

```

getSchedules<T>(criteria?: {

  id?: string;

  type?: "scheduled" | "delayed" | "cron" | "interval";

  timeRange?: { start?: Date; end?: Date };

}): Schedule<T>[]


```

Get scheduled tasks matching the criteria. This method is synchronous.

### `cancelSchedule()`

TypeScript

```

async cancelSchedule(id: string): Promise<boolean>


```

Cancel a scheduled task. Returns `true` if cancelled, `false` if not found.

### `keepAlive()`

TypeScript

```

async keepAlive(): Promise<() => void>


```

Prevent the Durable Object from being evicted due to inactivity by creating a 30-second heartbeat schedule. Returns a disposer function that cancels the heartbeat when called. The disposer is idempotent — calling it multiple times is safe.

Always call the disposer when the work is done — otherwise the heartbeat continues indefinitely.

* [  JavaScript ](#tab-panel-2748)
* [  TypeScript ](#tab-panel-2749)

JavaScript

```

const dispose = await this.keepAlive();

try {

  // Long-running work that must not be interrupted

  const result = await longRunningComputation();

  await sendResults(result);

} finally {

  dispose();

}


```

TypeScript

```

const dispose = await this.keepAlive();

try {

  // Long-running work that must not be interrupted

  const result = await longRunningComputation();

  await sendResults(result);

} finally {

  dispose();

}


```

### `keepAliveWhile()`

TypeScript

```

async keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>


```

Run an async function while keeping the Durable Object alive. The heartbeat is automatically started before the function runs and stopped when it completes (whether it succeeds or throws). Returns the value returned by the function.

This is the recommended way to use `keepAlive` — it guarantees cleanup.

* [  JavaScript ](#tab-panel-2742)
* [  TypeScript ](#tab-panel-2743)

JavaScript

```

const result = await this.keepAliveWhile(async () => {

  const data = await longRunningComputation();

  return data;

});


```

TypeScript

```

const result = await this.keepAliveWhile(async () => {

  const data = await longRunningComputation();

  return data;

});


```

## Keeping the agent alive

Durable Objects are evicted after a period of inactivity (typically 70-140 seconds with no incoming requests, WebSocket messages, or alarms). During long-running operations — streaming LLM responses, waiting on external APIs, running multi-step computations — the agent can be evicted mid-flight.

`keepAlive()` prevents this by creating a 30-second heartbeat schedule. The internal heartbeat callback is a no-op — the alarm firing itself is what resets the inactivity timer. Because it uses the scheduling system:

* The heartbeat does not conflict with your own schedules (the scheduling system multiplexes through a single alarm slot)
* The heartbeat shows up in `getSchedules()` if you need to inspect it
* Multiple concurrent `keepAlive()` calls each get their own schedule, so they do not interfere with each other

### Multiple concurrent callers

Each `keepAlive()` call returns an independent disposer:

* [  JavaScript ](#tab-panel-2752)
* [  TypeScript ](#tab-panel-2753)

JavaScript

```

const dispose1 = await this.keepAlive();

const dispose2 = await this.keepAlive();


// Both heartbeats are active

dispose1(); // Only cancels the first heartbeat

// Agent is still alive via dispose2's heartbeat


dispose2(); // Now the agent can go idle


```

TypeScript

```

const dispose1 = await this.keepAlive();

const dispose2 = await this.keepAlive();


// Both heartbeats are active

dispose1(); // Only cancels the first heartbeat

// Agent is still alive via dispose2's heartbeat


dispose2(); // Now the agent can go idle


```

### AIChatAgent

`AIChatAgent` automatically calls `keepAlive()` during streaming responses. You do not need to add it yourself when using `AIChatAgent` — every LLM stream is protected from idle eviction by default.

### When to use keepAlive

| Scenario                                    | Use keepAlive?                         |
| ------------------------------------------- | -------------------------------------- |
| Streaming LLM responses via AIChatAgent     | No — already built in                  |
| Long-running computation in a custom Agent  | Yes                                    |
| Waiting on a slow external API call         | Yes                                    |
| Multi-step tool execution                   | Yes                                    |
| Short request-response handlers             | No — not needed                        |
| Background work via scheduling or workflows | No — alarms already keep the DO active |

Note

`keepAlive()` is marked `@experimental` and may change between releases.

## Limits

* **Maximum tasks:** Limited by SQLite storage (each task is a row). Practical limit is tens of thousands per agent.
* **Task size:** Each task (including payload) can be up to 2MB.
* **Minimum delay:** 0 seconds (runs on next alarm tick)
* **Cron precision:** Minute-level (not seconds)
* **Interval precision:** Second-level
* **Cron jobs:** After execution, automatically rescheduled for the next occurrence
* **Interval jobs:** After execution, rescheduled for `now + intervalSeconds`; skipped if still running

## Next steps

[ Queue tasks ](https://developers.cloudflare.com/agents/api-reference/queue-tasks/) Immediate background task processing. 

[ Run Workflows ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Durable multi-step background processing. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/schedule-tasks/","name":"Schedule tasks"}}]}
```

---

---
title: Store and sync state
description: Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/store-and-sync-state.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Store and sync state

Agents provide built-in state management with automatic persistence and real-time synchronization across all connected clients.

## Overview

State within an Agent is:

* **Persistent** \- Automatically saves to SQLite, survives restarts and hibernation
* **Synchronized** \- Changes are broadcast to all connected WebSocket clients instantly
* **Bidirectional** \- Both server and clients can update state
* **Type-safe** \- Full TypeScript support with generics
* **Immediately consistent** \- Read your own writes
* **Thread-safe** \- Safe for concurrent updates
* **Fast** \- State is colocated wherever the Agent is running

Agent state is stored in a SQL database embedded within each individual Agent instance. You can interact with it using the higher-level `this.setState` API (recommended), which allows you to sync state and trigger events on state changes, or by directly querying the database with `this.sql`.

State vs Props

**State** is persistent data that survives restarts and syncs across clients. **[Props](https://developers.cloudflare.com/agents/api-reference/routing/#props)** are one-time initialization arguments passed when an agent is instantiated - use props for configuration that does not need to persist.

* [  JavaScript ](#tab-panel-2776)
* [  TypeScript ](#tab-panel-2777)

JavaScript

```

import { Agent } from "agents";


export class GameAgent extends Agent {

  // Default state for new agents

  initialState = {

    players: [],

    score: 0,

    status: "waiting",

  };


  // React to state changes

  onStateChanged(state, source) {

    if (source !== "server" && state.players.length >= 2) {

      // Client added a player, start the game

      this.setState({ ...state, status: "playing" });

    }

  }


  addPlayer(name) {

    this.setState({

      ...this.state,

      players: [...this.state.players, name],

    });

  }

}


```

TypeScript

```

import { Agent } from "agents";


type GameState = {

  players: string[];

  score: number;

  status: "waiting" | "playing" | "finished";

};


export class GameAgent extends Agent<Env, GameState> {

  // Default state for new agents

  initialState: GameState = {

    players: [],

    score: 0,

    status: "waiting",

  };


  // React to state changes

  onStateChanged(state: GameState, source: Connection | "server") {

    if (source !== "server" && state.players.length >= 2) {

      // Client added a player, start the game

      this.setState({ ...state, status: "playing" });

    }

  }


  addPlayer(name: string) {

    this.setState({

      ...this.state,

      players: [...this.state.players, name],

    });

  }

}


```

## Defining initial state

Use the `initialState` property to define default values for new agent instances:

* [  JavaScript ](#tab-panel-2766)
* [  TypeScript ](#tab-panel-2767)

JavaScript

```

export class ChatAgent extends Agent {

  initialState = {

    messages: [],

    settings: { theme: "dark", notifications: true },

    lastActive: null,

  };

}


```

TypeScript

```

type State = {

  messages: Message[];

  settings: UserSettings;

  lastActive: string | null;

};


export class ChatAgent extends Agent<Env, State> {

  initialState: State = {

    messages: [],

    settings: { theme: "dark", notifications: true },

    lastActive: null,

  };

}


```

### Type safety

The second generic parameter to `Agent` defines your state type:

* [  JavaScript ](#tab-panel-2762)
* [  TypeScript ](#tab-panel-2763)

JavaScript

```

// State is fully typed

export class MyAgent extends Agent {

  initialState = { count: 0 };


  increment() {

    // TypeScript knows this.state is MyState

    this.setState({ count: this.state.count + 1 });

  }

}


```

TypeScript

```

// State is fully typed

export class MyAgent extends Agent<Env, MyState> {

  initialState: MyState = { count: 0 };


  increment() {

    // TypeScript knows this.state is MyState

    this.setState({ count: this.state.count + 1 });

  }

}


```

### When initial state applies

Initial state is applied lazily on first access, not on every wake:

1. **New agent** \- `initialState` is used and persisted
2. **Existing agent** \- Persisted state is loaded from SQLite
3. **No `initialState` defined** \- `this.state` is `undefined`

* [  JavaScript ](#tab-panel-2764)
* [  TypeScript ](#tab-panel-2765)

JavaScript

```

class MyAgent extends Agent {

  initialState = { count: 0 };

  async onStart() {

    // Safe to access - returns initialState if new, or persisted state

    console.log("Current count:", this.state.count);

  }

}


```

TypeScript

```

class MyAgent extends Agent<Env, { count: number }> {

  initialState = { count: 0 };

  async onStart() {

    // Safe to access - returns initialState if new, or persisted state

    console.log("Current count:", this.state.count);

  }

}


```

## Reading state

Access the current state via the `this.state` getter:

* [  JavaScript ](#tab-panel-2772)
* [  TypeScript ](#tab-panel-2773)

JavaScript

```

class MyAgent extends Agent {

  async onRequest(request) {

    // Read current state

    const { players, status } = this.state;


    if (status === "waiting" && players.length < 2) {

      return new Response("Waiting for players...");

    }


    return Response.json(this.state);

  }

}


```

TypeScript

```

class MyAgent extends Agent<

  Env,

  { players: string[]; status: "waiting" | "playing" | "finished" }

> {

  async onRequest(request: Request) {

    // Read current state

    const { players, status } = this.state;


    if (status === "waiting" && players.length < 2) {

      return new Response("Waiting for players...");

    }


    return Response.json(this.state);

  }

}


```

### Undefined state

If you do not define `initialState`, `this.state` returns `undefined`:

* [  JavaScript ](#tab-panel-2768)
* [  TypeScript ](#tab-panel-2769)

JavaScript

```

export class MinimalAgent extends Agent {

  // No initialState defined


  async onConnect(connection) {

    if (!this.state) {

      // First time - initialize state

      this.setState({ initialized: true });

    }

  }

}


```

TypeScript

```

export class MinimalAgent extends Agent {

  // No initialState defined


  async onConnect(connection: Connection) {

    if (!this.state) {

      // First time - initialize state

      this.setState({ initialized: true });

    }

  }

}


```

## Updating state

Use `setState()` to update state. This:

1. Saves to SQLite (persistent)
2. Broadcasts to all connected clients (excluding connections where [shouldSendProtocolMessages](https://developers.cloudflare.com/agents/api-reference/protocol-messages/) returned `false`)
3. Triggers `onStateChanged()` (after broadcast; best-effort)

* [  JavaScript ](#tab-panel-2774)
* [  TypeScript ](#tab-panel-2775)

JavaScript

```

// Replace entire state

this.setState({

  players: ["Alice", "Bob"],

  score: 0,

  status: "playing",

});


// Update specific fields (spread existing state)

this.setState({

  ...this.state,

  score: this.state.score + 10,

});


```

TypeScript

```

// Replace entire state

this.setState({

  players: ["Alice", "Bob"],

  score: 0,

  status: "playing",

});


// Update specific fields (spread existing state)

this.setState({

  ...this.state,

  score: this.state.score + 10,

});


```

### State must be serializable

State is stored as JSON, so it must be serializable:

* [  JavaScript ](#tab-panel-2778)
* [  TypeScript ](#tab-panel-2779)

JavaScript

```

// Good - plain objects, arrays, primitives

this.setState({

  items: ["a", "b", "c"],

  count: 42,

  active: true,

  metadata: { key: "value" },

});


// Bad - functions, classes, circular references

// Functions do not serialize

// Dates become strings, lose methods

// Circular references fail


// For dates, use ISO strings

this.setState({

  createdAt: new Date().toISOString(),

});


```

TypeScript

```

// Good - plain objects, arrays, primitives

this.setState({

  items: ["a", "b", "c"],

  count: 42,

  active: true,

  metadata: { key: "value" },

});


// Bad - functions, classes, circular references

// Functions do not serialize

// Dates become strings, lose methods

// Circular references fail


// For dates, use ISO strings

this.setState({

  createdAt: new Date().toISOString(),

});


```

## Responding to state changes

Override `onStateChanged()` to react when state changes (notifications/side-effects):

* [  JavaScript ](#tab-panel-2770)
* [  TypeScript ](#tab-panel-2771)

JavaScript

```

class MyAgent extends Agent {

  onStateChanged(state, source) {

    console.log("State updated:", state);

    console.log("Updated by:", source === "server" ? "server" : source.id);

  }

}


```

TypeScript

```

class MyAgent extends Agent<Env, GameState> {

  onStateChanged(state: GameState, source: Connection | "server") {

    console.log("State updated:", state);

    console.log("Updated by:", source === "server" ? "server" : source.id);

  }

}


```

### The source parameter

The `source` shows who triggered the update:

| Value      | Meaning                             |
| ---------- | ----------------------------------- |
| "server"   | Agent called setState()             |
| Connection | A client pushed state via WebSocket |

This is useful for:

* Avoiding infinite loops (do not react to your own updates)
* Validating client input
* Triggering side effects only on client actions

* [  JavaScript ](#tab-panel-2782)
* [  TypeScript ](#tab-panel-2783)

JavaScript

```

class MyAgent extends Agent {

  onStateChanged(state, source) {

    // Ignore server-initiated updates

    if (source === "server") return;


    // A client updated state - validate and process

    const connection = source;

    console.log(`Client ${connection.id} updated state`);


    // Maybe trigger something based on the change

    if (state.status === "submitted") {

      this.processSubmission(state);

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent<

  Env,

  { status: "waiting" | "playing" | "finished" }

> {

  onStateChanged(state: GameState, source: Connection | "server") {

    // Ignore server-initiated updates

    if (source === "server") return;


    // A client updated state - validate and process

    const connection = source;

    console.log(`Client ${connection.id} updated state`);


    // Maybe trigger something based on the change

    if (state.status === "submitted") {

      this.processSubmission(state);

    }

  }

}


```

### Common pattern: Client-driven actions

* [  JavaScript ](#tab-panel-2784)
* [  TypeScript ](#tab-panel-2785)

JavaScript

```

class MyAgent extends Agent {

  onStateChanged(state, source) {

    if (source === "server") return;


    // Client added a message

    const lastMessage = state.messages[state.messages.length - 1];

    if (lastMessage && !lastMessage.processed) {

      // Process and update

      this.setState({

        ...state,

        messages: state.messages.map((m) =>

          m.id === lastMessage.id ? { ...m, processed: true } : m,

        ),

      });

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent<Env, { messages: Message[] }> {

  onStateChanged(state: State, source: Connection | "server") {

    if (source === "server") return;


    // Client added a message

    const lastMessage = state.messages[state.messages.length - 1];

    if (lastMessage && !lastMessage.processed) {

      // Process and update

      this.setState({

        ...state,

        messages: state.messages.map((m) =>

          m.id === lastMessage.id ? { ...m, processed: true } : m,

        ),

      });

    }

  }

}


```

## Validating state updates

If you want to validate or reject state updates, override `validateStateChange()`:

* Runs before persistence and broadcast
* Must be synchronous
* Throwing aborts the update

* [  JavaScript ](#tab-panel-2780)
* [  TypeScript ](#tab-panel-2781)

JavaScript

```

class MyAgent extends Agent {

  validateStateChange(nextState, source) {

    // Example: reject negative scores

    if (nextState.score < 0) {

      throw new Error("score cannot be negative");

    }


    // Example: only allow certain status transitions

    if (this.state.status === "finished" && nextState.status !== "finished") {

      throw new Error("Cannot restart a finished game");

    }

  }

}


```

TypeScript

```

class MyAgent extends Agent<Env, GameState> {

  validateStateChange(nextState: GameState, source: Connection | "server") {

    // Example: reject negative scores

    if (nextState.score < 0) {

      throw new Error("score cannot be negative");

    }


    // Example: only allow certain status transitions

    if (this.state.status === "finished" && nextState.status !== "finished") {

      throw new Error("Cannot restart a finished game");

    }

  }

}


```

Note

`onStateChanged()` is not intended for validation; it is a notification hook and should not block broadcasts. Use `validateStateChange()` for validation.

## Client-side state sync

State synchronizes automatically with connected clients.

### React (useAgent)

* [  JavaScript ](#tab-panel-2792)
* [  TypeScript ](#tab-panel-2793)

JavaScript

```

import { useAgent } from "agents/react";


function GameUI() {

  const agent = useAgent({

    agent: "game-agent",

    name: "room-123",

    onStateUpdate: (state, source) => {

      console.log("State updated:", state);

    },

  });


  // Push state to agent

  const addPlayer = (name) => {

    agent.setState({

      ...agent.state,

      players: [...agent.state.players, name],

    });

  };


  return <div>Players: {agent.state?.players.join(", ")}</div>;

}


```

TypeScript

```

import { useAgent } from "agents/react";


function GameUI() {

  const agent = useAgent({

    agent: "game-agent",

    name: "room-123",

    onStateUpdate: (state, source) => {

      console.log("State updated:", state);

    }

  });


  // Push state to agent

  const addPlayer = (name: string) => {

    agent.setState({

      ...agent.state,

      players: [...agent.state.players, name]

    });

  };


  return <div>Players: {agent.state?.players.join(", ")}</div>;

}


```

### Vanilla JS (AgentClient)

* [  JavaScript ](#tab-panel-2786)
* [  TypeScript ](#tab-panel-2787)

JavaScript

```

import { AgentClient } from "agents/client";


const client = new AgentClient({

  agent: "game-agent",

  name: "room-123",

  onStateUpdate: (state) => {

    document.getElementById("score").textContent = state.score;

  },

});


// Push state update

client.setState({ ...client.state, score: 100 });


```

TypeScript

```

import { AgentClient } from "agents/client";


const client = new AgentClient({

  agent: "game-agent",

  name: "room-123",

  onStateUpdate: (state) => {

    document.getElementById("score").textContent = state.score;

  },

});


// Push state update

client.setState({ ...client.state, score: 100 });


```

### State flow

flowchart TD
    subgraph Agent
        S["this.state<br/>(persisted in SQLite)"]
    end
    subgraph Clients
        C1["Client 1"]
        C2["Client 2"]
        C3["Client 3"]
    end
    C1 & C2 & C3 -->|setState| S
    S -->|broadcast via WebSocket| C1 & C2 & C3

## State from Workflows

When using [Workflows](https://developers.cloudflare.com/agents/api-reference/run-workflows/), you can update agent state from workflow steps:

* [  JavaScript ](#tab-panel-2790)
* [  TypeScript ](#tab-panel-2791)

JavaScript

```

// In your workflow

class MyWorkflow extends Workflow {

  async run(event, step) {

    // Replace entire state

    await step.updateAgentState({ status: "processing", progress: 0 });


    // Merge partial updates (preserves other fields)

    await step.mergeAgentState({ progress: 50 });


    // Reset to initialState

    await step.resetAgentState();


    return result;

  }

}


```

TypeScript

```

// In your workflow

class MyWorkflow extends Workflow<Env> {

  async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) {

    // Replace entire state

    await step.updateAgentState({ status: "processing", progress: 0 });


    // Merge partial updates (preserves other fields)

    await step.mergeAgentState({ progress: 50 });


    // Reset to initialState

    await step.resetAgentState();


    return result;

  }

}


```

These are durable operations - they persist even if the workflow retries.

## SQL API

Every individual Agent instance has its own SQL (SQLite) database that runs within the same context as the Agent itself. This means that inserting or querying data within your Agent is effectively zero-latency: the Agent does not have to round-trip across a continent or the world to access its own data.

You can access the SQL API within any method on an Agent via `this.sql`. The SQL API accepts template literals:

* [  JavaScript ](#tab-panel-2788)
* [  TypeScript ](#tab-panel-2789)

JavaScript

```

export class MyAgent extends Agent {

  async onRequest(request) {

    let userId = new URL(request.url).searchParams.get("userId");


    // 'users' is just an example here: you can create arbitrary tables and define your own schemas

    // within each Agent's database using SQL (SQLite syntax).

    let [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;

    return Response.json(user);

  }

}


```

TypeScript

```

export class MyAgent extends Agent {

  async onRequest(request: Request) {

    let userId = new URL(request.url).searchParams.get("userId");


    // 'users' is just an example here: you can create arbitrary tables and define your own schemas

    // within each Agent's database using SQL (SQLite syntax).

    let [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;

    return Response.json(user);

  }

}


```

You can also supply a TypeScript type argument to the query, which will be used to infer the type of the result:

* [  JavaScript ](#tab-panel-2794)
* [  TypeScript ](#tab-panel-2795)

JavaScript

```

export class MyAgent extends Agent {

  async onRequest(request) {

    let userId = new URL(request.url).searchParams.get("userId");

    // Supply the type parameter to the query when calling this.sql

    // This assumes the results returns one or more User rows with "id", "name", and "email" columns

    const [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;

    return Response.json(user);

  }

}


```

TypeScript

```

type User = {

  id: string;

  name: string;

  email: string;

};


export class MyAgent extends Agent {

  async onRequest(request: Request) {

    let userId = new URL(request.url).searchParams.get("userId");

    // Supply the type parameter to the query when calling this.sql

    // This assumes the results returns one or more User rows with "id", "name", and "email" columns

    const [user] = this.sql<User>`SELECT * FROM users WHERE id = ${userId}`;

    return Response.json(user);

  }

}


```

You do not need to specify an array type (`User[]` or `Array<User>`), as `this.sql` will always return an array of the specified type.

Note

Providing a type parameter does not validate that the result matches your type definition. If you need to validate incoming events, we recommend a library such as [zod ↗](https://zod.dev/) or your own validator logic.

The SQL API exposed to an Agent is similar to the one [within Durable Objects](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/#sql-api). You can use the same SQL queries with the Agent's database. Create tables and query data, just as you would with Durable Objects or [D1](https://developers.cloudflare.com/d1/).

## Best practices

### Keep state small

State is broadcast to all clients on every change. For large data:

TypeScript

```

// Bad - storing large arrays in state

initialState = {

  allMessages: [] // Could grow to thousands of items

};


// Good - store in SQL, keep state light

initialState = {

  messageCount: 0,

  lastMessageId: null

};


// Query SQL for full data

async getMessages(limit = 50) {

  return this.sql`SELECT * FROM messages ORDER BY created_at DESC LIMIT ${limit}`;

}


```

### Optimistic updates

For responsive UIs, update client state immediately:

* [  JavaScript ](#tab-panel-2798)
* [  TypeScript ](#tab-panel-2799)

JavaScript

```

// Client-side

function sendMessage(text) {

  const optimisticMessage = {

    id: crypto.randomUUID(),

    text,

    pending: true,

  };


  // Update immediately

  agent.setState({

    ...agent.state,

    messages: [...agent.state.messages, optimisticMessage],

  });


  // Server will confirm/update

}


// Server-side

class MyAgent extends Agent {

  onStateChanged(state, source) {

    if (source === "server") return;


    const pendingMessages = state.messages.filter((m) => m.pending);

    for (const msg of pendingMessages) {

      // Validate and confirm

      this.setState({

        ...state,

        messages: state.messages.map((m) =>

          m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m,

        ),

      });

    }

  }

}


```

TypeScript

```

// Client-side

function sendMessage(text: string) {

  const optimisticMessage = {

    id: crypto.randomUUID(),

    text,

    pending: true,

  };


  // Update immediately

  agent.setState({

    ...agent.state,

    messages: [...agent.state.messages, optimisticMessage],

  });


  // Server will confirm/update

}


// Server-side

class MyAgent extends Agent<Env, { messages: Message[] }> {

  onStateChanged(state: GameState, source: Connection | "server") {

    if (source === "server") return;


    const pendingMessages = state.messages.filter((m) => m.pending);

    for (const msg of pendingMessages) {

      // Validate and confirm

      this.setState({

        ...state,

        messages: state.messages.map((m) =>

          m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m,

        ),

      });

    }

  }

}


```

### State vs SQL

| Use State For                      | Use SQL For       |
| ---------------------------------- | ----------------- |
| UI state (loading, selected items) | Historical data   |
| Real-time counters                 | Large collections |
| Active session data                | Relationships     |
| Configuration                      | Queryable data    |

* [  JavaScript ](#tab-panel-2796)
* [  TypeScript ](#tab-panel-2797)

JavaScript

```

export class ChatAgent extends Agent {

  // State: current UI state

  initialState = {

    typing: [],

    unreadCount: 0,

    activeUsers: [],

  };


  // SQL: message history

  async getMessages(limit = 100) {

    return this.sql`

      SELECT * FROM messages

      ORDER BY created_at DESC

      LIMIT ${limit}

    `;

  }


  async saveMessage(message) {

    this.sql`

      INSERT INTO messages (id, text, user_id, created_at)

      VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})

    `;

    // Update state for real-time UI

    this.setState({

      ...this.state,

      unreadCount: this.state.unreadCount + 1,

    });

  }

}


```

TypeScript

```

export class ChatAgent extends Agent {

  // State: current UI state

  initialState = {

    typing: [],

    unreadCount: 0,

    activeUsers: [],

  };


  // SQL: message history

  async getMessages(limit = 100) {

    return this.sql`

      SELECT * FROM messages

      ORDER BY created_at DESC

      LIMIT ${limit}

    `;

  }


  async saveMessage(message: Message) {

    this.sql`

      INSERT INTO messages (id, text, user_id, created_at)

      VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})

    `;

    // Update state for real-time UI

    this.setState({

      ...this.state,

      unreadCount: this.state.unreadCount + 1,

    });

  }

}


```

### Avoid infinite loops

Be careful not to trigger state updates in response to your own updates:

TypeScript

```

// Bad - infinite loop

onStateChanged(state: State) {

  this.setState({ ...state, lastUpdated: Date.now() });

}


// Good - check source

onStateChanged(state: State, source: Connection | "server") {

  if (source === "server") return; // Do not react to own updates

  this.setState({ ...state, lastUpdated: Date.now() });

}


```

## Use Agent state as model context

You can combine the state and SQL APIs in your Agent with its ability to [call AI models](https://developers.cloudflare.com/agents/api-reference/using-ai-models/) to include historical context within your prompts to a model. Modern Large Language Models (LLMs) often have very large context windows (up to millions of tokens), which allows you to pull relevant context into your prompt directly.

For example, you can use an Agent's built-in SQL database to pull history, query a model with it, and append to that history ahead of the next call to the model:

* [  JavaScript ](#tab-panel-2800)
* [  TypeScript ](#tab-panel-2801)

JavaScript

```

export class ReasoningAgent extends Agent {

  async callReasoningModel(prompt) {

    let result = this

      .sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`;

    let context = [];

    for (const row of result) {

      context.push(row.entry);

    }


    const systemPrompt = prompt.system || "You are a helpful assistant.";

    const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;


    try {

      const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {

        messages: [

          { role: "system", content: systemPrompt },

          { role: "user", content: userPrompt },

        ],

      });


      // Store the response in history

      this

        .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`;


      return response.response;

    } catch (error) {

      console.error("Error calling reasoning model:", error);

      throw error;

    }

  }

}


```

TypeScript

```

interface Env {

  AI: Ai;

}


export class ReasoningAgent extends Agent<Env> {

  async callReasoningModel(prompt: Prompt) {

    let result = this

      .sql<History>`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`;

    let context = [];

    for (const row of result) {

      context.push(row.entry);

    }


    const systemPrompt = prompt.system || "You are a helpful assistant.";

    const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;


    try {

      const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {

        messages: [

          { role: "system", content: systemPrompt },

          { role: "user", content: userPrompt },

        ],

      });


      // Store the response in history

      this

        .sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`;


      return response.response;

    } catch (error) {

      console.error("Error calling reasoning model:", error);

      throw error;

    }

  }

}


```

This works because each instance of an Agent has its own database, and the state stored in that database is private to that Agent: whether it is acting on behalf of a single user, a room or channel, or a deep research tool. By default, you do not have to manage contention or reach out over the network to a centralized database to retrieve and store state.

## API reference

### Properties

| Property     | Type  | Description                  |
| ------------ | ----- | ---------------------------- |
| state        | State | Current state (getter)       |
| initialState | State | Default state for new agents |

### Methods

| Method              | Signature                                                  | Description                                   |
| ------------------- | ---------------------------------------------------------- | --------------------------------------------- |
| setState            | (state: State) => void                                     | Update state, persist, and broadcast          |
| onStateChanged      | (state: State, source: Connection \| "server") => void     | Called when state changes                     |
| validateStateChange | (nextState: State, source: Connection \| "server") => void | Validate before persistence (throw to reject) |

### Workflow step methods

| Method                        | Description                         |
| ----------------------------- | ----------------------------------- |
| step.updateAgentState(state)  | Replace agent state from workflow   |
| step.mergeAgentState(partial) | Merge partial state from workflow   |
| step.resetAgentState()        | Reset to initialState from workflow |

## Next steps

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ Build a chat agent ](https://developers.cloudflare.com/agents/getting-started/build-a-chat-agent/) Build and deploy an AI chat agent. 

[ WebSockets ](https://developers.cloudflare.com/agents/api-reference/websockets/) Build interactive agents with real-time data streaming. 

[ Run Workflows ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Orchestrate asynchronous workflows from your agent. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/store-and-sync-state/","name":"Store and sync state"}}]}
```

---

---
title: Using AI Models
description: Agents can call AI models from any provider. Workers AI is built in and requires no API keys. You can also use OpenAI, Anthropic, Google Gemini, or any service that exposes an OpenAI-compatible API.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ AI ](https://developers.cloudflare.com/search/?tags=AI) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/using-ai-models.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Using AI Models

Agents can call AI models from any provider. [Workers AI](https://developers.cloudflare.com/workers-ai/) is built in and requires no API keys. You can also use [OpenAI ↗](https://platform.openai.com/docs/quickstart?language=javascript), [Anthropic ↗](https://docs.anthropic.com/en/api/client-sdks#typescript), [Google Gemini ↗](https://ai.google.dev/gemini-api/docs/openai), or any service that exposes an OpenAI-compatible API.

The [AI SDK ↗](https://sdk.vercel.ai/docs/introduction) provides a unified interface across all of these providers, and is what `AIChatAgent` and the starter template use under the hood. You can also use the model routing features in [AI Gateway](https://developers.cloudflare.com/ai-gateway/) to route across providers, eval responses, and manage rate limits.

## Calling AI Models

You can call models from any method within an Agent, including from HTTP requests using the [onRequest](https://developers.cloudflare.com/agents/api-reference/agents-api/) handler, when a [scheduled task](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) runs, when handling a WebSocket message in the [onMessage](https://developers.cloudflare.com/agents/api-reference/websockets/) handler, or from any of your own methods.

Agents can call AI models on their own — autonomously — and can handle long-running responses that take minutes (or longer) to respond in full. If a client disconnects mid-stream, the Agent keeps running and can catch the client up when it reconnects.

### Streaming over WebSockets

Modern reasoning models can take some time to both generate a response _and_ stream the response back to the client. Instead of buffering the entire response, you can stream it back over [WebSockets](https://developers.cloudflare.com/agents/api-reference/websockets/).

* [  JavaScript ](#tab-panel-2814)
* [  TypeScript ](#tab-panel-2815)

src/index.js

```

import { Agent } from "agents";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class MyAgent extends Agent {

  async onConnect(connection, ctx) {

    //

  }


  async onMessage(connection, message) {

    let msg = JSON.parse(message);

    await this.queryReasoningModel(connection, msg.prompt);

  }


  async queryReasoningModel(connection, userPrompt) {

    try {

      const workersai = createWorkersAI({ binding: this.env.AI });

      const result = streamText({

        model: workersai("@cf/zai-org/glm-4.7-flash"),

        prompt: userPrompt,

      });


      for await (const chunk of result.textStream) {

        if (chunk) {

          connection.send(JSON.stringify({ type: "chunk", content: chunk }));

        }

      }


      connection.send(JSON.stringify({ type: "done" }));

    } catch (error) {

      connection.send(JSON.stringify({ type: "error", error: error }));

    }

  }

}


```

src/index.ts

```

import { Agent } from "agents";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


interface Env {

  AI: Ai;

}


export class MyAgent extends Agent<Env> {

  async onConnect(connection: Connection, ctx: ConnectionContext) {

    //

  }


  async onMessage(connection: Connection, message: WSMessage) {

    let msg = JSON.parse(message);

    await this.queryReasoningModel(connection, msg.prompt);

  }


  async queryReasoningModel(connection: Connection, userPrompt: string) {

    try {

      const workersai = createWorkersAI({ binding: this.env.AI });

      const result = streamText({

        model: workersai("@cf/zai-org/glm-4.7-flash"),

        prompt: userPrompt,

      });


      for await (const chunk of result.textStream) {

        if (chunk) {

          connection.send(JSON.stringify({ type: "chunk", content: chunk }));

        }

      }


      connection.send(JSON.stringify({ type: "done" }));

    } catch (error) {

      connection.send(JSON.stringify({ type: "error", error: error }));

    }

  }

}


```

You can also persist AI model responses back to [Agent state](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) using `this.setState`. If a user disconnects, read the message history back and send it to the user when they reconnect.

## Workers AI

You can use [any of the models available in Workers AI](https://developers.cloudflare.com/workers-ai/models/) within your Agent by [configuring a binding](https://developers.cloudflare.com/workers-ai/configuration/bindings/). No API keys are required.

Workers AI supports streaming responses by setting `stream: true`. Use streaming to avoid buffering and delaying responses, especially for larger models or reasoning models.

* [  JavaScript ](#tab-panel-2808)
* [  TypeScript ](#tab-panel-2809)

src/index.js

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async onRequest(request) {

    const stream = await this.env.AI.run(

      "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",

      {

        prompt: "Build me a Cloudflare Worker that returns JSON.",

        stream: true,

      },

    );


    return new Response(stream, {

      headers: { "content-type": "text/event-stream" },

    });

  }

}


```

src/index.ts

```

import { Agent } from "agents";


interface Env {

  AI: Ai;

}


export class MyAgent extends Agent<Env> {

  async onRequest(request: Request) {

    const stream = await this.env.AI.run(

      "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",

      {

        prompt: "Build me a Cloudflare Worker that returns JSON.",

        stream: true,

      },

    );


    return new Response(stream, {

      headers: { "content-type": "text/event-stream" },

    });

  }

}


```

Your Wrangler configuration needs an `ai` binding:

* [  wrangler.jsonc ](#tab-panel-2802)
* [  wrangler.toml ](#tab-panel-2803)

```

{

  "ai": {

    "binding": "AI",

  },

}


```

```

[ai]

binding = "AI"


```

### Model routing

You can use [AI Gateway](https://developers.cloudflare.com/ai-gateway/) directly from an Agent by specifying a [gateway configuration](https://developers.cloudflare.com/ai-gateway/usage/providers/workersai/) when calling the AI binding. Model routing lets you route requests across providers based on availability, rate limits, or cost budgets.

* [  JavaScript ](#tab-panel-2812)
* [  TypeScript ](#tab-panel-2813)

src/index.js

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  async onRequest(request) {

    const response = await this.env.AI.run(

      "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",

      {

        prompt: "Build me a Cloudflare Worker that returns JSON.",

      },

      {

        gateway: {

          id: "{gateway_id}",

          skipCache: false,

          cacheTtl: 3360,

        },

      },

    );


    return Response.json(response);

  }

}


```

src/index.ts

```

import { Agent } from "agents";


interface Env {

  AI: Ai;

}


export class MyAgent extends Agent<Env> {

  async onRequest(request: Request) {

    const response = await this.env.AI.run(

      "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",

      {

        prompt: "Build me a Cloudflare Worker that returns JSON.",

      },

      {

        gateway: {

          id: "{gateway_id}",

          skipCache: false,

          cacheTtl: 3360,

        },

      },

    );


    return Response.json(response);

  }

}


```

The `ai` binding in your Wrangler configuration is shared across both Workers AI and AI Gateway.

* [  wrangler.jsonc ](#tab-panel-2804)
* [  wrangler.toml ](#tab-panel-2805)

```

{

  "ai": {

    "binding": "AI",

  },

}


```

```

[ai]

binding = "AI"


```

Visit the [AI Gateway documentation](https://developers.cloudflare.com/ai-gateway/) to learn how to configure a gateway and retrieve a gateway ID.

## AI SDK

The [AI SDK ↗](https://sdk.vercel.ai/docs/introduction) provides a unified API for text generation, tool calling, structured responses, and more. It works with any provider that has an AI SDK adapter, including Workers AI via [workers-ai-provider ↗](https://www.npmjs.com/package/workers-ai-provider).

 npm  yarn  pnpm  bun 

```
npm i ai workers-ai-provider
```

```
yarn add ai workers-ai-provider
```

```
pnpm add ai workers-ai-provider
```

```
bun add ai workers-ai-provider
```

* [  JavaScript ](#tab-panel-2810)
* [  TypeScript ](#tab-panel-2811)

src/index.js

```

import { Agent } from "agents";

import { generateText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class MyAgent extends Agent {

  async onRequest(request) {

    const workersai = createWorkersAI({ binding: this.env.AI });

    const { text } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: "Build me an AI agent on Cloudflare Workers",

    });


    return Response.json({ modelResponse: text });

  }

}


```

src/index.ts

```

import { Agent } from "agents";

import { generateText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


interface Env {

  AI: Ai;

}


export class MyAgent extends Agent<Env> {

  async onRequest(request: Request): Promise<Response> {

    const workersai = createWorkersAI({ binding: this.env.AI });

    const { text } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: "Build me an AI agent on Cloudflare Workers",

    });


    return Response.json({ modelResponse: text });

  }

}


```

You can swap the provider to use OpenAI, Anthropic, or any other AI SDK-compatible adapter:

 npm  yarn  pnpm  bun 

```
npm i ai @ai-sdk/openai
```

```
yarn add ai @ai-sdk/openai
```

```
pnpm add ai @ai-sdk/openai
```

```
bun add ai @ai-sdk/openai
```

* [  JavaScript ](#tab-panel-2806)
* [  TypeScript ](#tab-panel-2807)

src/index.js

```

import { Agent } from "agents";

import { generateText } from "ai";

import { openai } from "@ai-sdk/openai";


export class MyAgent extends Agent {

  async onRequest(request) {

    const { text } = await generateText({

      model: openai("gpt-4o"),

      prompt: "Build me an AI agent on Cloudflare Workers",

    });


    return Response.json({ modelResponse: text });

  }

}


```

src/index.ts

```

import { Agent } from "agents";

import { generateText } from "ai";

import { openai } from "@ai-sdk/openai";


export class MyAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const { text } = await generateText({

      model: openai("gpt-4o"),

      prompt: "Build me an AI agent on Cloudflare Workers",

    });


    return Response.json({ modelResponse: text });

  }

}


```

## OpenAI-compatible endpoints

Agents can call models across any service that supports the OpenAI API. For example, you can use the OpenAI SDK to call one of [Google's Gemini models ↗](https://ai.google.dev/gemini-api/docs/openai#node.js) directly from your Agent.

Agents can stream responses back over HTTP using Server-Sent Events (SSE) from within an `onRequest` handler, or by using the native [WebSocket API](https://developers.cloudflare.com/agents/api-reference/websockets/) to stream responses back to a client.

* [  JavaScript ](#tab-panel-2816)
* [  TypeScript ](#tab-panel-2817)

src/index.js

```

import { Agent } from "agents";

import { OpenAI } from "openai";


export class MyAgent extends Agent {

  async onRequest(request) {

    const client = new OpenAI({

      apiKey: this.env.GEMINI_API_KEY,

      baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",

    });


    let { readable, writable } = new TransformStream();

    let writer = writable.getWriter();

    const textEncoder = new TextEncoder();


    this.ctx.waitUntil(

      (async () => {

        const stream = await client.chat.completions.create({

          model: "gemini-2.0-flash",

          messages: [

            { role: "user", content: "Write me a Cloudflare Worker." },

          ],

          stream: true,

        });


        for await (const part of stream) {

          writer.write(

            textEncoder.encode(part.choices[0]?.delta?.content || ""),

          );

        }

        writer.close();

      })(),

    );


    return new Response(readable);

  }

}


```

src/index.ts

```

import { Agent } from "agents";

import { OpenAI } from "openai";


export class MyAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const client = new OpenAI({

      apiKey: this.env.GEMINI_API_KEY,

      baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",

    });


    let { readable, writable } = new TransformStream();

    let writer = writable.getWriter();

    const textEncoder = new TextEncoder();


    this.ctx.waitUntil(

      (async () => {

        const stream = await client.chat.completions.create({

          model: "gemini-2.0-flash",

          messages: [

            { role: "user", content: "Write me a Cloudflare Worker." },

          ],

          stream: true,

        });


        for await (const part of stream) {

          writer.write(

            textEncoder.encode(part.choices[0]?.delta?.content || ""),

          );

        }

        writer.close();

      })(),

    );


    return new Response(readable);

  }

}


```

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/using-ai-models/","name":"Using AI Models"}}]}
```

---

---
title: WebSockets
description: Agents support WebSocket connections for real-time, bi-directional communication. This page covers server-side WebSocket handling. For client-side connection, refer to the Client SDK.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/api-reference/websockets.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# WebSockets

Agents support WebSocket connections for real-time, bi-directional communication. This page covers server-side WebSocket handling. For client-side connection, refer to the [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/).

## Lifecycle hooks

Agents have several lifecycle hooks that fire at different points:

| Hook                                        | When called                                               |
| ------------------------------------------- | --------------------------------------------------------- |
| onStart(props?)                             | Once when the agent first starts (before any connections) |
| onRequest(request)                          | When an HTTP request is received (non-WebSocket)          |
| onConnect(connection, ctx)                  | When a new WebSocket connection is established            |
| onMessage(connection, message)              | When a WebSocket message is received                      |
| onClose(connection, code, reason, wasClean) | When a WebSocket connection closes                        |
| onError(connection, error)                  | When a WebSocket error occurs                             |

### `onStart`

`onStart()` is called once when the agent first starts, before any connections are established:

* [  JavaScript ](#tab-panel-2820)
* [  TypeScript ](#tab-panel-2821)

JavaScript

```

export class MyAgent extends Agent {

  async onStart() {

    // Initialize resources

    console.log(`Agent ${this.name} starting...`);


    // Load data from storage

    const savedData = this.sql`SELECT * FROM cache`;

    for (const row of savedData) {

      // Rebuild in-memory state from persistent storage

    }

  }


  onConnect(connection) {

    // By the time connections arrive, onStart has completed

  }

}


```

TypeScript

```

export class MyAgent extends Agent {

  async onStart() {

    // Initialize resources

    console.log(`Agent ${this.name} starting...`);


    // Load data from storage

    const savedData = this.sql`SELECT * FROM cache`;

    for (const row of savedData) {

      // Rebuild in-memory state from persistent storage

    }

  }


  onConnect(connection: Connection) {

    // By the time connections arrive, onStart has completed

  }

}


```

## Handling connections

Define `onConnect` and `onMessage` methods on your Agent to accept WebSocket connections:

* [  JavaScript ](#tab-panel-2826)
* [  TypeScript ](#tab-panel-2827)

JavaScript

```

import { Agent, Connection, ConnectionContext, WSMessage } from "agents";


export class ChatAgent extends Agent {

  async onConnect(connection, ctx) {

    // Connections are automatically accepted

    // Access the original request for auth, headers, cookies

    const url = new URL(ctx.request.url);

    const token = url.searchParams.get("token");


    if (!token) {

      connection.close(4001, "Unauthorized");

      return;

    }


    // Store user info on this connection

    connection.setState({ authenticated: true });

  }


  async onMessage(connection, message) {

    if (typeof message === "string") {

      // Handle text message

      const data = JSON.parse(message);

      connection.send(JSON.stringify({ received: data }));

    }

  }

}


```

TypeScript

```

import { Agent, Connection, ConnectionContext, WSMessage } from "agents";


export class ChatAgent extends Agent {

  async onConnect(connection: Connection, ctx: ConnectionContext) {

    // Connections are automatically accepted

    // Access the original request for auth, headers, cookies

    const url = new URL(ctx.request.url);

    const token = url.searchParams.get("token");


    if (!token) {

      connection.close(4001, "Unauthorized");

      return;

    }


    // Store user info on this connection

    connection.setState({ authenticated: true });

  }


  async onMessage(connection: Connection, message: WSMessage) {

    if (typeof message === "string") {

      // Handle text message

      const data = JSON.parse(message);

      connection.send(JSON.stringify({ received: data }));

    }

  }

}


```

## Connection object

Each connected client has a unique `Connection` object:

| Property/Method       | Type   | Description                           |
| --------------------- | ------ | ------------------------------------- |
| id                    | string | Unique identifier for this connection |
| state                 | State  | Per-connection state object           |
| setState(state)       | void   | Update connection state               |
| send(message)         | void   | Send message to this client           |
| close(code?, reason?) | void   | Close the connection                  |

### Per-connection state

Store data specific to each connection (user info, preferences, etc.):

* [  JavaScript ](#tab-panel-2830)
* [  TypeScript ](#tab-panel-2831)

JavaScript

```

export class ChatAgent extends Agent {

  async onConnect(connection, ctx) {

    const userId = new URL(ctx.request.url).searchParams.get("userId");


    connection.setState({

      userId: userId || "anonymous",

      role: "user",

      joinedAt: Date.now(),

    });

  }


  async onMessage(connection, message) {

    // Access connection-specific state

    console.log(`Message from ${connection.state.userId}`);

  }

}


```

TypeScript

```

interface ConnectionState {

  userId: string;

  role: "admin" | "user";

  joinedAt: number;

}


export class ChatAgent extends Agent {

  async onConnect(

    connection: Connection<ConnectionState>,

    ctx: ConnectionContext,

  ) {

    const userId = new URL(ctx.request.url).searchParams.get("userId");


    connection.setState({

      userId: userId || "anonymous",

      role: "user",

      joinedAt: Date.now(),

    });

  }


  async onMessage(connection: Connection<ConnectionState>, message: WSMessage) {

    // Access connection-specific state

    console.log(`Message from ${connection.state.userId}`);

  }

}


```

## Broadcasting to all clients

Use `this.broadcast()` to send a message to all connected clients:

* [  JavaScript ](#tab-panel-2824)
* [  TypeScript ](#tab-panel-2825)

JavaScript

```

export class ChatAgent extends Agent {

  async onMessage(connection, message) {

    // Broadcast to all connected clients

    this.broadcast(

      JSON.stringify({

        from: connection.id,

        message: message,

        timestamp: Date.now(),

      }),

    );

  }


  // Broadcast from any method

  async notifyAll(event, data) {

    this.broadcast(JSON.stringify({ event, data }));

  }

}


```

TypeScript

```

export class ChatAgent extends Agent {

  async onMessage(connection: Connection, message: WSMessage) {

    // Broadcast to all connected clients

    this.broadcast(

      JSON.stringify({

        from: connection.id,

        message: message,

        timestamp: Date.now(),

      }),

    );

  }


  // Broadcast from any method

  async notifyAll(event: string, data: unknown) {

    this.broadcast(JSON.stringify({ event, data }));

  }

}


```

### Excluding connections

Pass an array of connection IDs to exclude from the broadcast:

* [  JavaScript ](#tab-panel-2818)
* [  TypeScript ](#tab-panel-2819)

JavaScript

```

// Broadcast to everyone except the sender

this.broadcast(

  JSON.stringify({ type: "user-typing", userId: "123" }),

  [connection.id], // Do not send to the originator

);


```

TypeScript

```

// Broadcast to everyone except the sender

this.broadcast(

  JSON.stringify({ type: "user-typing", userId: "123" }),

  [connection.id], // Do not send to the originator

);


```

## Connection tags

Tag connections for easy filtering. Override `getConnectionTags()` to assign tags when a connection is established:

* [  JavaScript ](#tab-panel-2832)
* [  TypeScript ](#tab-panel-2833)

JavaScript

```

export class ChatAgent extends Agent {

  getConnectionTags(connection, ctx) {

    const url = new URL(ctx.request.url);

    const role = url.searchParams.get("role");


    const tags = [];

    if (role === "admin") tags.push("admin");

    if (role === "moderator") tags.push("moderator");


    return tags; // Up to 9 tags, max 256 chars each

  }


  // Later, broadcast only to admins

  notifyAdmins(message) {

    for (const conn of this.getConnections("admin")) {

      conn.send(message);

    }

  }

}


```

TypeScript

```

export class ChatAgent extends Agent {

  getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {

    const url = new URL(ctx.request.url);

    const role = url.searchParams.get("role");


    const tags: string[] = [];

    if (role === "admin") tags.push("admin");

    if (role === "moderator") tags.push("moderator");


    return tags; // Up to 9 tags, max 256 chars each

  }


  // Later, broadcast only to admins

  notifyAdmins(message: string) {

    for (const conn of this.getConnections("admin")) {

      conn.send(message);

    }

  }

}


```

### Connection management methods

| Method            | Signature                               | Description                            |
| ----------------- | --------------------------------------- | -------------------------------------- |
| getConnections    | (tag?: string) => Iterable<Connection>  | Get all connections, optionally by tag |
| getConnection     | (id: string) => Connection \| undefined | Get connection by ID                   |
| getConnectionTags | (connection, ctx) => string\[\]         | Override to tag connections            |
| broadcast         | (message, without?: string\[\]) => void | Send to all connections                |

## Handling binary data

Messages can be strings or binary (`ArrayBuffer` / `ArrayBufferView`):

* [  JavaScript ](#tab-panel-2828)
* [  TypeScript ](#tab-panel-2829)

JavaScript

```

export class FileAgent extends Agent {

  async onMessage(connection, message) {

    if (message instanceof ArrayBuffer) {

      // Handle binary upload

      const bytes = new Uint8Array(message);

      await this.processFile(bytes);

      connection.send(

        JSON.stringify({ status: "received", size: bytes.length }),

      );

    } else if (typeof message === "string") {

      // Handle text command

      const command = JSON.parse(message);

      // ...

    }

  }

}


```

TypeScript

```

export class FileAgent extends Agent {

  async onMessage(connection: Connection, message: WSMessage) {

    if (message instanceof ArrayBuffer) {

      // Handle binary upload

      const bytes = new Uint8Array(message);

      await this.processFile(bytes);

      connection.send(

        JSON.stringify({ status: "received", size: bytes.length }),

      );

    } else if (typeof message === "string") {

      // Handle text command

      const command = JSON.parse(message);

      // ...

    }

  }

}


```

Note

Agents automatically send JSON text frames (identity, state, MCP servers) to every connection. If your client only handles binary data and cannot process these frames, use [shouldSendProtocolMessages](https://developers.cloudflare.com/agents/api-reference/protocol-messages/) to suppress them.

## Error and close handling

Handle connection errors and disconnections:

* [  JavaScript ](#tab-panel-2836)
* [  TypeScript ](#tab-panel-2837)

JavaScript

```

export class ChatAgent extends Agent {

  async onError(connection, error) {

    console.error(`Connection ${connection.id} error:`, error);

    // Clean up any resources for this connection

  }


  async onClose(connection, code, reason, wasClean) {

    console.log(`Connection ${connection.id} closed: ${code} ${reason}`);


    // Notify other clients

    this.broadcast(

      JSON.stringify({

        event: "user-left",

        userId: connection.state?.userId,

      }),

    );

  }

}


```

TypeScript

```

export class ChatAgent extends Agent {

  async onError(connection: Connection, error: unknown) {

    console.error(`Connection ${connection.id} error:`, error);

    // Clean up any resources for this connection

  }


  async onClose(

    connection: Connection,

    code: number,

    reason: string,

    wasClean: boolean,

  ) {

    console.log(`Connection ${connection.id} closed: ${code} ${reason}`);


    // Notify other clients

    this.broadcast(

      JSON.stringify({

        event: "user-left",

        userId: connection.state?.userId,

      }),

    );

  }

}


```

## Message types

| Type            | Description                     |
| --------------- | ------------------------------- |
| string          | Text message (typically JSON)   |
| ArrayBuffer     | Binary data                     |
| ArrayBufferView | Typed array view of binary data |

## Hibernation

Agents support hibernation — they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections.

### Enabling hibernation

Hibernation is enabled by default. To disable:

* [  JavaScript ](#tab-panel-2822)
* [  TypeScript ](#tab-panel-2823)

JavaScript

```

export class AlwaysOnAgent extends Agent {

  static options = { hibernate: false };

}


```

TypeScript

```

export class AlwaysOnAgent extends Agent {

  static options = { hibernate: false };

}


```

### How hibernation works

1. Agent is active, handling connections
2. After a period of inactivity with no messages, the agent hibernates (sleeps)
3. WebSocket connections remain open (handled by Cloudflare)
4. When a message arrives, the agent wakes up
5. `onMessage` is called as normal

### What persists across hibernation

| Persists                 | Does not persist    |
| ------------------------ | ------------------- |
| this.state (agent state) | In-memory variables |
| connection.state         | Timers/intervals    |
| SQLite data (this.sql)   | Promises in flight  |
| Connection metadata      | Local caches        |

Store important data in `this.state` or SQLite, not in class properties:

* [  JavaScript ](#tab-panel-2834)
* [  TypeScript ](#tab-panel-2835)

JavaScript

```

export class MyAgent extends Agent {

  initialState = { counter: 0 };


  // Do not do this - lost on hibernation

  localCounter = 0;


  onMessage(connection, message) {

    // Persists across hibernation

    this.setState({ counter: this.state.counter + 1 });


    // Lost after hibernation

    this.localCounter++;

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env, { counter: number }> {

  initialState = { counter: 0 };


  // Do not do this - lost on hibernation

  private localCounter = 0;


  onMessage(connection: Connection, message: WSMessage) {

    // Persists across hibernation

    this.setState({ counter: this.state.counter + 1 });


    // Lost after hibernation

    this.localCounter++;

  }

}


```

## Common patterns

### Presence tracking

Track who is online using per-connection state. Connection state is automatically cleaned up when users disconnect:

* [  JavaScript ](#tab-panel-2840)
* [  TypeScript ](#tab-panel-2841)

JavaScript

```

export class PresenceAgent extends Agent {

  onConnect(connection, ctx) {

    const url = new URL(ctx.request.url);

    const name = url.searchParams.get("name") || "Anonymous";


    connection.setState({

      name,

      joinedAt: Date.now(),

      lastSeen: Date.now(),

    });


    // Send current presence to new user

    connection.send(

      JSON.stringify({

        type: "presence",

        users: this.getPresence(),

      }),

    );


    // Notify others that someone joined

    this.broadcastPresence();

  }


  onClose(connection) {

    // No manual cleanup needed - connection state is automatically gone

    this.broadcastPresence();

  }


  onMessage(connection, message) {

    if (message === "ping") {

      connection.setState((prev) => ({

        ...prev,

        lastSeen: Date.now(),

      }));

      connection.send("pong");

    }

  }


  getPresence() {

    const users = {};

    for (const conn of this.getConnections()) {

      if (conn.state) {

        users[conn.id] = {

          name: conn.state.name,

          lastSeen: conn.state.lastSeen,

        };

      }

    }

    return users;

  }


  broadcastPresence() {

    this.broadcast(

      JSON.stringify({

        type: "presence",

        users: this.getPresence(),

      }),

    );

  }

}


```

TypeScript

```

type UserState = {

  name: string;

  joinedAt: number;

  lastSeen: number;

};


export class PresenceAgent extends Agent {

  onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {

    const url = new URL(ctx.request.url);

    const name = url.searchParams.get("name") || "Anonymous";


    connection.setState({

      name,

      joinedAt: Date.now(),

      lastSeen: Date.now(),

    });


    // Send current presence to new user

    connection.send(

      JSON.stringify({

        type: "presence",

        users: this.getPresence(),

      }),

    );


    // Notify others that someone joined

    this.broadcastPresence();

  }


  onClose(connection: Connection) {

    // No manual cleanup needed - connection state is automatically gone

    this.broadcastPresence();

  }


  onMessage(connection: Connection<UserState>, message: WSMessage) {

    if (message === "ping") {

      connection.setState((prev) => ({

        ...prev!,

        lastSeen: Date.now(),

      }));

      connection.send("pong");

    }

  }


  private getPresence() {

    const users: Record<string, { name: string; lastSeen: number }> = {};

    for (const conn of this.getConnections<UserState>()) {

      if (conn.state) {

        users[conn.id] = {

          name: conn.state.name,

          lastSeen: conn.state.lastSeen,

        };

      }

    }

    return users;

  }


  private broadcastPresence() {

    this.broadcast(

      JSON.stringify({

        type: "presence",

        users: this.getPresence(),

      }),

    );

  }

}


```

### Chat room with broadcast

* [  JavaScript ](#tab-panel-2838)
* [  TypeScript ](#tab-panel-2839)

JavaScript

```

export class ChatRoom extends Agent {

  onConnect(connection, ctx) {

    const url = new URL(ctx.request.url);

    const username = url.searchParams.get("username") || "Anonymous";


    connection.setState({ username });


    // Notify others

    this.broadcast(

      JSON.stringify({

        type: "join",

        user: username,

        timestamp: Date.now(),

      }),

      [connection.id], // Do not send to the joining user

    );

  }


  onMessage(connection, message) {

    if (typeof message !== "string") return;


    const { username } = connection.state;


    this.broadcast(

      JSON.stringify({

        type: "message",

        user: username,

        text: message,

        timestamp: Date.now(),

      }),

    );

  }


  onClose(connection) {

    const { username } = connection.state || {};

    if (username) {

      this.broadcast(

        JSON.stringify({

          type: "leave",

          user: username,

          timestamp: Date.now(),

        }),

      );

    }

  }

}


```

TypeScript

```

type Message = {

  type: "message" | "join" | "leave";

  user: string;

  text?: string;

  timestamp: number;

};


export class ChatRoom extends Agent {

  onConnect(connection: Connection, ctx: ConnectionContext) {

    const url = new URL(ctx.request.url);

    const username = url.searchParams.get("username") || "Anonymous";


    connection.setState({ username });


    // Notify others

    this.broadcast(

      JSON.stringify({

        type: "join",

        user: username,

        timestamp: Date.now(),

      } satisfies Message),

      [connection.id], // Do not send to the joining user

    );

  }


  onMessage(connection: Connection, message: WSMessage) {

    if (typeof message !== "string") return;


    const { username } = connection.state as { username: string };


    this.broadcast(

      JSON.stringify({

        type: "message",

        user: username,

        text: message,

        timestamp: Date.now(),

      } satisfies Message),

    );

  }


  onClose(connection: Connection) {

    const { username } = (connection.state as { username: string }) || {};

    if (username) {

      this.broadcast(

        JSON.stringify({

          type: "leave",

          user: username,

          timestamp: Date.now(),

        } satisfies Message),

      );

    }

  }

}


```

## Connecting from clients

For browser connections, use the Agents client SDK:

* **Vanilla JS**: `AgentClient` from `agents/client`
* **React**: `useAgent` hook from `agents/react`

Refer to [Client SDK](https://developers.cloudflare.com/agents/api-reference/client-sdk/) for full documentation.

## Next steps

[ State synchronization ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) Sync state between agents and clients. 

[ Callable methods ](https://developers.cloudflare.com/agents/api-reference/callable-methods/) RPC over WebSockets for method calls. 

[ Cross-domain authentication ](https://developers.cloudflare.com/agents/guides/cross-domain-authentication/) Secure WebSocket connections across domains. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/api-reference/","name":"API Reference"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/api-reference/websockets/","name":"WebSockets"}}]}
```

---

---
title: Agent class internals
description: The core of the agents library is the Agent class. You extend it, override a few methods, and get state management, WebSockets, scheduling, RPC, and more for free. This page explains how Agent is built, layer by layer, so you understand what is happening under the hood.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/concepts/agent-class.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Agent class internals

The core of the `agents` library is the `Agent` class. You extend it, override a few methods, and get state management, WebSockets, scheduling, RPC, and more for free. This page explains how `Agent` is built, layer by layer, so you understand what is happening under the hood.

The snippets shown here are illustrative and do not necessarily represent best practices. For the full API, refer to the [API reference](https://developers.cloudflare.com/agents/api-reference/) and the [source code ↗](https://github.com/cloudflare/agents/blob/main/packages/agents/src/index.ts).

## What is the Agent?

The `Agent` class is an extension of `DurableObject` — agents _are_ Durable Objects. If you are not familiar with Durable Objects, read [What are Durable Objects](https://developers.cloudflare.com/durable-objects/) first. At their core, Durable Objects are globally addressable (each instance has a unique ID), single-threaded compute instances with long-term storage (key-value and SQLite).

`Agent` does not extend `DurableObject` directly. It extends `Server` from the [partyserver ↗](https://github.com/cloudflare/partykit/tree/main/packages/partyserver) package, which extends `DurableObject`. Think of it as layers: **DurableObject** \> **Server** \> **Agent**.

## Layer 0: Durable Object

Let's briefly consider which primitives are exposed by Durable Objects so we understand how the outer layers make use of them. The Durable Object class comes with:

### `constructor`

TypeScript

```

constructor(ctx: DurableObjectState, env: Env) {}


```

The Workers runtime always calls the constructor to handle things internally. This means two things:

1. While the constructor is called every time the Durable Object is initialized, the signature is fixed. Developers cannot add or update parameters from the constructor.
2. Instead of instantiating the class manually, developers must use the binding APIs and do it through the [DurableObjectNamespace](https://developers.cloudflare.com/durable-objects/api/namespace/).

### RPC

By writing a Durable Object class which inherits from the built-in type `DurableObject`, public methods are exposed as RPC methods, which developers can call using a [DurableObjectStub from a Worker](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoking-methods-on-a-durable-object).

TypeScript

```

// This instance could've been active, hibernated,

// not initialized or maybe had never even been created!

const stub = env.MY_DO.getByName("foo");


// We can call any public method on the class. The runtime

// ensures the constructor is called if the instance was not active.

await stub.bar();


```

### `fetch()`

Durable Objects can take a `Request` from a Worker and send a `Response` back. This can only be done through the [fetch](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoking-the-fetch-handler) method (which the developer must implement).

### WebSockets

Durable Objects include first-class support for [WebSockets](https://developers.cloudflare.com/durable-objects/best-practices/websockets/). A Durable Object can accept a WebSocket it receives from a `Request` in `fetch` and forget about it. The base class provides methods that developers can implement that are called as callbacks. They effectively replace the need for event listeners.

The base class provides `webSocketMessage(ws, message)`, `webSocketClose(ws, code, reason, wasClean)` and `webSocketError(ws , error)` ([API](https://developers.cloudflare.com/workers/runtime-apis/websockets)).

TypeScript

```

export class MyDurableObject extends DurableObject {

  async fetch(request) {

    // Creates two ends of a WebSocket connection.

    const webSocketPair = new WebSocketPair();

    const [client, server] = Object.values(webSocketPair);


    // Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.

    this.ctx.acceptWebSocket(server);


    return new Response(null, {

      status: 101,

      webSocket: client,

    });

  }


  async webSocketMessage(ws, message) {

    ws.send(message);

  }

}


```

### `alarm()`

HTTP and RPC requests are not the only entrypoints for a Durable Object. Alarms allow developers to schedule an event to trigger at a later time. Whenever the next alarm is due, the runtime will call the `alarm()` method, which is left to the developer to implement.

To schedule an alarm, you can use the `this.ctx.storage.setAlarm()` method. For more information, refer to [Alarms](https://developers.cloudflare.com/durable-objects/api/alarms/).

### `this.ctx`

The base `DurableObject` class sets the [DurableObjectState](https://developers.cloudflare.com/durable-objects/api/state/) into `this.ctx`. There are a lot of interesting methods and properties, but we will focus on `this.ctx.storage`.

### `this.ctx.storage`

[DurableObjectStorage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) is the main interface with the Durable Object's persistence mechanisms, which include both a KV and SQLITE **synchronous** APIs.

TypeScript

```

const sql = this.ctx.storage.sql;


// Synchronous SQL query

const rows = sql.exec("SELECT * FROM contacts WHERE country = ?", "US");


// Key-value storage

const token = this.ctx.storage.get("someToken");


```

### `this.ctx.env`

Lastly, it is worth mentioning that the Durable Object also has the Worker `Env` in `this.env`. Learn more in [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings).

## Layer 1: `Server` (partyserver)

Now that you have seen what Durable Objects provide out of the box, the `Server` class from [partyserver ↗](https://github.com/cloudflare/partykit/tree/main/packages/partyserver) will make more sense. It is an opinionated `DurableObject` wrapper that replaces low-level primitives with developer-friendly callbacks.

`Server` does not add any storage operations of its own — it only wraps the Durable Object lifecycle.

### Addressing

`partyserver` exposes helpers to address Durable Objects by name instead of going through bindings manually. This includes a URL routing scheme (`<your-worker>/servers/:durableClass/:durableName`) that the Agent layer builds on.

TypeScript

```

// Note the await here!

const stub = await getServerByName(env.MY_DO, "foo");


// We can still call RPC methods.

await stub.bar();


```

The URL scheme also enables a request router. In the Agent layer, this is re-exported as `routeAgentRequest`:

TypeScript

```

  async fetch(request: Request, env: Env, ctx: ExecutionContext) {

    const res = await routeAgentRequest(request, env);


    if (res) return res;


    return new Response("Not found", { status: 404 });

  }


```

### `onStart`

The addressing layer allows `Server` to expose an `onStart` callback that runs every time the Durable Object starts up (after eviction, hibernation, or first creation) and before any `fetch` or RPC call.

TypeScript

```

class MyServer extends Server {

  onStart() {

    // Some initialization logic that you wish

    // to run every time the DO is started up.

    const sql = this.ctx.storage.sql;

    sql.exec(`...`);

  }

}


```

### `onRequest` and `onConnect`

`Server` already implements `fetch` for the underlying Durable Object and exposes two different callbacks that developers can make use of, `onRequest` and `onConnect` for HTTP requests and incoming WS connections, respectively (WebSocket connections are accepted by default).

TypeScript

```

class MyServer extends Server {

  async onRequest(request: Request) {

    const url = new URL(request.url);


    return new Response(`Hello from ${url.origin}!`);

  }


  async onConnect(conn, ctx) {

    const { request } = ctx;

    const url = new URL(request.url);


    // Connections are a WebSocket wrapper

    conn.send(`Hello from ${url.origin}!`);

  }

}


```

### WebSockets

Just as `onConnect` is the callback for every new connection, `Server` also provides wrappers on top of the default callbacks from the `DurableObject` class: `onMessage`, `onClose` and `onError`.

There's also `this.broadcast` that sends a WS message to all connected clients (no magic, just a loop over `this.getConnections()`!).

### `this.name`

It is hard to get a Durable Object's `name` from within it. `partyserver` tries to make it available in `this.name` but it is not a perfect solution. Learn more about it in [this GitHub issue ↗](https://github.com/cloudflare/workerd/issues/2240).

## Layer 2: Agent

Now finally, the `Agent` class. `Agent` extends `Server` and provides opinionated primitives for stateful, schedulable, and observable agents that can communicate via RPC, WebSockets, and (even!) email.

### `this.state` and `this.setState()`

One of the core features of `Agent` is **automatic state persistence**. Developers define the shape of their state via the generic parameter and `initialState` (which is only used if no state exists in storage), and the Agent handles loading, saving, and broadcasting state changes (check `Server`'s `this.broadcast()` above).

`this.state` is a getter that lazily loads state from storage (SQL). State is persisted across Durable Object evictions when it is updated with `this.setState()`, which automatically serializes the state and writes it back to storage.

There's also `this.onStateChanged` that you can override to react to state changes.

TypeScript

```

class MyAgent extends Agent<Env, { count: number }> {

  initialState = { count: 0 };


  increment() {

    this.setState({ count: this.state.count + 1 });

  }


  onStateChanged(state, source) {

    console.log("State updated:", state);

  }

}


```

State is stored in the `cf_agents_state` SQL table. State messages are sent with `type: "cf_agent_state"` (both from the client and the server). Since `agents` provides [JS and React clients](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/#synchronizing-state), real-time state updates are available out of the box.

### `this.sql`

The Agent provides a convenient `sql` template tag for executing queries against the Durable Object's SQL storage. It constructs parameterized queries and executes them. This uses the **synchronous** SQL API from `this.ctx.storage.sql`.

TypeScript

```

class MyAgent extends Agent {

  onStart() {

    this.sql`

      CREATE TABLE IF NOT EXISTS users (

        id TEXT PRIMARY KEY,

        name TEXT

      )

    `;


    const userId = "1";

    const userName = "Alice";

    this.sql`INSERT INTO users (id, name) VALUES (${userId}, ${userName})`;


    const users = this.sql<{ id: string; name: string }>`

      SELECT * FROM users WHERE id = ${userId}

    `;

    console.log(users); // [{ id: "1", name: "Alice" }]

  }

}


```

### RPC and Callable Methods

`agents` takes Durable Objects RPC one step further by implementing RPC through WebSockets, so clients can call methods on the Agent directly. To make a method callable through WebSocket, use the `@callable()` decorator. Methods can return a serializable value or a stream (when using `@callable({ stream: true })`).

TypeScript

```

class MyAgent extends Agent {

  @callable({ description: "Add two numbers" })

  async add(a: number, b: number) {

    return a + b;

  }

}


```

Clients can invoke this method by sending a WebSocket message:

```

{

  "type": "rpc",

  "id": "unique-request-id",

  "method": "add",

  "args": [2, 3]

}


```

For example, with the provided `React` client, it is as easy as:

TypeScript

```

const { stub } = useAgent({ name: "my-agent" });

const result = await stub.add(2, 3);

console.log(result); // 5


```

### `this.queue` and friends

Agents include a built-in task queue for deferred execution. This is useful for offloading work or retrying operations. The available methods are `this.queue`, `this.dequeue`, `this.dequeueAll`, `this.dequeueAllByCallback`, `this.getQueue`, and `this.getQueues`.

TypeScript

```

class MyAgent extends Agent {

  async onConnect() {

    // Queue a task to be executed later

    await this.queue("processTask", { userId: "123" });

  }


  async processTask(payload: { userId: string }, queueItem: QueueItem) {

    console.log("Processing task for user:", payload.userId);

  }

}


```

Tasks are stored in the `cf_agents_queues` SQL table and are automatically flushed in sequence. If a task succeeds, it is automatically dequeued.

### `this.schedule` and friends

Agents support scheduled execution of methods by wrapping the Durable Object's `alarm()`. The available methods are `this.schedule`, `this.getSchedule`, `this.getSchedules`, `this.cancelSchedule`. Schedules can be one-time, delayed, or recurring (using cron expressions).

Since Durable Objects only allow one alarm at a time, the `Agent` class works around this by managing multiple schedules in SQL and using a single alarm.

TypeScript

```

class MyAgent extends Agent {

  async foo() {

    // Schedule at a specific time

    await this.schedule(new Date("2025-12-25T00:00:00Z"), "sendGreeting", {

      message: "Merry Christmas!",

    });


    // Schedule with a delay (in seconds)

    await this.schedule(60, "checkStatus", { check: "health" });


    // Schedule with a cron expression

    await this.schedule("0 0 * * *", "dailyTask", { type: "cleanup" });

  }


  async sendGreeting(payload: { message: string }) {

    console.log(payload.message);

  }


  async checkStatus(payload: { check: string }) {

    console.log("Running check:", payload.check);

  }


  async dailyTask(payload: { type: string }) {

    console.log("Daily task:", payload.type);

  }

}


```

Schedules are stored in the `cf_agents_schedules` SQL table. Cron schedules automatically reschedule themselves after execution, while one-time schedules are deleted.

### `this.mcp` and friends

`Agent` includes a multi-server MCP client. This enables your Agent to interact with external services that expose MCP interfaces. The MCP client is properly documented in [MCP client API](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/).

TypeScript

```

class MyAgent extends Agent {

  async onStart() {

    // Add an HTTP MCP server (callbackHost only needed for OAuth servers)

    await this.addMcpServer("GitHub", "https://mcp.github.com/mcp", {

      callbackHost: "https://my-worker.example.workers.dev",

    });


    // Add an MCP server via RPC (Durable Object binding, no HTTP overhead)

    await this.addMcpServer("internal-tools", this.env.MyMCP);

  }

}


```

### Email Handling

Agents can receive and reply to emails using Cloudflare's [Email Routing](https://developers.cloudflare.com/email-routing/email-workers/).

TypeScript

```

class MyAgent extends Agent {

  async onEmail(email: AgentEmail) {

    console.log("Received email from:", email.from);

    console.log("Subject:", email.headers.get("subject"));


    const raw = await email.getRaw();

    console.log("Raw email size:", raw.length);


    // Reply to the email

    await this.replyToEmail(email, {

      fromName: "My Agent",

      subject: "Re: " + email.headers.get("subject"),

      body: "Thanks for your email!",

      contentType: "text/plain",

    });

  }

}


```

To route emails to your Agent, use `routeAgentEmail` in your Worker's email handler:

TypeScript

```

export default {

  async email(message, env, ctx) {

    await routeAgentEmail(message, env, {

      resolver: createAddressBasedEmailResolver("my-agent"),

    });

  },

} satisfies ExportedHandler<Env>;


```

### Context Management

`agents` wraps all your methods with an `AsyncLocalStorage` to maintain context throughout the request lifecycle. This allows you to access the current agent, connection, request, or email (depending on what event is being handled) from anywhere in your code:

TypeScript

```

import { getCurrentAgent } from "agents";


function someUtilityFunction() {

  const { agent, connection, request, email } = getCurrentAgent();


  if (agent) {

    console.log("Current agent:", agent.name);

  }


  if (connection) {

    console.log("WebSocket connection ID:", connection.id);

  }

}


```

### `this.onError`

`Agent` extends `Server`'s `onError` so it can be used to handle errors that are not necessarily WebSocket errors. It is called with a `Connection` or `unknown` error.

TypeScript

```

class MyAgent extends Agent {

  onError(connectionOrError: Connection | unknown, error?: unknown) {

    if (error) {

      // WebSocket connection error

      console.error("Connection error:", error);

    } else {

      // Server error

      console.error("Server error:", connectionOrError);

    }


    // Optionally throw to propagate the error

    throw connectionOrError;

  }

}


```

### `this.destroy`

`this.destroy()` drops all tables, deletes alarms, clears storage, and aborts the context. To ensure that the Durable Object is fully evicted, `this.ctx.abort()` is called asynchronously using `setTimeout()` to allow any currently executing handlers (like scheduled tasks) to complete their cleanup operations before the context is aborted.

This means `this.ctx.abort()` throws an uncatchable error that will show up in your logs, but it does so after yielding to the event loop (read more about it in [abort()](https://developers.cloudflare.com/durable-objects/api/state/#abort)).

The `destroy()` method can be safely called within scheduled tasks. When called from within a schedule callback, the Agent sets an internal flag to skip any remaining database updates, and yields `ctx.abort()` to the event loop to ensure the alarm handler completes cleanly before the Agent is evicted.

TypeScript

```

class MyAgent extends Agent {

  async onStart() {

    console.log("Agent is starting up...");

    // Initialize your agent

  }


  async cleanup() {

    // This wipes everything!

    await this.destroy();

  }


  async selfDestruct() {

    // Safe to call from within a scheduled task

    await this.schedule(60, "destroyAfterDelay", {});

  }


  async destroyAfterDelay() {

    // This will safely destroy the Agent even when

    // called from within the alarm handler

    await this.destroy();

  }

}


```

Using destroy() in scheduled tasks

You can safely call `this.destroy()` from within a scheduled task callback. The Agent SDK sets an internal flag to prevent database updates after destruction and defers the context abort to ensure the alarm handler completes cleanly.

### Routing

The `Agent` class re-exports the [addressing helpers](#addressing) as `getAgentByName` and `routeAgentRequest`.

TypeScript

```

const stub = await getAgentByName(env.MY_DO, "foo");

await stub.someMethod();


const res = await routeAgentRequest(request, env);

if (res) return res;


return new Response("Not found", { status: 404 });


```

## Layer 3: `AIChatAgent`

The [AIChatAgent](https://developers.cloudflare.com/agents/api-reference/chat-agents/) class from `@cloudflare/ai-chat` extends `Agent` with an opinionated layer for AI chat. It adds automatic message persistence to SQLite, resumable streaming, tool support (server-side, client-side, and human-in-the-loop), and a React hook (`useAgentChat`) for building chat UIs.

The full hierarchy is: **DurableObject** \> **Server** \> **Agent** \> **AIChatAgent**.

If you are building a chat agent, start with `AIChatAgent`. If you need lower-level control or are not building a chat interface, use `Agent` directly.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/concepts/agent-class/","name":"Agent class internals"}}]}
```

---

---
title: Calling LLMs
description: Agents change how you work with LLMs. In a stateless Worker, every request starts from scratch — you reconstruct context, call a model, return the response, and forget everything. An Agent keeps state between calls, stays connected to clients over WebSocket, and can call models on its own schedule without a user present.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ AI ](https://developers.cloudflare.com/search/?tags=AI) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/concepts/calling-llms.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Calling LLMs

Agents change how you work with LLMs. In a stateless Worker, every request starts from scratch — you reconstruct context, call a model, return the response, and forget everything. An Agent keeps state between calls, stays connected to clients over WebSocket, and can call models on its own schedule without a user present.

This page covers the patterns that become possible when your LLM calls happen inside a stateful Agent. For provider setup and code examples, refer to [Using AI Models](https://developers.cloudflare.com/agents/api-reference/using-ai-models/).

## State as context

Every Agent has a built-in [SQL database](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) and key-value state. Instead of passing an entire conversation history from the client on every request, the Agent stores it and builds prompts from its own storage.

* [  JavaScript ](#tab-panel-2842)
* [  TypeScript ](#tab-panel-2843)

JavaScript

```

import { Agent } from "agents";


export class ResearchAgent extends Agent {

  async buildPrompt(userMessage) {

    const history = this.sql`

      SELECT role, content FROM messages

      ORDER BY timestamp DESC LIMIT 50`;


    const preferences = this.sql`

      SELECT key, value FROM user_preferences`;


    return [

      { role: "system", content: this.systemPrompt(preferences) },

      ...history.reverse(),

      { role: "user", content: userMessage },

    ];

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class ResearchAgent extends Agent<Env> {

  async buildPrompt(userMessage: string) {

    const history = this.sql<{ role: string; content: string }>`

      SELECT role, content FROM messages

      ORDER BY timestamp DESC LIMIT 50`;


    const preferences = this.sql<{ key: string; value: string }>`

      SELECT key, value FROM user_preferences`;


    return [

      { role: "system", content: this.systemPrompt(preferences) },

      ...history.reverse(),

      { role: "user", content: userMessage },

    ];

  }

}


```

This means the client does not need to send the full conversation on every message. The Agent owns the history, can prune it, enrich it with retrieved documents, or summarize older turns before sending to the model.

## Surviving disconnections

Reasoning models like DeepSeek R1 or GLM-4 can take 30 seconds to several minutes to respond. In a stateless request-response architecture, the client must stay connected the entire time. If the connection drops, the response is lost.

An Agent keeps running after the client disconnects. When the response arrives, the Agent can persist it to state and deliver it when the client reconnects — even hours or days later.

* [  JavaScript ](#tab-panel-2844)
* [  TypeScript ](#tab-panel-2845)

JavaScript

```

import { Agent } from "agents";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class MyAgent extends Agent {

  async onMessage(connection, message) {

    const { prompt } = JSON.parse(message);

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt,

    });


    for await (const chunk of result.textStream) {

      connection.send(JSON.stringify({ type: "chunk", content: chunk }));

    }


    this.sql`INSERT INTO responses (prompt, response, timestamp)

      VALUES (${prompt}, ${await result.text}, ${Date.now()})`;

  }

}


```

TypeScript

```

import { Agent } from "agents";

import { streamText } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class MyAgent extends Agent<Env> {

  async onMessage(connection: Connection, message: WSMessage) {

    const { prompt } = JSON.parse(message as string);

    const workersai = createWorkersAI({ binding: this.env.AI });


    const result = streamText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt,

    });


    for await (const chunk of result.textStream) {

      connection.send(JSON.stringify({ type: "chunk", content: chunk }));

    }


    this.sql`INSERT INTO responses (prompt, response, timestamp)

      VALUES (${prompt}, ${await result.text}, ${Date.now()})`;

  }

}


```

With [AIChatAgent](https://developers.cloudflare.com/agents/api-reference/chat-agents/), this is handled automatically — messages are persisted to SQLite and streams resume on reconnect.

## Autonomous model calls

Agents do not need a user request to call a model. You can schedule model calls to run in the background — for nightly summarization, periodic classification, monitoring, or any task that should happen without human interaction.

* [  JavaScript ](#tab-panel-2846)
* [  TypeScript ](#tab-panel-2847)

JavaScript

```

import { Agent } from "agents";


export class DigestAgent extends Agent {

  async onStart() {

    this.schedule("0 8 * * *", "generateDailyDigest", {});

  }


  async generateDailyDigest() {

    const articles = this.sql`

      SELECT title, body FROM articles

      WHERE created_at > datetime('now', '-1 day')`;


    const workersai = createWorkersAI({ binding: this.env.AI });

    const { text } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: `Summarize these articles:\n${articles.map((a) => a.title + ": " + a.body).join("\n\n")}`,

    });


    this.sql`INSERT INTO digests (summary, created_at)

      VALUES (${text}, ${Date.now()})`;


    this.broadcast(JSON.stringify({ type: "digest", summary: text }));

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class DigestAgent extends Agent<Env> {

  async onStart() {

    this.schedule("0 8 * * *", "generateDailyDigest", {});

  }


  async generateDailyDigest() {

    const articles = this.sql<{ title: string; body: string }>`

      SELECT title, body FROM articles

      WHERE created_at > datetime('now', '-1 day')`;


    const workersai = createWorkersAI({ binding: this.env.AI });

    const { text } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: `Summarize these articles:\n${articles.map((a) => a.title + ": " + a.body).join("\n\n")}`,

    });


    this.sql`INSERT INTO digests (summary, created_at)

      VALUES (${text}, ${Date.now()})`;


    this.broadcast(JSON.stringify({ type: "digest", summary: text }));

  }

}


```

## Multi-model pipelines

Because an Agent maintains state across calls, you can chain multiple models in a single method — using a fast model for classification, a reasoning model for planning, and an embedding model for retrieval — without losing context between steps.

* [  JavaScript ](#tab-panel-2850)
* [  TypeScript ](#tab-panel-2851)

JavaScript

```

import { Agent } from "agents";

import { generateText, embed } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class TriageAgent extends Agent {

  async triage(ticket) {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const { text: category } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: `Classify this support ticket into one of: billing, technical, account. Ticket: ${ticket}`,

    });


    const { embedding } = await embed({

      model: workersai("@cf/baai/bge-base-en-v1.5"),

      value: ticket,

    });

    const similar = await this.env.VECTOR_DB.query(embedding, { topK: 5 });


    const { text: response } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: `Draft a response for this ${category} ticket. Similar resolved tickets: ${JSON.stringify(similar)}. Ticket: ${ticket}`,

    });


    this.sql`INSERT INTO tickets (content, category, response, created_at)

      VALUES (${ticket}, ${category}, ${response}, ${Date.now()})`;


    return { category, response };

  }

}


```

TypeScript

```

import { Agent } from "agents";

import { generateText, embed } from "ai";

import { createWorkersAI } from "workers-ai-provider";


export class TriageAgent extends Agent<Env> {

  async triage(ticket: string) {

    const workersai = createWorkersAI({ binding: this.env.AI });


    const { text: category } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: `Classify this support ticket into one of: billing, technical, account. Ticket: ${ticket}`,

    });


    const { embedding } = await embed({

      model: workersai("@cf/baai/bge-base-en-v1.5"),

      value: ticket,

    });

    const similar = await this.env.VECTOR_DB.query(embedding, { topK: 5 });


    const { text: response } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt: `Draft a response for this ${category} ticket. Similar resolved tickets: ${JSON.stringify(similar)}. Ticket: ${ticket}`,

    });


    this.sql`INSERT INTO tickets (content, category, response, created_at)

      VALUES (${ticket}, ${category}, ${response}, ${Date.now()})`;


    return { category, response };

  }

}


```

Each intermediate result stays in the Agent's memory for the duration of the method, and the final result is persisted to SQL for future reference.

## Caching and cost control

Persistent storage means you can cache model responses and avoid redundant calls. This is especially useful for expensive operations like embeddings or long reasoning chains.

* [  JavaScript ](#tab-panel-2848)
* [  TypeScript ](#tab-panel-2849)

JavaScript

```

import { Agent } from "agents";


export class CachingAgent extends Agent {

  async cachedGenerate(prompt) {

    const cached = this.sql`

      SELECT response FROM llm_cache WHERE prompt = ${prompt}`;


    if (cached.length > 0) {

      return cached[0].response;

    }


    const workersai = createWorkersAI({ binding: this.env.AI });

    const { text } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt,

    });


    this.sql`INSERT INTO llm_cache (prompt, response, created_at)

      VALUES (${prompt}, ${text}, ${Date.now()})`;


    return text;

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class CachingAgent extends Agent<Env> {

  async cachedGenerate(prompt: string) {

    const cached = this.sql<{ response: string }>`

      SELECT response FROM llm_cache WHERE prompt = ${prompt}`;


    if (cached.length > 0) {

      return cached[0].response;

    }


    const workersai = createWorkersAI({ binding: this.env.AI });

    const { text } = await generateText({

      model: workersai("@cf/zai-org/glm-4.7-flash"),

      prompt,

    });


    this.sql`INSERT INTO llm_cache (prompt, response, created_at)

      VALUES (${prompt}, ${text}, ${Date.now()})`;


    return text;

  }

}


```

For provider-level caching and rate limit management across multiple agents, use [AI Gateway](https://developers.cloudflare.com/ai-gateway/).

## Next steps

[ Using AI Models ](https://developers.cloudflare.com/agents/api-reference/using-ai-models/) Provider setup, streaming, and code examples for Workers AI, OpenAI, Anthropic, and more. 

[ Chat agents ](https://developers.cloudflare.com/agents/api-reference/chat-agents/) AIChatAgent handles message persistence, resumable streaming, and tools automatically. 

[ Store and sync state ](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/) SQL database and key-value state APIs for building context and caching. 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Run autonomous model calls on a delay, schedule, or cron. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/concepts/calling-llms/","name":"Calling LLMs"}}]}
```

---

---
title: Human in the Loop
description: Human-in-the-Loop (HITL) workflows integrate human judgment and oversight into automated processes. These workflows pause at critical points for human review, validation, or decision-making before proceeding.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/concepts/human-in-the-loop.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Human in the Loop

Human-in-the-Loop (HITL) workflows integrate human judgment and oversight into automated processes. These workflows pause at critical points for human review, validation, or decision-making before proceeding.

## Why human-in-the-loop?

* **Compliance**: Regulatory requirements may mandate human approval for certain actions.
* **Safety**: High-stakes operations (payments, deletions, external communications) need oversight.
* **Quality**: Human review catches errors AI might miss.
* **Trust**: Users feel more confident when they can approve critical actions.

### Common use cases

| Use Case            | Example                              |
| ------------------- | ------------------------------------ |
| Financial approvals | Expense reports, payment processing  |
| Content moderation  | Publishing, email sending            |
| Data operations     | Bulk deletions, exports              |
| AI tool execution   | Confirming tool calls before running |
| Access control      | Granting permissions, role changes   |

## Patterns for human-in-the-loop

Cloudflare provides two main patterns for implementing human-in-the-loop:

### Workflow approval

For durable, multi-step processes with approval gates that can wait hours, days, or weeks. Use [Cloudflare Workflows](https://developers.cloudflare.com/workflows/) with the `waitForApproval()` method.

**Key APIs:**

* `waitForApproval(step, { timeout })` — Pause workflow until approved
* `approveWorkflow(instanceId, options)` — Approve a waiting workflow
* `rejectWorkflow(instanceId, options)` — Reject a waiting workflow

**Best for:** Expense approvals, content publishing pipelines, data export requests

### MCP elicitation

For MCP servers that need to request additional structured input from users during tool execution. The MCP client renders a form based on your JSON Schema.

**Key API:**

* `elicitInput(options, context)` — Request structured input from the user

**Best for:** Interactive tool confirmations, gathering additional parameters mid-execution

## How workflows handle approvals

![A human-in-the-loop diagram](https://developers.cloudflare.com/_astro/human-in-the-loop.C2xls7fV_ZMwbba.svg) 

In a workflow-based approval:

1. The workflow reaches an approval step and calls `waitForApproval()`
2. The workflow pauses and reports progress to the agent
3. The agent updates its state with the pending approval
4. Connected clients see the pending approval and can approve or reject
5. When approved, the workflow resumes with the approval metadata
6. If rejected or timed out, the workflow handles the rejection appropriately

## Best practices

### Long-term state persistence

Human review processes do not operate on predictable timelines. A reviewer might need days or weeks to make a decision, especially for complex cases requiring additional investigation or multiple approvals. Your system needs to maintain perfect state consistency throughout this period, including:

* The original request and context
* All intermediate decisions and actions
* Any partial progress or temporary states
* Review history and feedback

Tip

[Durable Objects](https://developers.cloudflare.com/durable-objects/) provide an ideal solution for managing state in Human-in-the-Loop workflows, offering persistent compute instances that maintain state for hours, weeks, or months.

### Timeouts and escalation

Set timeouts to prevent workflows from waiting indefinitely. Use [scheduling](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) to:

* Send reminders after a period of inactivity
* Escalate to managers or alternative approvers
* Auto-reject or auto-approve based on business rules

### Audit trails

Maintain immutable audit logs of all approval decisions using the [SQL API](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/). Record:

* Who made the decision
* When the decision was made
* The reason or justification
* Any relevant metadata

### Continuous improvement

Human reviewers play a crucial role in evaluating and improving LLM performance. Implement a systematic evaluation process where human feedback is collected not just on the final output, but on the LLM's decision-making process:

* **Decision quality assessment**: Have reviewers evaluate the LLM's reasoning process and decision points.
* **Edge case identification**: Use human expertise to identify scenarios where performance could be improved.
* **Feedback collection**: Gather structured feedback that can be used to fine-tune the LLM. [AI Gateway](https://developers.cloudflare.com/ai-gateway/evaluations/add-human-feedback/) can help set up an LLM feedback loop.

### Error handling and recovery

Robust error handling is essential for maintaining workflow integrity. Your system should gracefully handle:

* Reviewer unavailability
* System outages
* Conflicting reviews
* Timeout expiration

Implement clear escalation paths for exceptional cases and automatic checkpointing that allows workflows to resume from the last stable state after any interruption.

## Next steps

[ Human-in-the-loop patterns ](https://developers.cloudflare.com/agents/guides/human-in-the-loop/) Implementation examples for approval flows. 

[ Run Workflows ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Complete API for workflow approvals. 

[ MCP elicitation ](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/#elicitation-human-in-the-loop) Interactive input from MCP clients. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/concepts/human-in-the-loop/","name":"Human in the Loop"}}]}
```

---

---
title: Tools
description: Tools enable AI systems to interact with external services and perform actions. They provide a structured way for agents and workflows to invoke APIs, manipulate data, and integrate with external systems. Tools form the bridge between AI decision-making capabilities and real-world actions.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/concepts/tools.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Tools

### What are tools?

Tools enable AI systems to interact with external services and perform actions. They provide a structured way for agents and workflows to invoke APIs, manipulate data, and integrate with external systems. Tools form the bridge between AI decision-making capabilities and real-world actions.

### Understanding tools

In an AI system, tools are typically implemented as function calls that the AI can use to accomplish specific tasks. For example, a travel booking agent might have tools for:

* Searching flight availability
* Checking hotel rates
* Processing payments
* Sending confirmation emails

Each tool has a defined interface specifying its inputs, outputs, and expected behavior. This allows the AI system to understand when and how to use each tool appropriately.

### Common tool patterns

#### API integration tools

The most common type of tools are those that wrap external APIs. These tools handle the complexity of API authentication, request formatting, and response parsing, presenting a clean interface to the AI system.

#### Model Context Protocol (MCP)

The [Model Context Protocol ↗](https://modelcontextprotocol.io/introduction) provides a standardized way to define and interact with tools. Think of it as an abstraction on top of APIs designed for LLMs to interact with external resources. MCP defines a consistent interface for:

* **Tool Discovery**: Systems can dynamically discover available tools
* **Parameter Validation**: Tools specify their input requirements using JSON Schema
* **Error Handling**: Standardized error reporting and recovery
* **State Management**: Tools can maintain state across invocations

#### Data processing tools

Tools that handle data transformation and analysis are essential for many AI workflows. These might include:

* CSV parsing and analysis
* Image processing
* Text extraction
* Data validation

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/concepts/tools/","name":"Tools"}}]}
```

---

---
title: What are agents?
description: An agent is an AI system that can autonomously execute tasks by making decisions about tool usage and process flow. Unlike traditional automation that follows predefined paths, agents can dynamically adapt their approach based on context and intermediate results. Agents are also distinct from co-pilots (such as traditional chat applications) in that they can fully automate a task, as opposed to simply augmenting and extending human input.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ AI ](https://developers.cloudflare.com/search/?tags=AI)[ LLM ](https://developers.cloudflare.com/search/?tags=LLM) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/concepts/what-are-agents.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# What are agents?

An agent is an AI system that can autonomously execute tasks by making decisions about tool usage and process flow. Unlike traditional automation that follows predefined paths, agents can dynamically adapt their approach based on context and intermediate results. Agents are also distinct from co-pilots (such as traditional chat applications) in that they can fully automate a task, as opposed to simply augmenting and extending human input.

* **Agents** → non-linear, non-deterministic (can change from run to run)
* **Workflows** → linear, deterministic execution paths
* **Co-pilots** → augmentative AI assistance requiring human intervention

## Example: Booking vacations

If this is your first time working with or interacting with agents, this example illustrates how an agent works within a context like booking a vacation.

Imagine you are trying to book a vacation. You need to research flights, find hotels, check restaurant reviews, and keep track of your budget.

### Traditional workflow automation

A traditional automation system follows a predetermined sequence:

* Takes specific inputs (dates, location, budget)
* Calls predefined API endpoints in a fixed order
* Returns results based on hardcoded criteria
* Cannot adapt if unexpected situations arise
![Traditional workflow automation diagram](https://developers.cloudflare.com/_astro/workflow-automation.D1rsykgR_Z1dw1Js.svg) 

### AI Co-pilot

A co-pilot acts as an intelligent assistant that:

* Provides hotel and itinerary recommendations based on your preferences
* Can understand and respond to natural language queries
* Offers guidance and suggestions
* Requires human decision-making and action for execution
![A co-pilot diagram](https://developers.cloudflare.com/_astro/co-pilot.BZ_kRuK6_Z2sKyKr.svg) 

### Agent

An agent combines AI's ability to make judgments and call the relevant tools to execute the task. An agent's output will be nondeterministic given:

* Real-time availability and pricing changes
* Dynamic prioritization of constraints
* Ability to recover from failures
* Adaptive decision-making based on intermediate results
![An agent diagram](https://developers.cloudflare.com/_astro/agent-workflow.5VDKtHdO_Z1Hdwi1.svg) 

An agent can dynamically generate an itinerary and execute on booking reservations, similarly to what you would expect from a travel agent.

## Components of agent systems

Agent systems typically have three primary components:

* **Decision Engine**: Usually an LLM (Large Language Model) that determines action steps
* **Tool Integration**: APIs, functions, and services the agent can utilize — often via [MCP](https://developers.cloudflare.com/agents/model-context-protocol/)
* **Memory System**: Maintains context and tracks task progress

### How agents work

Agents operate in a continuous loop of:

1. **Observing** the current state or task
2. **Planning** what actions to take, using AI for reasoning
3. **Executing** those actions using available tools
4. **Learning** from the results (storing results in memory, updating task progress, and preparing for next iteration)

## Building agents on Cloudflare

The Cloudflare Agents SDK provides the infrastructure for building production agents:

* **Persistent state** — Each agent instance has its own SQLite database for storing context and memory
* **Real-time sync** — State changes automatically broadcast to all connected clients via WebSockets
* **Hibernation** — Agents sleep when idle and wake on demand, so you only pay for what you use
* **Global edge deployment** — Agents run close to your users on Cloudflare's network
* **Built-in capabilities** — Scheduling, task queues, workflows, email handling, and more

## Next steps

[ Quick start ](https://developers.cloudflare.com/agents/getting-started/quick-start/) Build your first agent in 10 minutes. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ Using AI models ](https://developers.cloudflare.com/agents/api-reference/using-ai-models/) Integrate OpenAI, Anthropic, and other providers. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/concepts/what-are-agents/","name":"What are agents?"}}]}
```

---

---
title: Workflows
description: Cloudflare Workflows provide durable, multi-step execution for tasks that need to survive failures, retry automatically, and wait for external events. When integrated with Agents, Workflows handle long-running background processing while Agents manage real-time communication.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/concepts/workflows.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Workflows

## What are Workflows?

[Cloudflare Workflows](https://developers.cloudflare.com/workflows/) provide durable, multi-step execution for tasks that need to survive failures, retry automatically, and wait for external events. When integrated with Agents, Workflows handle long-running background processing while Agents manage real-time communication.

### Agents vs. Workflows

Agents and Workflows have complementary strengths:

| Capability              | Agents                     | Workflows                      |
| ----------------------- | -------------------------- | ------------------------------ |
| Execution model         | Can run indefinitely       | Run to completion              |
| Real-time communication | WebSockets, HTTP streaming | Not supported                  |
| State persistence       | Built-in SQL database      | Step-level persistence         |
| Failure handling        | Application-defined        | Automatic retries and recovery |
| External events         | Direct handling            | Pause and wait for events      |
| User interaction        | Direct (chat, UI)          | Through Agent callbacks        |

Agents can loop, branch, and interact directly with users. Workflows execute steps sequentially with guaranteed delivery and can pause for days waiting for approvals or external data.

### When to use each

**Use Agents alone for:**

* Chat and messaging applications
* Quick API calls and responses
* Real-time collaborative features
* Tasks under 30 seconds

**Use Agents with Workflows for:**

* Data processing pipelines
* Report generation
* Human-in-the-loop approval flows
* Tasks requiring guaranteed delivery
* Multi-step operations with retry requirements

**Use Workflows alone for:**

* Background jobs with or without user approval
* Scheduled data synchronization
* Event-driven processing pipelines

## How Agents and Workflows communicate

The `AgentWorkflow` class (imported from `agents/workflows`) provides bidirectional communication between Workflows and their originating Agent.

### Workflow to Agent

Workflows can communicate with Agents through several mechanisms:

* **RPC calls**: Directly call Agent methods with full type safety via `this.agent`
* **Progress reporting**: Send progress updates via `this.reportProgress()` that trigger Agent callbacks
* **State updates**: Modify Agent state via `step.updateAgentState()` or `step.mergeAgentState()`, which broadcasts to connected clients
* **Client broadcasts**: Send messages to all WebSocket clients via `this.broadcastToClients()`

* [  JavaScript ](#tab-panel-2852)
* [  TypeScript ](#tab-panel-2853)

JavaScript

```

// Inside a workflow's run() method

await this.agent.updateTaskStatus(taskId, "processing"); // RPC call

await this.reportProgress({ step: "process", percent: 0.5 }); // Progress (non-durable)

this.broadcastToClients({ type: "update", taskId }); // Broadcast (non-durable)

await step.mergeAgentState({ taskProgress: 0.5 }); // State update (durable)


```

TypeScript

```

// Inside a workflow's run() method

await this.agent.updateTaskStatus(taskId, "processing"); // RPC call

await this.reportProgress({ step: "process", percent: 0.5 }); // Progress (non-durable)

this.broadcastToClients({ type: "update", taskId }); // Broadcast (non-durable)

await step.mergeAgentState({ taskProgress: 0.5 }); // State update (durable)


```

### Agent to Workflow

Agents can interact with running Workflows by:

* **Starting workflows**: Launch new workflow instances with `runWorkflow()`
* **Sending events**: Dispatch events with `sendWorkflowEvent()`
* **Approval/rejection**: Respond to approval requests with `approveWorkflow()` / `rejectWorkflow()`
* **Workflow control**: Pause, resume, terminate, or restart workflows
* **Status queries**: Check workflow progress with `getWorkflow()` / `getWorkflows()`

## Durable vs. non-durable operations

Understanding durability is key to using workflows effectively:

### Non-durable (may repeat on retry)

These operations are lightweight and suitable for frequent updates, but may execute multiple times if the workflow retries:

* `this.reportProgress()` — Progress reporting
* `this.broadcastToClients()` — WebSocket broadcasts
* Direct RPC calls to `this.agent`

### Durable (idempotent, won't repeat)

These operations use the `step` parameter and are guaranteed to execute exactly once:

* `step.do()` — Execute durable steps
* `step.reportComplete()` / `step.reportError()` — Completion reporting
* `step.sendEvent()` — Custom events
* `step.updateAgentState()` / `step.mergeAgentState()` — State synchronization

## Durability guarantees

Workflows provide durability through step-based execution:

1. **Step completion is permanent** — Once a step completes, it will not re-execute even if the workflow restarts
2. **Automatic retries** — Failed steps retry with configurable backoff
3. **Event persistence** — Workflows can wait for events for up to one year
4. **State recovery** — Workflow state survives infrastructure failures

This durability model means workflows are well-suited for tasks where partial completion must be preserved, such as multi-stage data processing or transactions spanning multiple systems.

## Workflow tracking

When an Agent starts a workflow using `runWorkflow()`, the workflow is automatically tracked in the Agent's internal database. This enables:

* Querying workflow status by ID, name, or metadata with cursor-based pagination
* Monitoring progress through lifecycle callbacks (`onWorkflowProgress`, `onWorkflowComplete`, `onWorkflowError`)
* Workflow control: pause, resume, terminate, restart
* Cleaning up completed workflow records with `deleteWorkflow()` / `deleteWorkflows()`
* Correlating workflows with users or sessions through metadata

## Common patterns

### Background processing with progress

An Agent receives a request, starts a Workflow for heavy processing, and broadcasts progress updates to connected clients as the Workflow executes each step.

* [  JavaScript ](#tab-panel-2854)
* [  TypeScript ](#tab-panel-2855)

JavaScript

```

// Workflow reports progress after each item

for (let i = 0; i < items.length; i++) {

  await step.do(`process-${i}`, async () => processItem(items[i]));

  await this.reportProgress({

    step: `process-${i}`,

    percent: (i + 1) / items.length,

    message: `Processed ${i + 1}/${items.length}`,

  });

}


```

TypeScript

```

// Workflow reports progress after each item

for (let i = 0; i < items.length; i++) {

  await step.do(`process-${i}`, async () => processItem(items[i]));

  await this.reportProgress({

    step: `process-${i}`,

    percent: (i + 1) / items.length,

    message: `Processed ${i + 1}/${items.length}`,

  });

}


```

### Human-in-the-loop approval

A Workflow prepares a request, pauses to wait for approval using `waitForApproval()`, and the Agent provides UI for users to approve or reject via `approveWorkflow()` / `rejectWorkflow()`. The Workflow resumes or throws `WorkflowRejectedError` based on the decision.

### Resilient external API calls

A Workflow wraps external API calls in durable steps with retry logic. If the API fails or the workflow restarts, completed calls are not repeated and failed calls retry automatically.

* [  JavaScript ](#tab-panel-2856)
* [  TypeScript ](#tab-panel-2857)

JavaScript

```

const result = await step.do(

  "call-api",

  {

    retries: { limit: 5, delay: "10 seconds", backoff: "exponential" },

    timeout: "5 minutes",

  },

  async () => {

    const response = await fetch("https://api.example.com/process");

    if (!response.ok) throw new Error(`API error: ${response.status}`);

    return response.json();

  },

);


```

TypeScript

```

const result = await step.do(

  "call-api",

  {

    retries: { limit: 5, delay: "10 seconds", backoff: "exponential" },

    timeout: "5 minutes",

  },

  async () => {

    const response = await fetch("https://api.example.com/process");

    if (!response.ok) throw new Error(`API error: ${response.status}`);

    return response.json();

  },

);


```

### State synchronization

A Workflow updates Agent state at key milestones using `step.updateAgentState()` or `step.mergeAgentState()`. These state changes broadcast to all connected clients, keeping UIs synchronized without polling.

## Related resources

[ Run Workflows API ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Implementation details for agent workflows. 

[ Cloudflare Workflows ](https://developers.cloudflare.com/workflows/) Workflow fundamentals and documentation. 

[ Human-in-the-loop ](https://developers.cloudflare.com/agents/concepts/human-in-the-loop/) Approval flows and manual intervention. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/concepts/","name":"Concepts"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/concepts/workflows/","name":"Workflows"}}]}
```

---

---
title: Implement Effective Agent Patterns
description: Implement common agent patterns using the Agents SDK framework.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/anthropic-agent-patterns.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Implement Effective Agent Patterns

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/anthropic-agent-patterns/","name":"Implement Effective Agent Patterns"}}]}
```

---

---
title: Build a Remote MCP Client
description: Build an AI Agent that acts as a remote MCP client.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/build-mcp-client.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build a Remote MCP Client

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/build-mcp-client/","name":"Build a Remote MCP Client"}}]}
```

---

---
title: Build an Interactive ChatGPT App
description: This guide will show you how to build and deploy an interactive ChatGPT App on Cloudflare Workers that can:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/chatgpt-app.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build an Interactive ChatGPT App

**Last reviewed:**  5 months ago 

## Deploy your first ChatGPT App

This guide will show you how to build and deploy an interactive ChatGPT App on Cloudflare Workers that can:

* Render rich, interactive UI widgets directly in ChatGPT conversations
* Maintain real-time, multi-user state using Durable Objects
* Enable bidirectional communication between your app and ChatGPT
* Build multiplayer experiences that run entirely within ChatGPT

You will build a real-time multiplayer chess game that demonstrates these capabilities. Players can start or join games, make moves on an interactive chessboard, and even ask ChatGPT for strategic advice—all without leaving the conversation.

Your ChatGPT App will use the **Model Context Protocol (MCP)** to expose tools and UI resources that ChatGPT can invoke on your behalf.

You can view the full code for this example [here ↗](https://github.com/cloudflare/agents/tree/main/openai-sdk/chess-app).

## Prerequisites

Before you begin, you will need:

* A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up)
* [Node.js ↗](https://nodejs.org/) installed (v18 or later)
* A [ChatGPT Plus or Team account ↗](https://chat.openai.com/) with developer mode enabled
* Basic knowledge of React and TypeScript

## 1\. Enable ChatGPT Developer Mode

To use ChatGPT Apps (also called connectors), you need to enable developer mode:

1. Open [ChatGPT ↗](https://chat.openai.com/).
2. Go to **Settings** \> **Apps & Connectors** \> **Advanced Settings**
3. Toggle **Developer mode ON**

Once enabled, you will be able to install custom apps during development and testing.

## 2\. Create your ChatGPT App project

1. Create a new project for your Chess App:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- my-chess-app
```

```
yarn create cloudflare my-chess-app
```

```
pnpm create cloudflare@latest my-chess-app
```

1. Navigate into your project:

Terminal window

```

cd my-chess-app


```

1. Install the required dependencies:

Terminal window

```

npm install agents @modelcontextprotocol/sdk chess.js react react-dom react-chessboard


```

1. Install development dependencies:

Terminal window

```

npm install -D @cloudflare/vite-plugin @vitejs/plugin-react vite vite-plugin-singlefile @types/react @types/react-dom


```

## 3\. Configure your project

1. Update your `wrangler.jsonc` to configure Durable Objects and assets:

* [  wrangler.jsonc ](#tab-panel-2908)
* [  wrangler.toml ](#tab-panel-2909)

```

{

  "name": "my-chess-app",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {

    "bindings": [

      {

        "name": "CHESS",

        "class_name": "ChessGame",

      },

    ],

  },

  "migrations": [

    {

      "tag": "v1",

      "new_sqlite_classes": ["ChessGame"],

    },

  ],

  "assets": {

    "directory": "dist",

    "binding": "ASSETS",

  },

}


```

```

name = "my-chess-app"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[[durable_objects.bindings]]

name = "CHESS"

class_name = "ChessGame"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "ChessGame" ]


[assets]

directory = "dist"

binding = "ASSETS"


```

1. Create a `vite.config.ts` for building your React UI:

TypeScript

```

import { cloudflare } from "@cloudflare/vite-plugin";

import react from "@vitejs/plugin-react";

import { defineConfig } from "vite";

import { viteSingleFile } from "vite-plugin-singlefile";


export default defineConfig({

  plugins: [react(), cloudflare(), viteSingleFile()],

  build: {

    minify: false,

  },

});


```

1. Update your `package.json` scripts:

```

{

  "scripts": {

    "dev": "vite",

    "build": "vite build",

    "deploy": "vite build && wrangler deploy"

  }

}


```

## 4\. Create the Chess game engine

1. Create the game logic using Durable Objects at `src/chess.tsx`:

```

import { Agent, callable, getCurrentAgent } from "agents";

import { Chess } from "chess.js";


type Color = "w" | "b";


type ConnectionState = {

  playerId: string;

};


export type State = {

  board: string;

  players: { w?: string; b?: string };

  status: "waiting" | "active" | "mate" | "draw" | "resigned";

  winner?: Color;

  lastSan?: string;

};


export class ChessGame extends Agent<Env, State> {

  initialState: State = {

    board: new Chess().fen(),

    players: {},

    status: "waiting",

  };


  game = new Chess();


  constructor(

    ctx: DurableObjectState,

    public env: Env,

  ) {

    super(ctx, env);

    this.game.load(this.state.board);

  }


  private colorOf(playerId: string): Color | undefined {

    const { players } = this.state;

    if (players.w === playerId) return "w";

    if (players.b === playerId) return "b";

    return undefined;

  }


  @callable()

  join(params: { playerId: string; preferred?: Color | "any" }) {

    const { playerId, preferred = "any" } = params;

    const { connection } = getCurrentAgent();

    if (!connection) throw new Error("Not connected");


    connection.setState({ playerId });

    const s = this.state;


    // Already seated? Return seat

    const already = this.colorOf(playerId);

    if (already) {

      return { ok: true, role: already as Color, state: s };

    }


    // Choose a seat

    const free: Color[] = (["w", "b"] as const).filter((c) => !s.players[c]);

    if (free.length === 0) {

      return { ok: true, role: "spectator" as const, state: s };

    }


    let seat: Color = free[0];

    if (preferred === "w" && free.includes("w")) seat = "w";

    if (preferred === "b" && free.includes("b")) seat = "b";


    s.players[seat] = playerId;

    s.status = s.players.w && s.players.b ? "active" : "waiting";

    this.setState(s);

    return { ok: true, role: seat, state: s };

  }


  @callable()

  move(

    move: { from: string; to: string; promotion?: string },

    expectedFen?: string,

  ) {

    if (this.state.status === "waiting") {

      return {

        ok: false,

        reason: "not-in-game",

        fen: this.game.fen(),

        status: this.state.status,

      };

    }


    const { connection } = getCurrentAgent();

    if (!connection) throw new Error("Not connected");

    const { playerId } = connection.state as ConnectionState;


    const seat = this.colorOf(playerId);

    if (!seat) {

      return {

        ok: false,

        reason: "not-in-game",

        fen: this.game.fen(),

        status: this.state.status,

      };

    }


    if (seat !== this.game.turn()) {

      return {

        ok: false,

        reason: "not-your-turn",

        fen: this.game.fen(),

        status: this.state.status,

      };

    }


    // Optimistic sync guard

    if (expectedFen && expectedFen !== this.game.fen()) {

      return {

        ok: false,

        reason: "stale",

        fen: this.game.fen(),

        status: this.state.status,

      };

    }


    const res = this.game.move(move);

    if (!res) {

      return {

        ok: false,

        reason: "illegal",

        fen: this.game.fen(),

        status: this.state.status,

      };

    }


    const fen = this.game.fen();

    let status: State["status"] = "active";

    if (this.game.isCheckmate()) status = "mate";

    else if (this.game.isDraw()) status = "draw";


    this.setState({

      ...this.state,

      board: fen,

      lastSan: res.san,

      status,

      winner:

        status === "mate" ? (this.game.turn() === "w" ? "b" : "w") : undefined,

    });


    return { ok: true, fen, san: res.san, status };

  }


  @callable()

  resign() {

    const { connection } = getCurrentAgent();

    if (!connection) throw new Error("Not connected");

    const { playerId } = connection.state as ConnectionState;


    const seat = this.colorOf(playerId);

    if (!seat) return { ok: false, reason: "not-in-game", state: this.state };


    const winner = seat === "w" ? "b" : "w";

    this.setState({ ...this.state, status: "resigned", winner });

    return { ok: true, state: this.state };

  }

}


```

## 5\. Create the MCP server and UI resource

1. Create your main worker at `src/index.ts`:

TypeScript

```

import { createMcpHandler } from "agents/mcp";

import { routeAgentRequest } from "agents";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { env } from "cloudflare:workers";


const getWidgetHtml = async (host: string) => {

  let html = await (await env.ASSETS.fetch("http://localhost/")).text();

  html = html.replace(

    "<!--RUNTIME_CONFIG-->",

    `<script>window.HOST = \`${host}\`;</script>`,

  );

  return html;

};


function createServer() {

  const server = new McpServer({ name: "Chess", version: "v1.0.0" });


  // Register a UI resource that ChatGPT can render

  server.registerResource(

    "chess",

    "ui://widget/index.html",

    {},

    async (_uri, extra) => {

      return {

        contents: [

          {

            uri: "ui://widget/index.html",

            mimeType: "text/html+skybridge",

            text: await getWidgetHtml(

              extra.requestInfo?.headers.host as string,

            ),

          },

        ],

      };

    },

  );


  // Register a tool that ChatGPT can call to render the UI

  server.registerTool(

    "playChess",

    {

      title: "Renders a chess game menu, ready to start or join a game.",

      annotations: { readOnlyHint: true },

      _meta: {

        "openai/outputTemplate": "ui://widget/index.html",

        "openai/toolInvocation/invoking": "Opening chess widget",

        "openai/toolInvocation/invoked": "Chess widget opened",

      },

    },

    async (_, _extra) => {

      return {

        content: [

          { type: "text", text: "Successfully rendered chess game menu" },

        ],

      };

    },

  );


  return server;

}


export default {

  async fetch(req: Request, env: Env, ctx: ExecutionContext) {

    const url = new URL(req.url);

    if (url.pathname.startsWith("/mcp")) {

      // Create a new server instance per request

      const server = createServer();

      return createMcpHandler(server)(req, env, ctx);

    }


    return (

      (await routeAgentRequest(req, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


export { ChessGame } from "./chess";


```

## 6\. Build the React UI

1. Create the HTML entry point at `index.html`:

```

<!doctype html>

<html>

  <head>

    <!--RUNTIME_CONFIG-->

  </head>

  <body>

    <div id="root" style="font-family: verdana"></div>

    <script type="module" src="/src/app.tsx"></script>

  </body>

</html>


```

1. Create the React app at `src/app.tsx`:

```

import { useEffect, useRef, useState } from "react";

import { useAgent } from "agents/react";

import { createRoot } from "react-dom/client";

import { Chess, type Square } from "chess.js";

import { Chessboard, type PieceDropHandlerArgs } from "react-chessboard";

import type { State as ServerState } from "./chess";


function usePlayerId() {

  const [pid] = useState(() => {

    const existing = localStorage.getItem("playerId");

    if (existing) return existing;

    const id = crypto.randomUUID();

    localStorage.setItem("playerId", id);

    return id;

  });

  return pid;

}


function App() {

  const playerId = usePlayerId();

  const [gameId, setGameId] = useState<string | null>(null);

  const [gameIdInput, setGameIdInput] = useState("");

  const [menuError, setMenuError] = useState<string | null>(null);


  const gameRef = useRef(new Chess());

  const [fen, setFen] = useState(gameRef.current.fen());

  const [myColor, setMyColor] = useState<"w" | "b" | "spectator">("spectator");

  const [pending, setPending] = useState(false);

  const [serverState, setServerState] = useState<ServerState | null>(null);

  const [joined, setJoined] = useState(false);


  const host = window.HOST ?? "http://localhost:5173/";


  const { stub } = useAgent<ServerState>({

    host,

    name: gameId ?? "__lobby__",

    agent: "chess",

    onStateUpdate: (s) => {

      if (!gameId) return;

      gameRef.current.load(s.board);

      setFen(s.board);

      setServerState(s);

    },

  });


  useEffect(() => {

    if (!gameId || joined) return;


    (async () => {

      try {

        const res = await stub.join({ playerId, preferred: "any" });

        if (!res?.ok) return;


        setMyColor(res.role);

        gameRef.current.load(res.state.board);

        setFen(res.state.board);

        setServerState(res.state);

        setJoined(true);

      } catch (error) {

        console.error("Failed to join game", error);

      }

    })();

  }, [playerId, gameId, stub, joined]);


  async function handleStartNewGame() {

    const newId = crypto.randomUUID();

    setGameId(newId);

    setGameIdInput(newId);

    setMenuError(null);

    setJoined(false);

  }


  async function handleJoinGame() {

    const trimmed = gameIdInput.trim();

    if (!trimmed) {

      setMenuError("Enter a game ID to join.");

      return;

    }

    setGameId(trimmed);

    setMenuError(null);

    setJoined(false);

  }


  const handleHelpClick = () => {

    window.openai?.sendFollowUpMessage?.({

      prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice.`,

    });

  };


  function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) {

    if (!gameId || !sourceSquare || !targetSquare || pending) return false;


    const game = gameRef.current;

    if (myColor === "spectator" || game.turn() !== myColor) return false;


    const piece = game.get(sourceSquare as Square);

    if (!piece || piece.color !== myColor) return false;


    const prevFen = game.fen();


    try {

      const local = game.move({

        from: sourceSquare,

        to: targetSquare,

        promotion: "q",

      });

      if (!local) return false;

    } catch {

      return false;

    }


    const nextFen = game.fen();

    setFen(nextFen);

    setPending(true);


    stub

      .move({ from: sourceSquare, to: targetSquare, promotion: "q" }, prevFen)

      .then((r) => {

        if (!r.ok) {

          game.load(r.fen);

          setFen(r.fen);

        }

      })

      .finally(() => setPending(false));


    return true;

  }


  return (

    <div style={{ padding: "20px", background: "#f8fafc", minHeight: "100vh" }}>

      {!gameId ? (

        <div

          style={{

            maxWidth: "420px",

            margin: "0 auto",

            background: "#fff",

            borderRadius: "16px",

            padding: "24px",

          }}

        >

          <h1>Ready to play?</h1>

          <p>Start a new match or join an existing game.</p>

          <button

            onClick={handleStartNewGame}

            style={{

              padding: "12px",

              background: "#2563eb",

              color: "#fff",

              border: "none",

              borderRadius: "8px",

              cursor: "pointer",

              width: "100%",

            }}

          >

            Start a new game

          </button>

          <div style={{ marginTop: "16px" }}>

            <input

              placeholder="Paste a game ID"

              value={gameIdInput}

              onChange={(e) => setGameIdInput(e.target.value)}

              style={{

                width: "100%",

                padding: "10px",

                borderRadius: "8px",

                border: "1px solid #ccc",

              }}

            />

            <button

              onClick={handleJoinGame}

              style={{

                marginTop: "8px",

                padding: "10px",

                background: "#0f172a",

                color: "#fff",

                border: "none",

                borderRadius: "8px",

                cursor: "pointer",

                width: "100%",

              }}

            >

              Join

            </button>

            {menuError && (

              <p style={{ color: "red", fontSize: "0.85rem" }}>{menuError}</p>

            )}

          </div>

        </div>

      ) : (

        <div style={{ maxWidth: "600px", margin: "0 auto" }}>

          <div

            style={{

              background: "#fff",

              padding: "16px",

              borderRadius: "16px",

              marginBottom: "16px",

            }}

          >

            <h2>Game {gameId}</h2>

            <p>Status: {serverState?.status}</p>

            <button

              onClick={handleHelpClick}

              style={{

                padding: "10px",

                background: "#2563eb",

                color: "#fff",

                border: "none",

                borderRadius: "8px",

                cursor: "pointer",

              }}

            >

              Ask for help

            </button>

          </div>

          <div

            style={{

              background: "#fff",

              padding: "16px",

              borderRadius: "16px",

            }}

          >

            <Chessboard

              position={fen}

              onPieceDrop={onPieceDrop}

              boardOrientation={myColor === "b" ? "black" : "white"}

            />

          </div>

        </div>

      )}

    </div>

  );

}


const root = createRoot(document.getElementById("root")!);

root.render(<App />);


```

Note

This is a simplified version of the UI. For the complete implementation with player slots, better styling, and game state management, check out the [full example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/openai-sdk/chess-app/src/app.tsx).

## 7\. Build and deploy

1. Build your React UI:

Terminal window

```

npm run build


```

This compiles your React app into a single HTML file in the `dist` directory.

1. Deploy to Cloudflare:

Terminal window

```

npx wrangler deploy


```

After deployment, you will see your app URL:

```

https://my-chess-app.YOUR_SUBDOMAIN.workers.dev


```

## 8\. Connect to ChatGPT

Now connect your deployed app to ChatGPT:

1. Open [ChatGPT ↗](https://chat.openai.com/).
2. Go to **Settings** \> **Apps & Connectors** \> **Create**
3. Give your app a **name**, and optionally a **description** and **icon**.
4. Enter your MCP endpoint: `https://my-chess-app.YOUR_SUBDOMAIN.workers.dev/mcp`.
5. Select **"No authentication"**.
6. Select **"Create"**.

## 9\. Play chess in ChatGPT

Try it out:

1. In your ChatGPT conversation, type: "Let's play chess".
2. ChatGPT will call the `playChess` tool and render your interactive chess widget.
3. Select **"Start a new game"** to create a game.
4. Share the game ID with a friend who can join via their own ChatGPT conversation.
5. Make moves by dragging pieces on the board.
6. Select **"Ask for help"** to get strategic advice from ChatGPT

Note

You might need to manually select the connector in the prompt box the first time you use it. Select **"+"** \> **"More"** \> **\[App name\]**.

## Key concepts

### MCP Server

The Model Context Protocol (MCP) server defines tools and resources that ChatGPT can access. Note that we create a new server instance per request to prevent cross-client response leakage:

TypeScript

```

function createServer() {

  const server = new McpServer({ name: "Chess", version: "v1.0.0" });


  // Register a UI resource that ChatGPT can render

  server.registerResource(

    "chess",

    "ui://widget/index.html",

    {},

    async (_uri, extra) => {

      return {

        contents: [

          {

            uri: "ui://widget/index.html",

            mimeType: "text/html+skybridge",

            text: await getWidgetHtml(

              extra.requestInfo?.headers.host as string,

            ),

          },

        ],

      };

    },

  );


  // Register a tool that ChatGPT can call to render the UI

  server.registerTool(

    "playChess",

    {

      title: "Renders a chess game menu, ready to start or join a game.",

      annotations: { readOnlyHint: true },

      _meta: {

        "openai/outputTemplate": "ui://widget/index.html",

        "openai/toolInvocation/invoking": "Opening chess widget",

        "openai/toolInvocation/invoked": "Chess widget opened",

      },

    },

    async (_, _extra) => {

      return {

        content: [

          { type: "text", text: "Successfully rendered chess game menu" },

        ],

      };

    },

  );


  return server;

}


```

### Game Engine with Agents

The `ChessGame` class extends `Agent` to create a stateful game engine:

```

export class ChessGame extends Agent<Env, State> {

  initialState: State = {

    board: new Chess().fen(),

    players: {},

    status: "waiting"

  };


  game = new Chess();


  constructor(

    ctx: DurableObjectState,

    public env: Env

  ) {

    super(ctx, env);

    this.game.load(this.state.board);

  }


```

Each game gets its own Agent instance, enabling:

* **Isolated state** per game
* **Real-time synchronization** across players
* **Persistent storage** that survives worker restarts

### Callable methods

Use the `@callable()` decorator to expose methods that clients can invoke:

TypeScript

```

@callable()

join(params: { playerId: string; preferred?: Color | "any" }) {

  const { playerId, preferred = "any" } = params;

  const { connection } = getCurrentAgent();

  if (!connection) throw new Error("Not connected");


  connection.setState({ playerId });

  const s = this.state;


  // Already seated? Return seat

  const already = this.colorOf(playerId);

  if (already) {

    return { ok: true, role: already as Color, state: s };

  }


  // Choose a seat

  const free: Color[] = (["w", "b"] as const).filter((c) => !s.players[c]);

  if (free.length === 0) {

    return { ok: true, role: "spectator" as const, state: s };

  }


  let seat: Color = free[0];

  if (preferred === "w" && free.includes("w")) seat = "w";

  if (preferred === "b" && free.includes("b")) seat = "b";


  s.players[seat] = playerId;

  s.status = s.players.w && s.players.b ? "active" : "waiting";

  this.setState(s);

  return { ok: true, role: seat, state: s };

}


```

### React integration

The `useAgent` hook connects your React app to the Durable Object:

```

const { stub } = useAgent<ServerState>({

  host,

  name: gameId ?? "__lobby__",

  agent: "chess",

  onStateUpdate: (s) => {

    gameRef.current.load(s.board);

    setFen(s.board);

    setServerState(s);

  },

});


```

Call methods on the agent:

```

const res = await stub.join({ playerId, preferred: "any" });

await stub.move({ from: "e2", to: "e4" });


```

### Bidirectional communication

Your app can send messages to ChatGPT:

TypeScript

```

const handleHelpClick = () => {

  window.openai?.sendFollowUpMessage?.({

    prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice as there are no tools for you to use.`,

  });

};


```

This creates a new message in the ChatGPT conversation with context about the current game state.

## Next steps

Now that you have a working ChatGPT App, you can:

* Add more tools: Expose additional capabilities and UIs through MCP tools and resources.
* Enhance the UI: Build more sophisticated interfaces with React.

## Related resources

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

[ Durable Objects ](https://developers.cloudflare.com/durable-objects/) Learn about the underlying stateful infrastructure. 

[ Model Context Protocol ](https://modelcontextprotocol.io/) MCP specification and documentation. 

[ OpenAI Apps SDK ](https://developers.openai.com/apps-sdk/) Official OpenAI Apps SDK reference. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/chatgpt-app/","name":"Build an Interactive ChatGPT App"}}]}
```

---

---
title: Connect to an MCP server
description: Your Agent can connect to external Model Context Protocol (MCP) servers to access their tools and extend your Agent's capabilities. In this tutorial, you'll create an Agent that connects to an MCP server and uses one of its tools.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/connect-mcp-client.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Connect to an MCP server

**Last reviewed:**  5 months ago 

Your Agent can connect to external [Model Context Protocol (MCP) ↗](https://modelcontextprotocol.io) servers to access their tools and extend your Agent's capabilities. In this tutorial, you'll create an Agent that connects to an MCP server and uses one of its tools.

## What you will build

An Agent with endpoints to:

* Connect to an MCP server
* List available tools from connected servers
* Get the connection status

## Prerequisites

An MCP server to connect to (or use the public example in this tutorial).

## 1\. Create a basic Agent

1. Create a new Agent project using the `hello-world` template:  
 npm  yarn  pnpm  
```  
npm create cloudflare@latest -- my-mcp-client --template=cloudflare/ai/demos/hello-world  
```  
```  
yarn create cloudflare my-mcp-client --template=cloudflare/ai/demos/hello-world  
```  
```  
pnpm create cloudflare@latest my-mcp-client --template=cloudflare/ai/demos/hello-world  
```
2. Move into the project directory:  
Terminal window  
```  
cd my-mcp-client  
```  
Your Agent is ready! The template includes a minimal Agent in `src/index.ts`:  
   * [  JavaScript ](#tab-panel-2910)  
   * [  TypeScript ](#tab-panel-2911)  
JavaScript  
```  
import { Agent, routeAgentRequest } from "agents";  
export class HelloAgent extends Agent {  
  async onRequest(request) {  
    return new Response("Hello, Agent!", { status: 200 });  
  }  
}  
export default {  
  async fetch(request, env) {  
    return (  
      (await routeAgentRequest(request, env, { cors: true })) ||  
      new Response("Not found", { status: 404 })  
    );  
  },  
};  
```  
TypeScript  
```  
import { Agent, routeAgentRequest } from "agents";  
type Env = {  
  HelloAgent: DurableObjectNamespace<HelloAgent>;  
};  
export class HelloAgent extends Agent<Env> {  
  async onRequest(request: Request): Promise<Response> {  
    return new Response("Hello, Agent!", { status: 200 });  
  }  
}  
export default {  
  async fetch(request: Request, env: Env) {  
    return (  
      (await routeAgentRequest(request, env, { cors: true })) ||  
      new Response("Not found", { status: 404 })  
    );  
  },  
} satisfies ExportedHandler<Env>;  
```

## 2\. Add MCP connection endpoint

1. Add an endpoint to connect to MCP servers. Update your Agent class in `src/index.ts`:  
   * [  JavaScript ](#tab-panel-2914)  
   * [  TypeScript ](#tab-panel-2915)  
JavaScript  
```  
export class HelloAgent extends Agent {  
  async onRequest(request) {  
    const url = new URL(request.url);  
    // Connect to an MCP server  
    if (url.pathname.endsWith("add-mcp") && request.method === "POST") {  
      const { serverUrl, name } = await request.json();  
      const { id, authUrl } = await this.addMcpServer(name, serverUrl);  
      if (authUrl) {  
        // OAuth required - return auth URL  
        return new Response(JSON.stringify({ serverId: id, authUrl }), {  
          headers: { "Content-Type": "application/json" },  
        });  
      }  
      return new Response(  
        JSON.stringify({ serverId: id, status: "connected" }),  
        { headers: { "Content-Type": "application/json" } },  
      );  
    }  
    return new Response("Not found", { status: 404 });  
  }  
}  
```  
TypeScript  
```  
export class HelloAgent extends Agent<Env> {  
  async onRequest(request: Request): Promise<Response> {  
    const url = new URL(request.url);  
    // Connect to an MCP server  
    if (url.pathname.endsWith("add-mcp") && request.method === "POST") {  
      const { serverUrl, name } = (await request.json()) as {  
        serverUrl: string;  
        name: string;  
      };  
      const { id, authUrl } = await this.addMcpServer(name, serverUrl);  
      if (authUrl) {  
        // OAuth required - return auth URL  
        return new Response(  
          JSON.stringify({ serverId: id, authUrl }),  
          { headers: { "Content-Type": "application/json" } },  
        );  
      }  
      return new Response(  
        JSON.stringify({ serverId: id, status: "connected" }),  
        { headers: { "Content-Type": "application/json" } },  
      );  
    }  
    return new Response("Not found", { status: 404 });  
  }  
}  
```

The `addMcpServer()` method connects to an MCP server. If the server requires OAuth authentication, it returns an `authUrl` that users must visit to complete authorization.

## 3\. Test the connection

1. Start your development server:  
Terminal window  
```  
npm start  
```
2. In a new terminal, connect to an MCP server (using a public example):  
Terminal window  
```  
curl -X POST http://localhost:8788/agents/hello-agent/default/add-mcp \  
  -H "Content-Type: application/json" \  
  -d '{  
    "serverUrl": "https://docs.mcp.cloudflare.com/mcp",  
    "name": "Example Server"  
  }'  
```  
You should see a response with the server ID:  
```  
{  
  "serverId": "example-server-id",  
  "status": "connected"  
}  
```

## 4\. List available tools

1. Add an endpoint to see which tools are available from connected servers:  
   * [  JavaScript ](#tab-panel-2912)  
   * [  TypeScript ](#tab-panel-2913)  
JavaScript  
```  
export class HelloAgent extends Agent {  
  async onRequest(request) {  
    const url = new URL(request.url);  
    // ... previous add-mcp endpoint ...  
    // List MCP state (servers, tools, etc)  
    if (url.pathname.endsWith("mcp-state") && request.method === "GET") {  
      const mcpState = this.getMcpServers();  
      return Response.json(mcpState);  
    }  
    return new Response("Not found", { status: 404 });  
  }  
}  
```  
TypeScript  
```  
export class HelloAgent extends Agent<Env> {  
  async onRequest(request: Request): Promise<Response> {  
    const url = new URL(request.url);  
    // ... previous add-mcp endpoint ...  
    // List MCP state (servers, tools, etc)  
    if (url.pathname.endsWith("mcp-state") && request.method === "GET") {  
      const mcpState = this.getMcpServers();  
      return Response.json(mcpState);  
    }  
    return new Response("Not found", { status: 404 });  
  }  
}  
```
2. Test it:  
Terminal window  
```  
curl http://localhost:8788/agents/hello-agent/default/mcp-state  
```  
You'll see all connected servers, their connection states, and available tools:  
```  
{  
  "servers": {  
    "example-server-id": {  
      "name": "Example Server",  
      "state": "ready",  
      "server_url": "https://docs.mcp.cloudflare.com/mcp",  
      ...  
    }  
  },  
  "tools": [  
    {  
      "name": "add",  
      "description": "Add two numbers",  
      "serverId": "example-server-id",  
      ...  
    }  
  ]  
}  
```

## Summary

You created an Agent that can:

* Connect to external MCP servers dynamically
* Handle OAuth authentication flows when required
* List all available tools from connected servers
* Monitor connection status

Connections persist in the Agent's [SQL storage](https://developers.cloudflare.com/agents/api-reference/store-and-sync-state/), so they remain active across requests.

## Next steps

[ Handle OAuth flows ](https://developers.cloudflare.com/agents/guides/oauth-mcp-client/) Configure OAuth callbacks and error handling. 

[ MCP Client API ](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/) Complete API documentation for MCP clients. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/connect-mcp-client/","name":"Connect to an MCP server"}}]}
```

---

---
title: Cross-domain authentication
description: When your Agents are deployed, to keep things secure, send a token from the client, then verify it on the server. This guide covers authentication patterns for WebSocket connections to agents.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/cross-domain-authentication.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Cross-domain authentication

When your Agents are deployed, to keep things secure, send a token from the client, then verify it on the server. This guide covers authentication patterns for WebSocket connections to agents.

## WebSocket authentication

WebSockets are not HTTP, so the handshake is limited when making cross-domain connections.

You cannot send:

* Custom headers during the upgrade
* `Authorization: Bearer ...` on connect

You can:

* Put a signed, short-lived token in the connection URL as query parameters
* Verify the token in your server's connect path

Note

Never place raw secrets in URLs. Use a JWT or a signed token that expires quickly, and is scoped to the user or room.

### Same origin

If the client and server share the origin, the browser will send cookies during the WebSocket handshake. Session-based auth can work here. Prefer HTTP-only cookies.

### Cross origin

Cookies do not help across origins. Pass credentials in the URL query, then verify on the server.

## Usage examples

### Static authentication

* [  JavaScript ](#tab-panel-2916)
* [  TypeScript ](#tab-panel-2917)

JavaScript

```

import { useAgent } from "agents/react";


function ChatComponent() {

  const agent = useAgent({

    agent: "my-agent",

    query: {

      token: "demo-token-123",

      userId: "demo-user",

    },

  });


  // Use agent to make calls, access state, etc.

}


```

TypeScript

```

import { useAgent } from "agents/react";


function ChatComponent() {

  const agent = useAgent({

    agent: "my-agent",

    query: {

      token: "demo-token-123",

      userId: "demo-user",

    },

  });


  // Use agent to make calls, access state, etc.

}


```

### Async authentication

Build query values right before connect. Use Suspense for async setup.

* [  JavaScript ](#tab-panel-2922)
* [  TypeScript ](#tab-panel-2923)

JavaScript

```

import { useAgent } from "agents/react";

import { Suspense, useCallback } from "react";


function ChatComponent() {

  const asyncQuery = useCallback(async () => {

    const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);

    return {

      token,

      userId: user.id,

      timestamp: Date.now().toString(),

    };

  }, []);


  const agent = useAgent({

    agent: "my-agent",

    query: asyncQuery,

  });


  // Use agent to make calls, access state, etc.

}


function App() {

  return (

    <Suspense fallback={<div>Authenticating...</div>}>

      <ChatComponent />

    </Suspense>

  );

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { Suspense, useCallback } from "react";


function ChatComponent() {

  const asyncQuery = useCallback(async () => {

    const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);

    return {

      token,

      userId: user.id,

      timestamp: Date.now().toString(),

    };

  }, []);


  const agent = useAgent({

    agent: "my-agent",

    query: asyncQuery,

  });


  // Use agent to make calls, access state, etc.

}


function App() {

  return (

    <Suspense fallback={<div>Authenticating...</div>}>

      <ChatComponent />

    </Suspense>

  );

}


```

### JWT refresh pattern

Refresh the token when the connection fails due to authentication error.

* [  JavaScript ](#tab-panel-2924)
* [  TypeScript ](#tab-panel-2925)

JavaScript

```

import { useAgent } from "agents/react";

import { useCallback } from "react";


const validateToken = async (token) => {

  // An example of how you might implement this

  const res = await fetch(`${API_HOST}/api/users/me`, {

    headers: {

      Authorization: `Bearer ${token}`,

    },

  });


  return res.ok;

};


const refreshToken = async () => {

  // Depends on implementation:

  // - You could use a longer-lived token to refresh the expired token

  // - De-auth the app and prompt the user to log in manually

  // - ...

};


function useJWTAgent(agentName) {

  const asyncQuery = useCallback(async () => {

    let token = localStorage.getItem("jwt");


    // If no token OR the token is no longer valid

    // request a fresh token

    if (!token || !(await validateToken(token))) {

      token = await refreshToken();

      localStorage.setItem("jwt", token);

    }


    return {

      token,

    };

  }, []);


  const agent = useAgent({

    agent: agentName,

    query: asyncQuery,

    queryDeps: [], // Run on mount

  });


  return agent;

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useCallback } from "react";


const validateToken = async (token: string) => {

  // An example of how you might implement this

  const res = await fetch(`${API_HOST}/api/users/me`, {

    headers: {

      Authorization: `Bearer ${token}`,

    },

  });


  return res.ok;

};


const refreshToken = async () => {

  // Depends on implementation:

  // - You could use a longer-lived token to refresh the expired token

  // - De-auth the app and prompt the user to log in manually

  // - ...

};


function useJWTAgent(agentName: string) {

  const asyncQuery = useCallback(async () => {

    let token = localStorage.getItem("jwt");


    // If no token OR the token is no longer valid

    // request a fresh token

    if (!token || !(await validateToken(token))) {

      token = await refreshToken();

      localStorage.setItem("jwt", token);

    }


    return {

      token,

    };

  }, []);


  const agent = useAgent({

    agent: agentName,

    query: asyncQuery,

    queryDeps: [], // Run on mount

  });


  return agent;

}


```

## Cross-domain authentication

Pass credentials in the URL when connecting to another host, then verify on the server.

### Static cross-domain auth

* [  JavaScript ](#tab-panel-2918)
* [  TypeScript ](#tab-panel-2919)

JavaScript

```

import { useAgent } from "agents/react";


function StaticCrossDomainAuth() {

  const agent = useAgent({

    agent: "my-agent",

    host: "https://my-agent.example.workers.dev",

    query: {

      token: "demo-token-123",

      userId: "demo-user",

    },

  });


  // Use agent to make calls, access state, etc.

}


```

TypeScript

```

import { useAgent } from "agents/react";


function StaticCrossDomainAuth() {

  const agent = useAgent({

    agent: "my-agent",

    host: "https://my-agent.example.workers.dev",

    query: {

      token: "demo-token-123",

      userId: "demo-user",

    },

  });


  // Use agent to make calls, access state, etc.

}


```

### Async cross-domain auth

* [  JavaScript ](#tab-panel-2920)
* [  TypeScript ](#tab-panel-2921)

JavaScript

```

import { useAgent } from "agents/react";

import { useCallback } from "react";


function AsyncCrossDomainAuth() {

  const asyncQuery = useCallback(async () => {

    const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);

    return {

      token,

      userId: user.id,

      timestamp: Date.now().toString(),

    };

  }, []);


  const agent = useAgent({

    agent: "my-agent",

    host: "https://my-agent.example.workers.dev",

    query: asyncQuery,

  });


  // Use agent to make calls, access state, etc.

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useCallback } from "react";


function AsyncCrossDomainAuth() {

  const asyncQuery = useCallback(async () => {

    const [token, user] = await Promise.all([getAuthToken(), getCurrentUser()]);

    return {

      token,

      userId: user.id,

      timestamp: Date.now().toString(),

    };

  }, []);


  const agent = useAgent({

    agent: "my-agent",

    host: "https://my-agent.example.workers.dev",

    query: asyncQuery,

  });


  // Use agent to make calls, access state, etc.

}


```

## Server-side verification

On the server side, verify the token in the `onConnect` handler:

* [  JavaScript ](#tab-panel-2926)
* [  TypeScript ](#tab-panel-2927)

JavaScript

```

import { Agent, Connection, ConnectionContext } from "agents";


export class SecureAgent extends Agent {

  async onConnect(connection, ctx) {

    const url = new URL(ctx.request.url);

    const token = url.searchParams.get("token");

    const userId = url.searchParams.get("userId");


    // Verify the token

    if (!token || !(await this.verifyToken(token, userId))) {

      connection.close(4001, "Unauthorized");

      return;

    }


    // Store user info on the connection state

    connection.setState({ userId, authenticated: true });

  }


  async verifyToken(token, userId) {

    // Implement your token verification logic

    // For example, verify a JWT signature, check expiration, etc.

    try {

      const payload = await verifyJWT(token, this.env.JWT_SECRET);

      return payload.sub === userId && payload.exp > Date.now() / 1000;

    } catch {

      return false;

    }

  }


  async onMessage(connection, message) {

    // Check if connection is authenticated

    if (!connection.state?.authenticated) {

      connection.send(JSON.stringify({ error: "Not authenticated" }));

      return;

    }


    // Process message for authenticated user

    const userId = connection.state.userId;

    // ...

  }

}


```

TypeScript

```

import { Agent, Connection, ConnectionContext } from "agents";


export class SecureAgent extends Agent {

  async onConnect(connection: Connection, ctx: ConnectionContext) {

    const url = new URL(ctx.request.url);

    const token = url.searchParams.get("token");

    const userId = url.searchParams.get("userId");


    // Verify the token

    if (!token || !(await this.verifyToken(token, userId))) {

      connection.close(4001, "Unauthorized");

      return;

    }


    // Store user info on the connection state

    connection.setState({ userId, authenticated: true });

  }


  private async verifyToken(token: string, userId: string): Promise<boolean> {

    // Implement your token verification logic

    // For example, verify a JWT signature, check expiration, etc.

    try {

      const payload = await verifyJWT(token, this.env.JWT_SECRET);

      return payload.sub === userId && payload.exp > Date.now() / 1000;

    } catch {

      return false;

    }

  }


  async onMessage(connection: Connection, message: string) {

    // Check if connection is authenticated

    if (!connection.state?.authenticated) {

      connection.send(JSON.stringify({ error: "Not authenticated" }));

      return;

    }


    // Process message for authenticated user

    const userId = connection.state.userId;

    // ...

  }

}


```

## Best practices

1. **Use short-lived tokens** \- Tokens in URLs may be logged. Keep expiration times short (minutes, not hours).
2. **Scope tokens appropriately** \- Include the agent name or instance in the token claims to prevent token reuse across agents.
3. **Validate on every connection** \- Always verify tokens in `onConnect`, not just once.
4. **Use HTTPS** \- Always use secure WebSocket connections (`wss://`) in production.
5. **Rotate secrets** \- Regularly rotate your JWT signing keys or token secrets.
6. **Log authentication failures** \- Track failed authentication attempts for security monitoring.

## Next steps

[ Routing ](https://developers.cloudflare.com/agents/api-reference/routing/) Routing and authentication hooks. 

[ WebSockets ](https://developers.cloudflare.com/agents/api-reference/websockets/) Real-time bidirectional communication. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/cross-domain-authentication/","name":"Cross-domain authentication"}}]}
```

---

---
title: Human-in-the-loop patterns
description: Implement human-in-the-loop functionality using Cloudflare Agents for workflow approvals and MCP elicitation
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/human-in-the-loop.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Human-in-the-loop patterns

Human-in-the-loop (HITL) patterns allow agents to pause execution and wait for human approval, confirmation, or input before proceeding. This is essential for compliance, safety, and oversight in agentic systems.

## Why human-in-the-loop?

* **Compliance**: Regulatory requirements may mandate human approval for certain actions
* **Safety**: High-stakes operations (payments, deletions, external communications) need oversight
* **Quality**: Human review catches errors AI might miss
* **Trust**: Users feel more confident when they can approve critical actions

### Common use cases

| Use Case            | Example                              |
| ------------------- | ------------------------------------ |
| Financial approvals | Expense reports, payment processing  |
| Content moderation  | Publishing, email sending            |
| Data operations     | Bulk deletions, exports              |
| AI tool execution   | Confirming tool calls before running |
| Access control      | Granting permissions, role changes   |

## Choosing a pattern

Cloudflare provides two main patterns for human-in-the-loop:

| Pattern               | Best for                                     | Key API           |
| --------------------- | -------------------------------------------- | ----------------- |
| **Workflow approval** | Multi-step processes, durable approval gates | waitForApproval() |
| **MCP elicitation**   | MCP servers requesting structured user input | elicitInput()     |

Decision guide:

* Use **Workflow approval** when you need durable, multi-step processes with approval gates that can wait hours, days, or weeks
* Use **MCP elicitation** when building MCP servers that need to request additional structured input from users during tool execution

## Workflow-based approval

For durable, multi-step processes, use [Cloudflare Workflows](https://developers.cloudflare.com/workflows/) with the `waitForApproval()` method. The workflow pauses until a human approves or rejects.

### Basic pattern

* [  JavaScript ](#tab-panel-2936)
* [  TypeScript ](#tab-panel-2937)

JavaScript

```

import { Agent } from "agents";

import { AgentWorkflow } from "agents/workflows";

export class ExpenseWorkflow extends AgentWorkflow {

  async run(event, step) {

    const expense = event.payload;


    // Step 1: Validate the expense

    const validated = await step.do("validate", async () => {

      if (expense.amount <= 0) {

        throw new Error("Invalid expense amount");

      }

      return { ...expense, validatedAt: Date.now() };

    });


    // Step 2: Report that we are waiting for approval

    await this.reportProgress({

      step: "approval",

      status: "pending",

      message: `Awaiting approval for $${expense.amount}`,

    });


    // Step 3: Wait for human approval (pauses the workflow)

    const approval = await this.waitForApproval(step, {

      timeout: "7 days",

    });


    console.log(`Approved by: ${approval?.approvedBy}`);


    // Step 4: Process the approved expense

    const result = await step.do("process", async () => {

      return { expenseId: crypto.randomUUID(), ...validated };

    });


    await step.reportComplete(result);

    return result;

  }

}


```

TypeScript

```

import { Agent } from "agents";

import { AgentWorkflow } from "agents/workflows";

import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";


type ExpenseParams = {

  amount: number;

  description: string;

  requestedBy: string;

};


export class ExpenseWorkflow extends AgentWorkflow<

  ExpenseAgent,

  ExpenseParams

> {

  async run(event: AgentWorkflowEvent<ExpenseParams>, step: AgentWorkflowStep) {

    const expense = event.payload;


    // Step 1: Validate the expense

    const validated = await step.do("validate", async () => {

      if (expense.amount <= 0) {

        throw new Error("Invalid expense amount");

      }

      return { ...expense, validatedAt: Date.now() };

    });


    // Step 2: Report that we are waiting for approval

    await this.reportProgress({

      step: "approval",

      status: "pending",

      message: `Awaiting approval for $${expense.amount}`,

    });


    // Step 3: Wait for human approval (pauses the workflow)

    const approval = await this.waitForApproval<{ approvedBy: string }>(step, {

      timeout: "7 days",

    });


    console.log(`Approved by: ${approval?.approvedBy}`);


    // Step 4: Process the approved expense

    const result = await step.do("process", async () => {

      return { expenseId: crypto.randomUUID(), ...validated };

    });


    await step.reportComplete(result);

    return result;

  }

}


```

### Agent methods for approval

The agent provides methods to approve or reject waiting workflows:

* [  JavaScript ](#tab-panel-2940)
* [  TypeScript ](#tab-panel-2941)

JavaScript

```

import { Agent, callable } from "agents";


export class ExpenseAgent extends Agent {

  initialState = {

    pendingApprovals: [],

  };


  // Approve a waiting workflow

  @callable()

  async approve(workflowId, approvedBy) {

    await this.approveWorkflow(workflowId, {

      reason: "Expense approved",

      metadata: { approvedBy, approvedAt: Date.now() },

    });


    // Update state to reflect approval

    this.setState({

      ...this.state,

      pendingApprovals: this.state.pendingApprovals.filter(

        (p) => p.workflowId !== workflowId,

      ),

    });

  }


  // Reject a waiting workflow

  @callable()

  async reject(workflowId, reason) {

    await this.rejectWorkflow(workflowId, { reason });


    this.setState({

      ...this.state,

      pendingApprovals: this.state.pendingApprovals.filter(

        (p) => p.workflowId !== workflowId,

      ),

    });

  }


  // Track workflow progress to update pending approvals

  async onWorkflowProgress(workflowName, workflowId, progress) {

    const p = progress;


    if (p.step === "approval" && p.status === "pending") {

      // Add to pending approvals list for UI display

      this.setState({

        ...this.state,

        pendingApprovals: [

          ...this.state.pendingApprovals,

          {

            workflowId,

            amount: 0, // Would come from workflow params

            description: p.message || "",

            requestedBy: "user",

            requestedAt: Date.now(),

          },

        ],

      });

    }

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


type PendingApproval = {

  workflowId: string;

  amount: number;

  description: string;

  requestedBy: string;

  requestedAt: number;

};


type ExpenseState = {

  pendingApprovals: PendingApproval[];

};


export class ExpenseAgent extends Agent<Env, ExpenseState> {

  initialState: ExpenseState = {

    pendingApprovals: [],

  };


  // Approve a waiting workflow

  @callable()

  async approve(workflowId: string, approvedBy: string): Promise<void> {

    await this.approveWorkflow(workflowId, {

      reason: "Expense approved",

      metadata: { approvedBy, approvedAt: Date.now() },

    });


    // Update state to reflect approval

    this.setState({

      ...this.state,

      pendingApprovals: this.state.pendingApprovals.filter(

        (p) => p.workflowId !== workflowId,

      ),

    });

  }


  // Reject a waiting workflow

  @callable()

  async reject(workflowId: string, reason: string): Promise<void> {

    await this.rejectWorkflow(workflowId, { reason });


    this.setState({

      ...this.state,

      pendingApprovals: this.state.pendingApprovals.filter(

        (p) => p.workflowId !== workflowId,

      ),

    });

  }


  // Track workflow progress to update pending approvals

  async onWorkflowProgress(

    workflowName: string,

    workflowId: string,

    progress: unknown,

  ): Promise<void> {

    const p = progress as { step: string; status: string; message?: string };


    if (p.step === "approval" && p.status === "pending") {

      // Add to pending approvals list for UI display

      this.setState({

        ...this.state,

        pendingApprovals: [

          ...this.state.pendingApprovals,

          {

            workflowId,

            amount: 0, // Would come from workflow params

            description: p.message || "",

            requestedBy: "user",

            requestedAt: Date.now(),

          },

        ],

      });

    }

  }

}


```

### Timeout handling

Set timeouts to prevent workflows from waiting indefinitely:

* [  JavaScript ](#tab-panel-2930)
* [  TypeScript ](#tab-panel-2931)

JavaScript

```

const approval = await this.waitForApproval(step, {

  timeout: "7 days", // Also supports: "1 hour", "30 minutes", etc.

});


if (!approval) {

  // Timeout expired - escalate or auto-reject

  await step.reportError("Approval timeout - escalating to manager");

  throw new Error("Approval timeout");

}


```

TypeScript

```

const approval = await this.waitForApproval<{ approvedBy: string }>(step, {

  timeout: "7 days", // Also supports: "1 hour", "30 minutes", etc.

});


if (!approval) {

  // Timeout expired - escalate or auto-reject

  await step.reportError("Approval timeout - escalating to manager");

  throw new Error("Approval timeout");

}


```

### Escalation with scheduling

Use `schedule()` to set up escalation reminders:

* [  JavaScript ](#tab-panel-2932)
* [  TypeScript ](#tab-panel-2933)

JavaScript

```

import { Agent, callable } from "agents";


class ExpenseAgent extends Agent {

  @callable()

  async submitForApproval(expense) {

    // Start the approval workflow

    const workflowId = await this.runWorkflow("EXPENSE_WORKFLOW", expense);


    // Schedule reminder after 4 hours

    await this.schedule(Date.now() + 4 * 60 * 60 * 1000, "sendReminder", {

      workflowId,

    });


    // Schedule escalation after 24 hours

    await this.schedule(Date.now() + 24 * 60 * 60 * 1000, "escalateApproval", {

      workflowId,

    });


    return workflowId;

  }


  async sendReminder(payload) {

    const workflow = this.getWorkflow(payload.workflowId);

    if (workflow?.status === "waiting") {

      // Send reminder notification

      console.log("Reminder: approval still pending");

    }

  }


  async escalateApproval(payload) {

    const workflow = this.getWorkflow(payload.workflowId);

    if (workflow?.status === "waiting") {

      // Escalate to manager

      console.log("Escalating to manager");

    }

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


class ExpenseAgent extends Agent<Env, ExpenseState> {

  @callable()

  async submitForApproval(expense: ExpenseParams): Promise<string> {

    // Start the approval workflow

    const workflowId = await this.runWorkflow("EXPENSE_WORKFLOW", expense);


    // Schedule reminder after 4 hours

    await this.schedule(Date.now() + 4 * 60 * 60 * 1000, "sendReminder", {

      workflowId,

    });


    // Schedule escalation after 24 hours

    await this.schedule(Date.now() + 24 * 60 * 60 * 1000, "escalateApproval", {

      workflowId,

    });


    return workflowId;

  }


  async sendReminder(payload: { workflowId: string }) {

    const workflow = this.getWorkflow(payload.workflowId);

    if (workflow?.status === "waiting") {

      // Send reminder notification

      console.log("Reminder: approval still pending");

    }

  }


  async escalateApproval(payload: { workflowId: string }) {

    const workflow = this.getWorkflow(payload.workflowId);

    if (workflow?.status === "waiting") {

      // Escalate to manager

      console.log("Escalating to manager");

    }

  }

}


```

### Audit trail with SQL

Use `this.sql` to maintain an immutable audit trail:

* [  JavaScript ](#tab-panel-2934)
* [  TypeScript ](#tab-panel-2935)

JavaScript

```

import { Agent, callable } from "agents";


class ExpenseAgent extends Agent {

  async onStart() {

    // Create audit table

    this.sql`

      CREATE TABLE IF NOT EXISTS approval_audit (

        id INTEGER PRIMARY KEY AUTOINCREMENT,

        workflow_id TEXT NOT NULL,

        decision TEXT NOT NULL CHECK(decision IN ('approved', 'rejected')),

        decided_by TEXT NOT NULL,

        decided_at INTEGER NOT NULL,

        reason TEXT

      )

    `;

  }


  @callable()

  async approve(workflowId, userId, reason) {

    // Record the decision in SQL (immutable audit log)

    this.sql`

      INSERT INTO approval_audit (workflow_id, decision, decided_by, decided_at, reason)

      VALUES (${workflowId}, 'approved', ${userId}, ${Date.now()}, ${reason || null})

    `;


    // Process the approval

    await this.approveWorkflow(workflowId, {

      reason: reason || "Approved",

      metadata: { approvedBy: userId },

    });

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


class ExpenseAgent extends Agent<Env, ExpenseState> {

  async onStart() {

    // Create audit table

    this.sql`

      CREATE TABLE IF NOT EXISTS approval_audit (

        id INTEGER PRIMARY KEY AUTOINCREMENT,

        workflow_id TEXT NOT NULL,

        decision TEXT NOT NULL CHECK(decision IN ('approved', 'rejected')),

        decided_by TEXT NOT NULL,

        decided_at INTEGER NOT NULL,

        reason TEXT

      )

    `;

  }


  @callable()

  async approve(

    workflowId: string,

    userId: string,

    reason?: string,

  ): Promise<void> {

    // Record the decision in SQL (immutable audit log)

    this.sql`

      INSERT INTO approval_audit (workflow_id, decision, decided_by, decided_at, reason)

      VALUES (${workflowId}, 'approved', ${userId}, ${Date.now()}, ${reason || null})

    `;


    // Process the approval

    await this.approveWorkflow(workflowId, {

      reason: reason || "Approved",

      metadata: { approvedBy: userId },

    });

  }

}


```

### Configuration

* [  wrangler.jsonc ](#tab-panel-2928)
* [  wrangler.toml ](#tab-panel-2929)

```

{

  "name": "expense-approval",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {

    "bindings": [{ "name": "EXPENSE_AGENT", "class_name": "ExpenseAgent" }],

  },

  "workflows": [

    {

      "name": "expense-workflow",

      "binding": "EXPENSE_WORKFLOW",

      "class_name": "ExpenseWorkflow",

    },

  ],

  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ExpenseAgent"] }],

}


```

```

name = "expense-approval"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[[durable_objects.bindings]]

name = "EXPENSE_AGENT"

class_name = "ExpenseAgent"


[[workflows]]

name = "expense-workflow"

binding = "EXPENSE_WORKFLOW"

class_name = "ExpenseWorkflow"


[[migrations]]

tag = "v1"

new_sqlite_classes = [ "ExpenseAgent" ]


```

## MCP elicitation

When building MCP servers with `McpAgent`, you can request additional user input during tool execution using **elicitation**. The MCP client renders a form based on your JSON Schema and returns the user's response.

### Basic pattern

* [  JavaScript ](#tab-panel-2942)
* [  TypeScript ](#tab-panel-2943)

JavaScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


export class CounterMCP extends McpAgent {

  server = new McpServer({

    name: "counter-server",

    version: "1.0.0",

  });


  initialState = { counter: 0 };


  async init() {

    this.server.tool(

      "increase-counter",

      "Increase the counter by a user-specified amount",

      { confirm: z.boolean().describe("Do you want to increase the counter?") },

      async ({ confirm }, extra) => {

        if (!confirm) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Request additional input from the user

        const userInput = await this.server.server.elicitInput(

          {

            message: "By how much do you want to increase the counter?",

            requestedSchema: {

              type: "object",

              properties: {

                amount: {

                  type: "number",

                  title: "Amount",

                  description: "The amount to increase the counter by",

                },

              },

              required: ["amount"],

            },

          },

          { relatedRequestId: extra.requestId },

        );


        // Check if user accepted or cancelled

        if (userInput.action !== "accept" || !userInput.content) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Use the input

        const amount = Number(userInput.content.amount);

        this.setState({

          ...this.state,

          counter: this.state.counter + amount,

        });


        return {

          content: [

            {

              type: "text",

              text: `Counter increased by ${amount}, now at ${this.state.counter}`,

            },

          ],

        };

      },

    );

  }

}


```

TypeScript

```

import { McpAgent } from "agents/mcp";

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

import { z } from "zod";


type State = { counter: number };


export class CounterMCP extends McpAgent<Env, State, {}> {

  server = new McpServer({

    name: "counter-server",

    version: "1.0.0",

  });


  initialState: State = { counter: 0 };


  async init() {

    this.server.tool(

      "increase-counter",

      "Increase the counter by a user-specified amount",

      { confirm: z.boolean().describe("Do you want to increase the counter?") },

      async ({ confirm }, extra) => {

        if (!confirm) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Request additional input from the user

        const userInput = await this.server.server.elicitInput(

          {

            message: "By how much do you want to increase the counter?",

            requestedSchema: {

              type: "object",

              properties: {

                amount: {

                  type: "number",

                  title: "Amount",

                  description: "The amount to increase the counter by",

                },

              },

              required: ["amount"],

            },

          },

          { relatedRequestId: extra.requestId },

        );


        // Check if user accepted or cancelled

        if (userInput.action !== "accept" || !userInput.content) {

          return { content: [{ type: "text", text: "Cancelled." }] };

        }


        // Use the input

        const amount = Number(userInput.content.amount);

        this.setState({

          ...this.state,

          counter: this.state.counter + amount,

        });


        return {

          content: [

            {

              type: "text",

              text: `Counter increased by ${amount}, now at ${this.state.counter}`,

            },

          ],

        };

      },

    );

  }

}


```

## Elicitation vs workflow approval

| Aspect       | MCP Elicitation               | Workflow Approval             |
| ------------ | ----------------------------- | ----------------------------- |
| **Context**  | MCP server tool execution     | Multi-step workflow processes |
| **Duration** | Immediate (within tool call)  | Can wait hours/days/weeks     |
| **UI**       | JSON Schema-based form        | Custom UI via agent state     |
| **State**    | MCP session state             | Durable workflow state        |
| **Use case** | Interactive input during tool | Approval gates in pipelines   |

## Building approval UIs

### Pending approvals list

Use the agent's state to display pending approvals in your UI:

```

import { useAgent } from "agents/react";


function PendingApprovals() {

  const { state, agent } = useAgent({

    agent: "expense-agent",

    name: "main",

  });


  if (!state?.pendingApprovals?.length) {

    return <p>No pending approvals</p>;

  }


  return (

    <div className="approval-list">

      {state.pendingApprovals.map((item) => (

        <div key={item.workflowId} className="approval-card">

          <h3>${item.amount}</h3>

          <p>{item.description}</p>

          <p>Requested by {item.requestedBy}</p>


          <div className="actions">

            <button

              onClick={() => agent.stub.approve(item.workflowId, "admin")}

            >

              Approve

            </button>

            <button

              onClick={() => agent.stub.reject(item.workflowId, "Declined")}

            >

              Reject

            </button>

          </div>

        </div>

      ))}

    </div>

  );

}


```

## Multi-approver patterns

For sensitive operations requiring multiple approvers:

* [  JavaScript ](#tab-panel-2938)
* [  TypeScript ](#tab-panel-2939)

JavaScript

```

import { Agent, callable } from "agents";


class MultiApprovalAgent extends Agent {

  @callable()

  async approveMulti(workflowId, userId) {

    const approval = this.state.pendingMultiApprovals.find(

      (p) => p.workflowId === workflowId,

    );

    if (!approval) throw new Error("Approval not found");


    // Check if user already approved

    if (approval.currentApprovals.some((a) => a.userId === userId)) {

      throw new Error("Already approved by this user");

    }


    // Add this user's approval

    approval.currentApprovals.push({ userId, approvedAt: Date.now() });


    // Check if we have enough approvals

    if (approval.currentApprovals.length >= approval.requiredApprovals) {

      // Execute the approved action

      await this.approveWorkflow(workflowId, {

        metadata: { approvers: approval.currentApprovals },

      });

      return true;

    }


    this.setState({ ...this.state });

    return false; // Still waiting for more approvals

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


type MultiApproval = {

  workflowId: string;

  requiredApprovals: number;

  currentApprovals: Array<{ userId: string; approvedAt: number }>;

  rejections: Array<{ userId: string; rejectedAt: number; reason: string }>;

};


type State = {

  pendingMultiApprovals: MultiApproval[];

};


class MultiApprovalAgent extends Agent<Env, State> {

  @callable()

  async approveMulti(workflowId: string, userId: string): Promise<boolean> {

    const approval = this.state.pendingMultiApprovals.find(

      (p) => p.workflowId === workflowId,

    );

    if (!approval) throw new Error("Approval not found");


    // Check if user already approved

    if (approval.currentApprovals.some((a) => a.userId === userId)) {

      throw new Error("Already approved by this user");

    }


    // Add this user's approval

    approval.currentApprovals.push({ userId, approvedAt: Date.now() });


    // Check if we have enough approvals

    if (approval.currentApprovals.length >= approval.requiredApprovals) {

      // Execute the approved action

      await this.approveWorkflow(workflowId, {

        metadata: { approvers: approval.currentApprovals },

      });

      return true;

    }


    this.setState({ ...this.state });

    return false; // Still waiting for more approvals

  }

}


```

## Best practices

1. **Define clear approval criteria** — Only require confirmation for actions with meaningful consequences (payments, emails, data changes)
2. **Provide detailed context** — Show users exactly what the action will do, including all arguments
3. **Implement timeouts** — Use `schedule()` to escalate or auto-reject after reasonable periods
4. **Maintain audit trails** — Use `this.sql` to record all approval decisions for compliance
5. **Handle connection drops** — Store pending approvals in agent state so they survive disconnections
6. **Graceful degradation** — Provide fallback behavior if approvals are rejected

## Next steps

[ Run Workflows ](https://developers.cloudflare.com/agents/api-reference/run-workflows/) Complete waitForApproval() API reference. 

[ MCP servers ](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/) Build MCP agents with elicitation. 

[ Email notifications ](https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/) Send notifications for pending approvals. 

[ Schedule tasks ](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) Implement approval timeouts with schedules. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/human-in-the-loop/","name":"Human-in-the-loop patterns"}}]}
```

---

---
title: Handle OAuth with MCP servers
description: When connecting to OAuth-protected MCP servers (like Slack or Notion), your users need to authenticate before your Agent can access their data. This guide covers implementing OAuth flows for seamless authorization.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/oauth-mcp-client.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Handle OAuth with MCP servers

When connecting to OAuth-protected MCP servers (like Slack or Notion), your users need to authenticate before your Agent can access their data. This guide covers implementing OAuth flows for seamless authorization.

## How it works

1. Call `addMcpServer()` with the server URL
2. If OAuth is required, an `authUrl` is returned instead of connecting immediately
3. Present the `authUrl` to your user (redirect, popup, or link)
4. User authenticates on the provider's site
5. Provider redirects back to your Agent's callback URL
6. Your Agent completes the connection automatically

The MCP client uses a built-in `DurableObjectOAuthClientProvider` to manage OAuth state securely — storing a nonce and server ID, validating on callback, and cleaning up after use or expiration.

## Initiate OAuth

When connecting to an OAuth-protected server, check if `authUrl` is returned. If present, redirect your user to complete authorization:

* [  JavaScript ](#tab-panel-2948)
* [  TypeScript ](#tab-panel-2949)

JavaScript

```

export class MyAgent extends Agent {

  async onRequest(request) {

    const url = new URL(request.url);


    if (url.pathname.endsWith("/connect") && request.method === "POST") {

      const { id, authUrl } = await this.addMcpServer(

        "Cloudflare Observability",

        "https://observability.mcp.cloudflare.com/mcp",

      );


      if (authUrl) {

        // OAuth required - redirect user to authorize

        return Response.redirect(authUrl, 302);

      }


      // Already authenticated - connection complete

      return Response.json({ serverId: id, status: "connected" });

    }


    return new Response("Not found", { status: 404 });

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env> {

  async onRequest(request: Request): Promise<Response> {

    const url = new URL(request.url);


    if (url.pathname.endsWith("/connect") && request.method === "POST") {

      const { id, authUrl } = await this.addMcpServer(

        "Cloudflare Observability",

        "https://observability.mcp.cloudflare.com/mcp",

      );


      if (authUrl) {

        // OAuth required - redirect user to authorize

        return Response.redirect(authUrl, 302);

      }


      // Already authenticated - connection complete

      return Response.json({ serverId: id, status: "connected" });

    }


    return new Response("Not found", { status: 404 });

  }

}


```

### Alternative approaches

Instead of an automatic redirect, you can present the `authUrl` to your user as a:

* **Popup window**: `window.open(authUrl, '_blank', 'width=600,height=700')` for dashboard-style apps
* **Clickable link**: Display as a button or link for multi-step flows
* **Deep link**: Use custom URL schemes for mobile apps

## Configure callback behavior

After OAuth completes, the provider redirects back to your Agent's callback URL. By default, successful authentication redirects to your application origin, while failed authentication displays an HTML error page with the error message.

### Redirect to your application

Redirect users back to your application after OAuth completes:

* [  JavaScript ](#tab-panel-2944)
* [  TypeScript ](#tab-panel-2945)

JavaScript

```

export class MyAgent extends Agent {

  onStart() {

    this.mcp.configureOAuthCallback({

      successRedirect: "/dashboard",

      errorRedirect: "/auth-error",

    });

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env> {

  onStart() {

    this.mcp.configureOAuthCallback({

      successRedirect: "/dashboard",

      errorRedirect: "/auth-error",

    });

  }

}


```

Users return to `/dashboard` on success or `/auth-error?error=<message>` on failure.

### Close popup window

If you opened OAuth in a popup, close it automatically when complete:

* [  JavaScript ](#tab-panel-2946)
* [  TypeScript ](#tab-panel-2947)

JavaScript

```

import { Agent } from "agents";


export class MyAgent extends Agent {

  onStart() {

    this.mcp.configureOAuthCallback({

      customHandler: () => {

        // Close the popup after OAuth completes

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }

}


```

TypeScript

```

import { Agent } from "agents";


export class MyAgent extends Agent<Env> {

  onStart() {

    this.mcp.configureOAuthCallback({

      customHandler: () => {

        // Close the popup after OAuth completes

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }

}


```

Your main application can detect the popup closing and refresh the connection status. If OAuth fails, the connection state becomes `"failed"` and the error message is stored in `server.error` for display in your UI.

## Monitor connection status

### React applications

Use the `useAgent` hook for real-time updates via WebSocket:

* [  JavaScript ](#tab-panel-2952)
* [  TypeScript ](#tab-panel-2953)

JavaScript

```

import { useAgent } from "agents/react";

import { useState } from "react";

function App() {

  const [mcpState, setMcpState] = useState({

    prompts: [],

    resources: [],

    servers: {},

    tools: [],

  });


  const agent = useAgent({

    agent: "my-agent",

    name: "session-id",

    onMcpUpdate: (mcpServers) => {

      // Automatically called when MCP state changes!

      setMcpState(mcpServers);

    },

  });


  return (

    <div>

      {Object.entries(mcpState.servers).map(([id, server]) => (

        <div key={id}>

          <strong>{server.name}</strong>: {server.state}

          {server.state === "authenticating" && server.auth_url && (

            <button onClick={() => window.open(server.auth_url, "_blank")}>

              Authorize

            </button>

          )}

          {server.state === "failed" && server.error && (

            <p className="error">{server.error}</p>

          )}

        </div>

      ))}

    </div>

  );

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useState } from "react";

import type { MCPServersState } from "agents";


function App() {

  const [mcpState, setMcpState] = useState<MCPServersState>({

    prompts: [],

    resources: [],

    servers: {},

    tools: [],

  });


  const agent = useAgent({

    agent: "my-agent",

    name: "session-id",

    onMcpUpdate: (mcpServers: MCPServersState) => {

      // Automatically called when MCP state changes!

      setMcpState(mcpServers);

    },

  });


  return (

    <div>

      {Object.entries(mcpState.servers).map(([id, server]) => (

        <div key={id}>

          <strong>{server.name}</strong>: {server.state}

          {server.state === "authenticating" && server.auth_url && (

            <button onClick={() => window.open(server.auth_url, "_blank")}>

              Authorize

            </button>

          )}

          {server.state === "failed" && server.error && (

            <p className="error">{server.error}</p>

          )}

        </div>

      ))}

    </div>

  );

}


```

The `onMcpUpdate` callback fires automatically when MCP state changes — no polling needed.

### Other frameworks

Poll the connection status via an endpoint:

* [  JavaScript ](#tab-panel-2950)
* [  TypeScript ](#tab-panel-2951)

JavaScript

```

export class MyAgent extends Agent {

  async onRequest(request) {

    const url = new URL(request.url);


    if (

      url.pathname.endsWith("connection-status") &&

      request.method === "GET"

    ) {

      const mcpState = this.getMcpServers();


      const connections = Object.entries(mcpState.servers).map(

        ([id, server]) => ({

          serverId: id,

          name: server.name,

          state: server.state,

          isReady: server.state === "ready",

          needsAuth: server.state === "authenticating",

          authUrl: server.auth_url,

        }),

      );


      return Response.json(connections);

    }


    return new Response("Not found", { status: 404 });

  }

}


```

TypeScript

```

export class MyAgent extends Agent<Env> {

  async onRequest(request: Request): Promise<Response> {

    const url = new URL(request.url);


    if (

      url.pathname.endsWith("connection-status") &&

      request.method === "GET"

    ) {

      const mcpState = this.getMcpServers();


      const connections = Object.entries(mcpState.servers).map(

        ([id, server]) => ({

          serverId: id,

          name: server.name,

          state: server.state,

          isReady: server.state === "ready",

          needsAuth: server.state === "authenticating",

          authUrl: server.auth_url,

        }),

      );


      return Response.json(connections);

    }


    return new Response("Not found", { status: 404 });

  }

}


```

Connection states flow: `authenticating` (needs OAuth) → `connecting` (completing setup) → `ready` (available for use)

## Handle failures

When OAuth fails, the connection state becomes `"failed"` and the error message is stored in the `server.error` field. Display this error in your UI and allow users to retry:

* [  JavaScript ](#tab-panel-2954)
* [  TypeScript ](#tab-panel-2955)

JavaScript

```

import { useAgent } from "agents/react";

import { useState } from "react";

function App() {

  const [mcpState, setMcpState] = useState({

    prompts: [],

    resources: [],

    servers: {},

    tools: [],

  });


  const agent = useAgent({

    agent: "my-agent",

    name: "session-id",

    onMcpUpdate: setMcpState,

  });


  const handleRetry = async (serverId, serverUrl, name) => {

    // Remove failed connection

    await fetch(`/agents/my-agent/session-id/disconnect`, {

      method: "POST",

      body: JSON.stringify({ serverId }),

    });


    // Retry connection

    const response = await fetch(`/agents/my-agent/session-id/connect`, {

      method: "POST",

      body: JSON.stringify({ serverUrl, name }),

    });

    const { authUrl } = await response.json();

    if (authUrl) window.open(authUrl, "_blank");

  };


  return (

    <div>

      {Object.entries(mcpState.servers).map(([id, server]) => (

        <div key={id}>

          <strong>{server.name}</strong>: {server.state}

          {server.state === "failed" && (

            <div>

              {server.error && <p className="error">{server.error}</p>}

              <button

                onClick={() => handleRetry(id, server.server_url, server.name)}

              >

                Retry Connection

              </button>

            </div>

          )}

        </div>

      ))}

    </div>

  );

}


```

TypeScript

```

import { useAgent } from "agents/react";

import { useState } from "react";

import type { MCPServersState } from "agents";


function App() {

  const [mcpState, setMcpState] = useState<MCPServersState>({

    prompts: [],

    resources: [],

    servers: {},

    tools: [],

  });


  const agent = useAgent({

    agent: "my-agent",

    name: "session-id",

    onMcpUpdate: setMcpState,

  });


  const handleRetry = async (

    serverId: string,

    serverUrl: string,

    name: string,

  ) => {

    // Remove failed connection

    await fetch(`/agents/my-agent/session-id/disconnect`, {

      method: "POST",

      body: JSON.stringify({ serverId }),

    });


    // Retry connection

    const response = await fetch(`/agents/my-agent/session-id/connect`, {

      method: "POST",

      body: JSON.stringify({ serverUrl, name }),

    });

    const { authUrl } = await response.json();

    if (authUrl) window.open(authUrl, "_blank");

  };


  return (

    <div>

      {Object.entries(mcpState.servers).map(([id, server]) => (

        <div key={id}>

          <strong>{server.name}</strong>: {server.state}

          {server.state === "failed" && (

            <div>

              {server.error && <p className="error">{server.error}</p>}

              <button

                onClick={() => handleRetry(id, server.server_url, server.name)}

              >

                Retry Connection

              </button>

            </div>

          )}

        </div>

      ))}

    </div>

  );

}


```

Common failure reasons:

* **User canceled**: Closed OAuth window before completing authorization
* **Invalid credentials**: Provider credentials were incorrect
* **Permission denied**: User lacks required permissions
* **Expired session**: OAuth session timed out

Failed connections remain in state until removed with `removeMcpServer(serverId)`. The error message is automatically escaped to prevent XSS attacks, so it is safe to display directly in your UI.

## Complete example

This example demonstrates a complete OAuth integration with Cloudflare Observability. Users connect, authorize in a popup window, and the connection becomes available. Errors are automatically stored in the connection state for display in your UI.

* [  JavaScript ](#tab-panel-2956)
* [  TypeScript ](#tab-panel-2957)

JavaScript

```

import { Agent, routeAgentRequest } from "agents";


export class MyAgent extends Agent {

  onStart() {

    this.mcp.configureOAuthCallback({

      customHandler: () => {

        // Close popup after OAuth completes (success or failure)

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }


  async onRequest(request) {

    const url = new URL(request.url);


    // Connect to MCP server

    if (url.pathname.endsWith("/connect") && request.method === "POST") {

      const { id, authUrl } = await this.addMcpServer(

        "Cloudflare Observability",

        "https://observability.mcp.cloudflare.com/mcp",

      );


      if (authUrl) {

        return Response.json({

          serverId: id,

          authUrl: authUrl,

          message: "Please authorize access",

        });

      }


      return Response.json({ serverId: id, status: "connected" });

    }


    // Check connection status

    if (url.pathname.endsWith("/status") && request.method === "GET") {

      const mcpState = this.getMcpServers();

      const connections = Object.entries(mcpState.servers).map(

        ([id, server]) => ({

          serverId: id,

          name: server.name,

          state: server.state,

          authUrl: server.auth_url,

        }),

      );

      return Response.json(connections);

    }


    // Disconnect

    if (url.pathname.endsWith("/disconnect") && request.method === "POST") {

      const { serverId } = await request.json();

      await this.removeMcpServer(serverId);

      return Response.json({ message: "Disconnected" });

    }


    return new Response("Not found", { status: 404 });

  }

}


export default {

  async fetch(request, env) {

    return (

      (await routeAgentRequest(request, env, { cors: true })) ||

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { Agent, routeAgentRequest } from "agents";


type Env = {

  MyAgent: DurableObjectNamespace<MyAgent>;

};


export class MyAgent extends Agent<Env> {

  onStart() {

    this.mcp.configureOAuthCallback({

      customHandler: () => {

        // Close popup after OAuth completes (success or failure)

        return new Response("<script>window.close();</script>", {

          headers: { "content-type": "text/html" },

        });

      },

    });

  }


  async onRequest(request: Request): Promise<Response> {

    const url = new URL(request.url);


    // Connect to MCP server

    if (url.pathname.endsWith("/connect") && request.method === "POST") {

      const { id, authUrl } = await this.addMcpServer(

        "Cloudflare Observability",

        "https://observability.mcp.cloudflare.com/mcp",

      );


      if (authUrl) {

        return Response.json({

          serverId: id,

          authUrl: authUrl,

          message: "Please authorize access",

        });

      }


      return Response.json({ serverId: id, status: "connected" });

    }


    // Check connection status

    if (url.pathname.endsWith("/status") && request.method === "GET") {

      const mcpState = this.getMcpServers();

      const connections = Object.entries(mcpState.servers).map(

        ([id, server]) => ({

          serverId: id,

          name: server.name,

          state: server.state,

          authUrl: server.auth_url,

        }),

      );

      return Response.json(connections);

    }


    // Disconnect

    if (url.pathname.endsWith("/disconnect") && request.method === "POST") {

      const { serverId } = (await request.json()) as { serverId: string };

      await this.removeMcpServer(serverId);

      return Response.json({ message: "Disconnected" });

    }


    return new Response("Not found", { status: 404 });

  }

}


export default {

  async fetch(request: Request, env: Env) {

    return (

      (await routeAgentRequest(request, env, { cors: true })) ||

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

## Related

[ Connect to an MCP server ](https://developers.cloudflare.com/agents/guides/connect-mcp-client/) Get started without OAuth. 

[ MCP Client API ](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/) Complete API documentation for MCP clients. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/oauth-mcp-client/","name":"Handle OAuth with MCP servers"}}]}
```

---

---
title: Build a Remote MCP server
description: This guide will show you how to deploy your own remote MCP server on Cloudflare using Streamable HTTP transport, the current MCP specification standard. You have two options:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/remote-mcp-server.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build a Remote MCP server

This guide will show you how to deploy your own remote MCP server on Cloudflare using [Streamable HTTP transport](https://developers.cloudflare.com/agents/model-context-protocol/transport/), the current MCP specification standard. You have two options:

* **Without authentication** — anyone can connect and use the server (no login required).
* **With [authentication and authorization](https://developers.cloudflare.com/agents/guides/remote-mcp-server/#add-authentication)** — users sign in before accessing tools, and you can control which tools an agent can call based on the user's permissions.

## Choosing an approach

The Agents SDK provides multiple ways to create MCP servers. Choose the approach that fits your use case:

| Approach                                                                                      | Stateful? | Requires Durable Objects? | Best for                                       |
| --------------------------------------------------------------------------------------------- | --------- | ------------------------- | ---------------------------------------------- |
| [createMcpHandler()](https://developers.cloudflare.com/agents/api-reference/mcp-handler-api/) | No        | No                        | Stateless tools, simplest setup                |
| [McpAgent](https://developers.cloudflare.com/agents/api-reference/mcp-agent-api/)             | Yes       | Yes                       | Stateful tools, per-session state, elicitation |
| Raw WebStandardStreamableHTTPServerTransport                                                  | No        | No                        | Full control, no SDK dependency                |

* **`createMcpHandler()`** is the fastest way to get a stateless MCP server running. Use it when your tools do not need per-session state.
* **`McpAgent`** gives you a Durable Object per session with built-in state management, elicitation support, and both SSE and Streamable HTTP transports.
* **Raw transport** gives you full control if you want to use the `@modelcontextprotocol/sdk` directly without the Agents SDK helpers.

## Deploy your first MCP server

You can start by deploying a [public MCP server ↗](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless) without authentication, then add user authentication and scoped authorization later. If you already know your server will require authentication, you can skip ahead to the [next section](https://developers.cloudflare.com/agents/guides/remote-mcp-server/#add-authentication).

### Via the dashboard

The button below will guide you through everything you need to do to deploy an [example MCP server ↗](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless) to your Cloudflare account:

[![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless)

Once deployed, this server will be live at your `workers.dev` subdomain (for example, `remote-mcp-server-authless.your-account.workers.dev/mcp`). You can connect to it immediately using the [AI Playground ↗](https://playground.ai.cloudflare.com/) (a remote MCP client), [MCP inspector ↗](https://github.com/modelcontextprotocol/inspector) or [other MCP clients](https://developers.cloudflare.com/agents/guides/remote-mcp-server/#connect-from-an-mcp-client-via-a-local-proxy).

A new git repository will be set up on your GitHub or GitLab account for your MCP server, configured to automatically deploy to Cloudflare each time you push a change or merge a pull request to the main branch of the repository. You can clone this repository, [develop locally](https://developers.cloudflare.com/agents/guides/remote-mcp-server/#via-the-cli), and start customizing the MCP server with your own [tools](https://developers.cloudflare.com/agents/model-context-protocol/tools/).

### Via the CLI

You can use the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler) to create a new MCP Server on your local machine and deploy it to Cloudflare.

1. Open a terminal and run the following command:  
 npm  yarn  pnpm  
```  
npm create cloudflare@latest -- remote-mcp-server-authless --template=cloudflare/ai/demos/remote-mcp-authless  
```  
```  
yarn create cloudflare remote-mcp-server-authless --template=cloudflare/ai/demos/remote-mcp-authless  
```  
```  
pnpm create cloudflare@latest remote-mcp-server-authless --template=cloudflare/ai/demos/remote-mcp-authless  
```  
During setup, select the following options: - For _Do you want to add an AGENTS.md file to help AI coding tools understand Cloudflare APIs?_, choose `No`. - For _Do you want to use git for version control?_, choose `No`. - For _Do you want to deploy your application?_, choose `No` (we will be testing the server before deploying).  
Now, you have the MCP server setup, with dependencies installed.
2. Move into the project folder:  
Terminal window  
```  
cd remote-mcp-server-authless  
```
3. In the directory of your new project, run the following command to start the development server:  
Terminal window  
```  
npm start  
```  
```  
⎔ Starting local server...  
[wrangler:info] Ready on http://localhost:8788  
```  
Check the command output for the local port. In this example, the MCP server runs on port `8788`, and the MCP endpoint URL is `http://localhost:8788/mcp`.  
Note  
You cannot interact with the MCP server by opening the `/mcp` URL directly in a web browser. The `/mcp` endpoint expects an MCP client to send MCP protocol messages, which a browser does not do by default. In the next step, we will demonstrate how to connect to the server using an MCP client.
4. To test the server locally:  
   1. In a new terminal, run the [MCP inspector ↗](https://github.com/modelcontextprotocol/inspector). The MCP inspector is an interactive MCP client that allows you to connect to your MCP server and invoke tools from a web browser.  
   Terminal window  
   ```  
   npx @modelcontextprotocol/inspector@latest  
   ```  
   ```  
   🚀 MCP Inspector is up and running at:  
     http://localhost:5173/?MCP_PROXY_AUTH_TOKEN=46ab..cd3  
   🌐 Opening browser...  
   ```  
   The MCP Inspector will launch in your web browser. You can also launch it manually by opening a browser and going to `http://localhost:<PORT>`. Check the command output for the local port where MCP Inspector is running. In this example, MCP Inspector is served on port `5173`.  
   2. In the MCP inspector, enter the URL of your MCP server (`http://localhost:8788/mcp`), and select **Connect**. Select **List Tools** to show the tools that your MCP server exposes.
5. You can now deploy your MCP server to Cloudflare. From your project directory, run:  
Terminal window  
```  
npx wrangler@latest deploy  
```  
If you have already [connected a git repository](https://developers.cloudflare.com/workers/ci-cd/builds/) to the Worker with your MCP server, you can deploy your MCP server by pushing a change or merging a pull request to the main branch of the repository.  
The MCP server will be deployed to your `*.workers.dev` subdomain at `https://remote-mcp-server-authless.your-account.workers.dev/mcp`.
6. To test the remote MCP server, take the URL of your deployed MCP server (`https://remote-mcp-server-authless.your-account.workers.dev/mcp`) and enter it in the MCP inspector running on `http://localhost:5173`.

You now have a remote MCP server that MCP clients can connect to.

## Connect from an MCP client via a local proxy

Now that your remote MCP server is running, you can use the [mcp-remote local proxy ↗](https://www.npmjs.com/package/mcp-remote) to connect Claude Desktop or other MCP clients to it — even if your MCP client does not support remote transport or authorization on the client side. This lets you test what an interaction with your remote MCP server will be like with a real MCP client.

For example, to connect from Claude Desktop:

1. Update your Claude Desktop configuration to point to the URL of your MCP server:  
```  
{  
  "mcpServers": {  
    "math": {  
      "command": "npx",  
      "args": [  
        "mcp-remote",  
        "https://remote-mcp-server-authless.your-account.workers.dev/mcp"  
      ]  
    }  
  }  
}  
```
2. Restart Claude Desktop to load the MCP Server. Once this is done, Claude will be able to make calls to your remote MCP server.
3. To test, ask Claude to use one of your tools. For example:  
```  
Could you use the math tool to add 23 and 19?  
```  
Claude should invoke the tool and show the result generated by the remote MCP server.

To learn how to use remote MCP servers with other MCP clients, refer to [Test a Remote MCP Server](https://developers.cloudflare.com/agents/guides/test-remote-mcp-server).

## Add Authentication

The public MCP server example you deployed earlier allows any client to connect and invoke tools without logging in. To add user authentication to your MCP server, you can integrate Cloudflare Access or a third-party service as the OAuth provider. Your MCP server handles secure login flows and issues access tokens that MCP clients can use to make authenticated tool calls. Users sign in with the OAuth provider and grant their AI agent permission to interact with the tools exposed by your MCP server, using scoped permissions.

### Cloudflare Access OAuth

You can configure your MCP server to require user authentication through Cloudflare Access. Cloudflare Access acts as an identity aggregator and verifies user emails, signals from your existing [identity providers](https://developers.cloudflare.com/cloudflare-one/integrations/identity-providers/) (such as GitHub or Google), and other attributes such as IP address or device certificates. When users connect to the MCP server, they will be prompted to log in to the configured identity provider and are only granted access if they pass your [Access policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/#selectors).

For a step-by-step deployment guide, refer to [Secure MCP servers with Access for SaaS](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/saas-mcp/).

### Third-party OAuth

You can connect your MCP server with any [OAuth provider](https://developers.cloudflare.com/agents/model-context-protocol/authorization/#2-third-party-oauth-provider) that supports the OAuth 2.0 specification, including GitHub, Google, Slack, [Stytch](https://developers.cloudflare.com/agents/model-context-protocol/authorization/#stytch), [Auth0](https://developers.cloudflare.com/agents/model-context-protocol/authorization/#auth0), [WorkOS](https://developers.cloudflare.com/agents/model-context-protocol/authorization/#workos), and more.

The following example demonstrates how to use GitHub as an OAuth provider.

#### Step 1 — Create a new MCP server

Run the following command to create a new MCP server with GitHub OAuth:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- my-mcp-server-github-auth --template=cloudflare/ai/demos/remote-mcp-github-oauth
```

```
yarn create cloudflare my-mcp-server-github-auth --template=cloudflare/ai/demos/remote-mcp-github-oauth
```

```
pnpm create cloudflare@latest my-mcp-server-github-auth --template=cloudflare/ai/demos/remote-mcp-github-oauth
```

Now, you have the MCP server setup, with dependencies installed. Move into that project folder:

Terminal window

```

cd my-mcp-server-github-auth


```

You'll notice that in the example MCP server, if you open `src/index.ts`, the primary difference is that the `defaultHandler` is set to the `GitHubHandler`:

TypeScript

```

import GitHubHandler from "./github-handler";


export default new OAuthProvider({

  apiRoute: "/mcp",

  apiHandler: MyMCP.serve("/mcp"),

  defaultHandler: GitHubHandler,

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

});


```

This ensures that your users are redirected to GitHub to authenticate. To get this working though, you need to create OAuth client apps in the steps below.

#### Step 2 — Create an OAuth App

You'll need to create two [GitHub OAuth Apps ↗](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) to use GitHub as an authentication provider for your MCP server — one for local development, and one for production.

#### Step 2.1 — Create a new OAuth App for local development

1. Navigate to [github.com/settings/developers ↗](https://github.com/settings/developers) to create a new OAuth App with the following settings:  
   * **Application name**: `My MCP Server (local)`  
   * **Homepage URL**: `http://localhost:8788`  
   * **Authorization callback URL**: `http://localhost:8788/callback`
2. For the OAuth app you just created, add the client ID of the OAuth app as `GITHUB_CLIENT_ID` and generate a client secret, adding it as `GITHUB_CLIENT_SECRET` to a `.env` file in the root of your project, which [will be used to set secrets in local development](https://developers.cloudflare.com/workers/configuration/secrets/).  
Terminal window  
```  
touch .env  
echo 'GITHUB_CLIENT_ID="your-client-id"' >> .env  
echo 'GITHUB_CLIENT_SECRET="your-client-secret"' >> .env  
cat .env  
```
3. Run the following command to start the development server:  
Terminal window  
```  
npm start  
```  
Your MCP server is now running on `http://localhost:8788/mcp`.
4. In a new terminal, run the [MCP inspector ↗](https://github.com/modelcontextprotocol/inspector). The MCP inspector is an interactive MCP client that allows you to connect to your MCP server and invoke tools from a web browser.  
Terminal window  
```  
npx @modelcontextprotocol/inspector@latest  
```
5. Open the MCP inspector in your web browser:  
Terminal window  
```  
open http://localhost:5173  
```
6. In the inspector, enter the URL of your MCP server, `http://localhost:8788/mcp`
7. In the main panel on the right, click the **OAuth Settings** button and then click **Quick OAuth Flow**.  
You should be redirected to a GitHub login or authorization page. After authorizing the MCP Client (the inspector) access to your GitHub account, you will be redirected back to the inspector.
8. Click **Connect** in the sidebar and you should see the "List Tools" button, which will list the tools that your MCP server exposes.

#### Step 2.2 — Create a new OAuth App for production

You'll need to repeat [Step 2.1](#step-21--create-a-new-oauth-app-for-local-development) to create a new OAuth App for production.

1. Navigate to [github.com/settings/developers ↗](https://github.com/settings/developers) to create a new OAuth App with the following settings:
* **Application name**: `My MCP Server (production)`
* **Homepage URL**: Enter the workers.dev URL of your deployed MCP server (ex: `worker-name.account-name.workers.dev`)
* **Authorization callback URL**: Enter the `/callback` path of the workers.dev URL of your deployed MCP server (ex: `worker-name.account-name.workers.dev/callback`)
1. For the OAuth app you just created, add the client ID and client secret, using Wrangler CLI:

Terminal window

```

npx wrangler secret put GITHUB_CLIENT_ID


```

Terminal window

```

npx wrangler secret put GITHUB_CLIENT_SECRET


```

```

npx wrangler secret put COOKIE_ENCRYPTION_KEY # add any random string here e.g. openssl rand -hex 32


```

Warning

When you create the first secret, Wrangler will ask if you want to create a new Worker. Submit "Y" to create a new Worker and save the secret.

1. Set up a KV namespace  
a. Create the KV namespace:  
Terminal window  
```  
npx wrangler kv namespace create "OAUTH_KV"  
```  
b. Update the `wrangler.jsonc` file with the resulting KV ID:  
```  
{  
  "kvNamespaces": [  
    {  
      "binding": "OAUTH_KV",  
      "id": "<YOUR_KV_NAMESPACE_ID>"  
    }  
  ]  
}  
```
2. Deploy the MCP server to your Cloudflare `workers.dev` domain:  
Terminal window  
```  
npm run deploy  
```
3. Connect to your server running at `worker-name.account-name.workers.dev/mcp` using the [AI Playground ↗](https://playground.ai.cloudflare.com/), MCP Inspector, or [other MCP clients](https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/), and authenticate with GitHub.

## Next steps

[ MCP Tools ](https://developers.cloudflare.com/agents/model-context-protocol/tools/) Add tools to your MCP server. 

[ Authorization ](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) Customize authentication and authorization. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/remote-mcp-server/","name":"Build a Remote MCP server"}}]}
```

---

---
title: Securing MCP servers
description: MCP servers, like any web application, need to be secured so they can be used by trusted users without abuse. The MCP specification uses OAuth 2.1 for authentication between MCP clients and servers.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/securing-mcp-server.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Securing MCP servers

MCP servers, like any web application, need to be secured so they can be used by trusted users without abuse. The MCP specification uses OAuth 2.1 for authentication between MCP clients and servers.

This guide covers security best practices for MCP servers that act as OAuth proxies to third-party providers (like GitHub or Google).

## OAuth protection with workers-oauth-provider

Cloudflare's [workers-oauth-provider ↗](https://github.com/cloudflare/workers-oauth-provider) handles token management, client registration, and access token validation:

* [  JavaScript ](#tab-panel-2958)
* [  TypeScript ](#tab-panel-2959)

JavaScript

```

import { OAuthProvider } from "@cloudflare/workers-oauth-provider";

import { MyMCP } from "./mcp";


export default new OAuthProvider({

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

  apiRoute: "/mcp",

  apiHandler: MyMCP.serve("/mcp"),

  defaultHandler: AuthHandler,

});


```

TypeScript

```

import { OAuthProvider } from "@cloudflare/workers-oauth-provider";

import { MyMCP } from "./mcp";


export default new OAuthProvider({

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  clientRegistrationEndpoint: "/register",

  apiRoute: "/mcp",

  apiHandler: MyMCP.serve("/mcp"),

  defaultHandler: AuthHandler,

});


```

## Consent dialog security

When your MCP server proxies to third-party OAuth providers, you must implement your own consent dialog before forwarding users upstream. This prevents the "confused deputy" problem where attackers could exploit cached consent.

### CSRF protection

Without CSRF protection, attackers can trick users into approving malicious OAuth clients. Use a random token stored in a secure cookie:

* [  JavaScript ](#tab-panel-2962)
* [  TypeScript ](#tab-panel-2963)

JavaScript

```

// Generate CSRF token when showing consent form

function generateCSRFProtection() {

  const token = crypto.randomUUID();

  const setCookie = `__Host-CSRF_TOKEN=${token}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`;

  return { token, setCookie };

}


// Validate CSRF token on form submission

function validateCSRFToken(formData, request) {

  const tokenFromForm = formData.get("csrf_token");

  const cookieHeader = request.headers.get("Cookie") || "";

  const tokenFromCookie = cookieHeader

    .split(";")

    .find((c) => c.trim().startsWith("__Host-CSRF_TOKEN="))

    ?.split("=")[1];


  if (!tokenFromForm || !tokenFromCookie || tokenFromForm !== tokenFromCookie) {

    throw new Error("CSRF token mismatch");

  }


  // Clear cookie after use (one-time use)

  return {

    clearCookie: `__Host-CSRF_TOKEN=; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=0`,

  };

}


```

TypeScript

```

// Generate CSRF token when showing consent form

function generateCSRFProtection() {

  const token = crypto.randomUUID();

  const setCookie = `__Host-CSRF_TOKEN=${token}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`;

  return { token, setCookie };

}


// Validate CSRF token on form submission

function validateCSRFToken(formData: FormData, request: Request) {

  const tokenFromForm = formData.get("csrf_token");

  const cookieHeader = request.headers.get("Cookie") || "";

  const tokenFromCookie = cookieHeader

    .split(";")

    .find((c) => c.trim().startsWith("__Host-CSRF_TOKEN="))

    ?.split("=")[1];


  if (!tokenFromForm || !tokenFromCookie || tokenFromForm !== tokenFromCookie) {

    throw new Error("CSRF token mismatch");

  }


  // Clear cookie after use (one-time use)

  return {

    clearCookie: `__Host-CSRF_TOKEN=; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=0`,

  };

}


```

Include the token as a hidden field in your consent form:

```

<input type="hidden" name="csrf_token" value="${csrfToken}" />


```

### Input sanitization

User-controlled content (client names, logos, URIs) can execute malicious scripts if not sanitized:

* [  JavaScript ](#tab-panel-2966)
* [  TypeScript ](#tab-panel-2967)

JavaScript

```

function sanitizeText(text) {

  return text

    .replace(/&/g, "&amp;")

    .replace(/</g, "&lt;")

    .replace(/>/g, "&gt;")

    .replace(/"/g, "&quot;")

    .replace(/'/g, "&#039;");

}


function sanitizeUrl(url) {

  if (!url) return "";

  try {

    const parsed = new URL(url);

    // Only allow http/https - reject javascript:, data:, file:

    if (!["http:", "https:"].includes(parsed.protocol)) {

      return "";

    }

    return url;

  } catch {

    return "";

  }

}


// Always sanitize before rendering

const clientName = sanitizeText(client.clientName);

const logoUrl = sanitizeText(sanitizeUrl(client.logoUri));


```

TypeScript

```

function sanitizeText(text: string): string {

  return text

    .replace(/&/g, "&amp;")

    .replace(/</g, "&lt;")

    .replace(/>/g, "&gt;")

    .replace(/"/g, "&quot;")

    .replace(/'/g, "&#039;");

}


function sanitizeUrl(url: string): string {

  if (!url) return "";

  try {

    const parsed = new URL(url);

    // Only allow http/https - reject javascript:, data:, file:

    if (!["http:", "https:"].includes(parsed.protocol)) {

      return "";

    }

    return url;

  } catch {

    return "";

  }

}


// Always sanitize before rendering

const clientName = sanitizeText(client.clientName);

const logoUrl = sanitizeText(sanitizeUrl(client.logoUri));


```

### Content Security Policy

CSP headers instruct browsers to block dangerous content:

* [  JavaScript ](#tab-panel-2964)
* [  TypeScript ](#tab-panel-2965)

JavaScript

```

function buildSecurityHeaders(setCookie, nonce) {

  const cspDirectives = [

    "default-src 'none'",

    "script-src 'self'" + (nonce ? ` 'nonce-${nonce}'` : ""),

    "style-src 'self' 'unsafe-inline'",

    "img-src 'self' https:",

    "font-src 'self'",

    "form-action 'self'",

    "frame-ancestors 'none'", // Prevent clickjacking

    "base-uri 'self'",

    "connect-src 'self'",

  ].join("; ");


  return {

    "Content-Security-Policy": cspDirectives,

    "X-Frame-Options": "DENY",

    "X-Content-Type-Options": "nosniff",

    "Content-Type": "text/html; charset=utf-8",

    "Set-Cookie": setCookie,

  };

}


```

TypeScript

```

function buildSecurityHeaders(setCookie: string, nonce?: string): HeadersInit {

  const cspDirectives = [

    "default-src 'none'",

    "script-src 'self'" + (nonce ? ` 'nonce-${nonce}'` : ""),

    "style-src 'self' 'unsafe-inline'",

    "img-src 'self' https:",

    "font-src 'self'",

    "form-action 'self'",

    "frame-ancestors 'none'", // Prevent clickjacking

    "base-uri 'self'",

    "connect-src 'self'",

  ].join("; ");


  return {

    "Content-Security-Policy": cspDirectives,

    "X-Frame-Options": "DENY",

    "X-Content-Type-Options": "nosniff",

    "Content-Type": "text/html; charset=utf-8",

    "Set-Cookie": setCookie,

  };

}


```

## State handling

Between the consent dialog and the OAuth callback, you need to ensure it is the same user. Use a state token stored in KV with a short expiration:

* [  JavaScript ](#tab-panel-2968)
* [  TypeScript ](#tab-panel-2969)

JavaScript

```

// Create state token before redirecting to upstream provider

async function createOAuthState(oauthReqInfo, kv) {

  const stateToken = crypto.randomUUID();

  await kv.put(`oauth:state:${stateToken}`, JSON.stringify(oauthReqInfo), {

    expirationTtl: 600, // 10 minutes

  });

  return { stateToken };

}


// Bind state to browser session with a hashed cookie

async function bindStateToSession(stateToken) {

  const encoder = new TextEncoder();

  const hashBuffer = await crypto.subtle.digest(

    "SHA-256",

    encoder.encode(stateToken),

  );

  const hashHex = Array.from(new Uint8Array(hashBuffer))

    .map((b) => b.toString(16).padStart(2, "0"))

    .join("");


  return {

    setCookie: `__Host-CONSENTED_STATE=${hashHex}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`,

  };

}


// Validate state in callback

async function validateOAuthState(request, kv) {

  const url = new URL(request.url);

  const stateFromQuery = url.searchParams.get("state");


  if (!stateFromQuery) {

    throw new Error("Missing state parameter");

  }


  // Check state exists in KV

  const storedData = await kv.get(`oauth:state:${stateFromQuery}`);

  if (!storedData) {

    throw new Error("Invalid or expired state");

  }


  // Validate state matches session cookie

  // ... (hash comparison logic)


  await kv.delete(`oauth:state:${stateFromQuery}`);

  return JSON.parse(storedData);

}


```

TypeScript

```

// Create state token before redirecting to upstream provider

async function createOAuthState(oauthReqInfo: AuthRequest, kv: KVNamespace) {

  const stateToken = crypto.randomUUID();

  await kv.put(`oauth:state:${stateToken}`, JSON.stringify(oauthReqInfo), {

    expirationTtl: 600, // 10 minutes

  });

  return { stateToken };

}


// Bind state to browser session with a hashed cookie

async function bindStateToSession(stateToken: string) {

  const encoder = new TextEncoder();

  const hashBuffer = await crypto.subtle.digest(

    "SHA-256",

    encoder.encode(stateToken),

  );

  const hashHex = Array.from(new Uint8Array(hashBuffer))

    .map((b) => b.toString(16).padStart(2, "0"))

    .join("");


  return {

    setCookie: `__Host-CONSENTED_STATE=${hashHex}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600`,

  };

}


// Validate state in callback

async function validateOAuthState(request: Request, kv: KVNamespace) {

  const url = new URL(request.url);

  const stateFromQuery = url.searchParams.get("state");


  if (!stateFromQuery) {

    throw new Error("Missing state parameter");

  }


  // Check state exists in KV

  const storedData = await kv.get(`oauth:state:${stateFromQuery}`);

  if (!storedData) {

    throw new Error("Invalid or expired state");

  }


  // Validate state matches session cookie

  // ... (hash comparison logic)


  await kv.delete(`oauth:state:${stateFromQuery}`);

  return JSON.parse(storedData);

}


```

## Cookie security

### Why use the `__Host-` prefix?

The `__Host-` prefix prevents subdomain attacks, which is especially important on `*.workers.dev` domains:

* Must be set with `Secure` flag (HTTPS only)
* Must have `Path=/`
* Must not have a `Domain` attribute

Without `__Host-`, an attacker controlling `evil.workers.dev` could set cookies for your `mcp-server.workers.dev` domain.

### Multiple OAuth flows

If running multiple OAuth flows on the same domain, namespace your cookies:

```

__Host-CSRF_TOKEN_GITHUB

__Host-CSRF_TOKEN_GOOGLE

__Host-APPROVED_CLIENTS_GITHUB

__Host-APPROVED_CLIENTS_GOOGLE


```

## Approved clients registry

Maintain a registry of approved client IDs per user to avoid showing the consent dialog repeatedly:

* [  JavaScript ](#tab-panel-2960)
* [  TypeScript ](#tab-panel-2961)

JavaScript

```

async function addApprovedClient(request, clientId, cookieSecret) {

  const existingClients =

    (await getApprovedClientsFromCookie(request, cookieSecret)) || [];

  const updatedClients = [...new Set([...existingClients, clientId])];


  const payload = JSON.stringify(updatedClients);

  const signature = await signData(payload, cookieSecret); // HMAC-SHA256

  const cookieValue = `${signature}.${btoa(payload)}`;


  return `__Host-APPROVED_CLIENTS=${cookieValue}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=2592000`;

}


```

TypeScript

```

async function addApprovedClient(

  request: Request,

  clientId: string,

  cookieSecret: string,

) {

  const existingClients =

    (await getApprovedClientsFromCookie(request, cookieSecret)) || [];

  const updatedClients = [...new Set([...existingClients, clientId])];


  const payload = JSON.stringify(updatedClients);

  const signature = await signData(payload, cookieSecret); // HMAC-SHA256

  const cookieValue = `${signature}.${btoa(payload)}`;


  return `__Host-APPROVED_CLIENTS=${cookieValue}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=2592000`;

}


```

When reading the cookie, verify the HMAC signature before trusting the data. If the client is not in the approved list, show the consent dialog.

## Security checklist

| Protection         | Purpose                          |
| ------------------ | -------------------------------- |
| CSRF tokens        | Prevent forged consent approvals |
| Input sanitization | Prevent XSS in consent dialogs   |
| CSP headers        | Block injected scripts           |
| State binding      | Prevent session fixation         |
| \_\_Host- cookies  | Prevent subdomain attacks        |
| HMAC signatures    | Verify cookie integrity          |

## Next steps

[ MCP authorization ](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) OAuth and authentication for MCP servers. 

[ Build a remote MCP server ](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) Deploy MCP servers on Cloudflare. 

[ MCP security best practices ](https://modelcontextprotocol.io/specification/draft/basic/security%5Fbest%5Fpractices) Official MCP specification security guide. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/securing-mcp-server/","name":"Securing MCP servers"}}]}
```

---

---
title: Build a Slack Agent
description: This guide will show you how to build and deploy an AI-powered Slack bot on Cloudflare Workers that can:
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/slack-agent.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Build a Slack Agent

## Deploy your first Slack Agent

This guide will show you how to build and deploy an AI-powered Slack bot on Cloudflare Workers that can:

* Respond to direct messages
* Reply when mentioned in channels
* Maintain conversation context in threads
* Use AI to generate intelligent responses

Your Slack Agent will be a multi-tenant application, meaning a single deployment can serve multiple Slack workspaces. Each workspace gets its own isolated agent instance with dedicated storage, powered by the [Agents SDK](https://developers.cloudflare.com/agents/).

You can view the full code for this example [here ↗](https://github.com/cloudflare/awesome-agents/tree/69963298b359ddd66331e8b3b378bb9ae666629f/agents/slack).

## Prerequisites

Before you begin, you will need:

* A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up)
* [Node.js ↗](https://nodejs.org/) installed (v18 or later)
* A [Slack workspace ↗](https://slack.com/create) where you have permission to install apps
* An [OpenAI API key ↗](https://platform.openai.com/api-keys) (or another LLM provider)

## 1\. Create a Slack App

First, create a new Slack App that your agent will use to interact with Slack:

1. Go to [api.slack.com/apps ↗](https://api.slack.com/apps) and select **Create New App**.
2. Select **From scratch**.
3. Give your app a name (for example, "My AI Assistant") and select your workspace.
4. Select **Create App**.

### Configure OAuth & Permissions

In your Slack App settings, go to **OAuth & Permissions** and add the following **Bot Token Scopes**:

* `chat:write` — Send messages as the bot
* `chat:write.public` — Send messages to channels without joining
* `channels:history` — View messages in public channels
* `app_mentions:read` — Receive mentions
* `im:write` — Send direct messages
* `im:history` — View direct message history

### Enable Event Subscriptions

You will later configure the Event Subscriptions URL after deploying your agent. But for now, go to **Event Subscriptions** in your Slack App settings and prepare to enable it.

Subscribe to the following bot events:

* `app_mention` — When the bot is @mentioned
* `message.im` — Direct messages to the bot

Do not enable it yet. You will enable it after deployment.

### Get your Slack credentials

From your Slack App settings, collect these values:

1. **Basic Information** \> **App Credentials**:  
   * **Client ID**  
   * **Client Secret**  
   * **Signing Secret**

Keep these handy — you will need them in the next step.

## 2\. Create your Slack Agent project

1. Create a new project for your Slack Agent:

 npm  yarn  pnpm 

```
npm create cloudflare@latest -- my-slack-agent
```

```
yarn create cloudflare my-slack-agent
```

```
pnpm create cloudflare@latest my-slack-agent
```

1. Navigate into your project:

Terminal window

```

cd my-slack-agent


```

1. Install the required dependencies:

Terminal window

```

npm install agents openai


```

## 3\. Set up your environment variables

1. Create a `.env` file in your project root for local development secrets:

Terminal window

```

touch .env


```

1. Add your credentials to `.env`:

Terminal window

```

SLACK_CLIENT_ID="your-slack-client-id"

SLACK_CLIENT_SECRET="your-slack-client-secret"

SLACK_SIGNING_SECRET="your-slack-signing-secret"

OPENAI_API_KEY="your-openai-api-key"

OPENAI_BASE_URL="https://gateway.ai.cloudflare.com/v1/YOUR_ACCOUNT_ID/YOUR_GATEWAY/openai"


```

Note

The `OPENAI_BASE_URL` is optional but recommended. Using [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) gives you caching, rate limiting, and analytics for your AI requests.

1. Update your `wrangler.jsonc` to configure your Agent:

* [  wrangler.jsonc ](#tab-panel-2970)
* [  wrangler.toml ](#tab-panel-2971)

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "name": "my-slack-agent",

  "main": "src/index.ts",

  // Set this to today's date

  "compatibility_date": "2026-04-03",

  "compatibility_flags": [

    "nodejs_compat"

  ],

  "durable_objects": {

    "bindings": [

      {

        "name": "MyAgent",

        "class_name": "MyAgent",

        "script_name": "my-slack-agent"

      }

    ]

  },

  "migrations": [

    {

      "tag": "v1",

      "new_classes": [

        "MyAgent"

      ]

    }

  ]

}


```

```

"$schema" = "./node_modules/wrangler/config-schema.json"

name = "my-slack-agent"

main = "src/index.ts"

# Set this to today's date

compatibility_date = "2026-04-03"

compatibility_flags = [ "nodejs_compat" ]


[[durable_objects.bindings]]

name = "MyAgent"

class_name = "MyAgent"

script_name = "my-slack-agent"


[[migrations]]

tag = "v1"

new_classes = [ "MyAgent" ]


```

## 4\. Create your Slack Agent

1. First, create the base `SlackAgent` class at `src/slack.ts`. This class handles OAuth, request verification, and event routing. You can view the [full implementation on GitHub ↗](https://github.com/cloudflare/awesome-agents/blob/69963298b359ddd66331e8b3b378bb9ae666629f/agents/slack/src/slack.ts).
2. Now create your agent implementation at `src/index.ts`:

TypeScript

```

import { env } from "cloudflare:workers";

import { SlackAgent } from "./slack";

import { OpenAI } from "openai";


const openai = new OpenAI({

  apiKey: env.OPENAI_API_KEY,

  baseURL: env.OPENAI_BASE_URL,

});


type SlackMsg = {

  user?: string;

  text?: string;

  ts: string;

  thread_ts?: string;

  subtype?: string;

  bot_id?: string;

};


function normalizeForLLM(msgs: SlackMsg[], selfUserId: string) {

  return msgs.map((m) => {

    const role = m.user && m.user !== selfUserId ? "user" : "assistant";

    const text = (m.text ?? "").replace(/<@([A-Z0-9]+)>/g, "@$1");

    return { role, content: text };

  });

}


export class MyAgent extends SlackAgent {

  async generateAIReply(conversation: SlackMsg[]) {

    const selfId = await this.ensureAppUserId();

    const messages = normalizeForLLM(conversation, selfId);


    const system = `You are a helpful AI assistant in Slack.

Be brief, specific, and actionable. If you're unsure, ask a single clarifying question.`;


    const input = [{ role: "system", content: system }, ...messages];


    const response = await openai.chat.completions.create({

      model: "gpt-4o-mini",

      messages: input,

    });


    const msg = response.choices[0].message.content;

    if (!msg) throw new Error("No message from AI");


    return msg;

  }


  async onSlackEvent(event: { type: string } & Record<string, unknown>) {

    // Ignore bot messages and subtypes (edits, joins, etc.)

    if (event.bot_id || event.subtype) return;


    // Handle direct messages

    if (event.type === "message") {

      const e = event as unknown as SlackMsg & { channel: string };

      const isDM = (e.channel || "").startsWith("D");

      const mentioned = (e.text || "").includes(

        `<@${await this.ensureAppUserId()}>`,

      );


      if (!isDM && !mentioned) return;


      const conversation = await this.fetchConversation(e.channel);

      const content = await this.generateAIReply(conversation);

      await this.sendMessage(content, { channel: e.channel });

      return;

    }


    // Handle @mentions in channels

    if (event.type === "app_mention") {

      const e = event as unknown as SlackMsg & {

        channel: string;

        text?: string;

      };

      const thread = await this.fetchThread(e.channel, e.thread_ts || e.ts);

      const content = await this.generateAIReply(thread);

      await this.sendMessage(content, {

        channel: e.channel,

        thread_ts: e.thread_ts || e.ts,

      });

      return;

    }

  }

}


export default MyAgent.listen({

  clientId: env.SLACK_CLIENT_ID,

  clientSecret: env.SLACK_CLIENT_SECRET,

  slackSigningSecret: env.SLACK_SIGNING_SECRET,

  scopes: [

    "chat:write",

    "chat:write.public",

    "channels:history",

    "app_mentions:read",

    "im:write",

    "im:history",

  ],

});


```

## 5\. Test locally

Start your development server:

Terminal window

```

npm run dev


```

Your agent is now running at `http://localhost:8787`.

### Configure Slack Event Subscriptions

Now that your agent is running locally, you need to expose it to Slack. Use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) to create a secure tunnel:

Terminal window

```

npx cloudflared tunnel --url http://localhost:8787


```

This will output a public URL like `https://random-subdomain.trycloudflare.com`.

Go back to your Slack App settings:

1. Go to **Event Subscriptions**.
2. Toggle **Enable Events** to **On**.
3. Enter your Request URL: `https://random-subdomain.trycloudflare.com/slack`.
4. Slack will send a verification request — if your agent is running correctly, it should show **Verified**.
5. Under **Subscribe to bot events**, add:  
   * `app_mention`  
   * `message.im`
6. Select **Save Changes**.

Note

Cloudflare Tunnel URLs are temporary. When testing locally, you will need to update the Request URL each time you restart the tunnel.

### Install your app to Slack

Visit `http://localhost:8787/install` in your browser. This will redirect you to Slack's authorization page. Select **Allow** to install the app to your workspace.

After authorization, you should see "Successfully registered!" in your browser.

### Test your agent

Open Slack. Then:

1. Send a DM to your bot — it should respond with an AI-generated message.
2. Mention your bot in a channel (e.g., `@My AI Assistant hello`) — it should reply in a thread.

If everything works, you're ready to deploy to production!

## 6\. Deploy to production

1. Before deploying, add your secrets to Cloudflare:

Terminal window

```

npx wrangler secret put SLACK_CLIENT_ID

npx wrangler secret put SLACK_CLIENT_SECRET

npx wrangler secret put SLACK_SIGNING_SECRET

npx wrangler secret put OPENAI_API_KEY

npx wrangler secret put OPENAI_BASE_URL


```

Note

You can skip `OPENAI_BASE_URL` if you're not using AI Gateway.

1. Deploy your agent:

Terminal window

```

npx wrangler deploy


```

After deploying, you will get a production URL like:

```

https://my-slack-agent.your-account.workers.dev


```

### Update Slack Event Subscriptions

Go back to your Slack App settings:

1. Go to **Event Subscriptions**.
2. Update the Request URL to your production URL: `https://my-slack-agent.your-account.workers.dev/slack`.
3. Select **Save Changes**.

### Distribute your app

Now that your agent is deployed, you can share it with others:

* **Single workspace**: Install it via `https://my-slack-agent.your-account.workers.dev/install`.
* **Public distribution**: Submit your app to the [Slack App Directory ↗](https://api.slack.com/start/distributing).

Each workspace that installs your app will get its own isolated agent instance with dedicated storage.

## How it works

### Multi-tenancy with Durable Objects

Your Slack Agent uses [Durable Objects](https://developers.cloudflare.com/durable-objects/) to provide isolated, stateful instances for each Slack workspace:

* Each workspace's `team_id` is used as the Durable Object ID.
* Each agent instance stores its own Slack access token in KV storage.
* Conversations are fetched on-demand from Slack's API.
* All agent logic runs in an isolated, consistent environment.

### OAuth flow

The agent handles Slack's OAuth 2.0 flow:

1. User visits `/install` \> redirected to Slack authorization.
2. User selects **Allow** \> Slack redirects to `/accept` with an authorization code.
3. Agent exchanges code for access token.
4. Agent stores token in the workspace's Durable Object.

### Event handling

When Slack sends an event:

1. Request arrives at `/slack` endpoint.
2. Agent verifies the request signature using HMAC-SHA256.
3. Agent routes the event to the correct workspace's Durable Object.
4. `onSlackEvent` method processes the event and generates a response.

## Customizing your agent

### Change the AI model

Update the model in `src/index.ts`:

TypeScript

```

const response = await openai.chat.completions.create({

  model: "gpt-4o", // or any other model

  messages: input,

});


```

### Add conversation memory

Store conversation history in Durable Object storage:

TypeScript

```

async storeMessage(channel: string, message: SlackMsg) {

  const history = await this.ctx.storage.kv.get(`history:${channel}`) || [];

  history.push(message);

  await this.ctx.storage.kv.put(`history:${channel}`, history);

}


```

### React to specific keywords

Add custom logic in `onSlackEvent`:

TypeScript

```

async onSlackEvent(event: { type: string } & Record<string, unknown>) {

  if (event.type === "message") {

    const e = event as unknown as SlackMsg & { channel: string };


    if (e.text?.includes("help")) {

      await this.sendMessage("Here's how I can help...", {

        channel: e.channel

      });

      return;

    }

  }


  // ... rest of your event handling

}


```

### Use different LLM providers

Replace OpenAI with [Workers AI](https://developers.cloudflare.com/workers-ai/):

TypeScript

```

import { Ai } from "@cloudflare/ai";


export class MyAgent extends SlackAgent {

  async generateAIReply(conversation: SlackMsg[]) {

    const ai = new Ai(this.ctx.env.AI);

    const response = await ai.run("@cf/meta/llama-3-8b-instruct", {

      messages: normalizeForLLM(conversation, await this.ensureAppUserId()),

    });

    return response.response;

  }

}


```

## Next steps

* Add [Slack Interactive Components ↗](https://api.slack.com/interactivity) (buttons, modals)
* Connect your Agent to an [MCP server](https://developers.cloudflare.com/agents/api-reference/mcp-client-api/)
* Add rate limiting to prevent abuse
* Implement conversation state management
* Use [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) to track usage
* Add [schedules](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/) for scheduled tasks

## Related resources

[ Agents documentation ](https://developers.cloudflare.com/agents/) Complete Agents framework documentation. 

[ Durable Objects ](https://developers.cloudflare.com/durable-objects/) Learn about the underlying stateful infrastructure. 

[ Slack API ](https://api.slack.com/) Official Slack API documentation. 

[ OpenAI API ](https://platform.openai.com/docs/) Official OpenAI API documentation. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/slack-agent/","name":"Build a Slack Agent"}}]}
```

---

---
title: Test a Remote MCP Server
description: Remote, authorized connections are an evolving part of the Model Context Protocol (MCP) specification. Not all MCP clients support remote connections yet.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

### Tags

[ MCP ](https://developers.cloudflare.com/search/?tags=MCP) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/test-remote-mcp-server.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Test a Remote MCP Server

Remote, authorized connections are an evolving part of the [Model Context Protocol (MCP) specification ↗](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/). Not all MCP clients support remote connections yet.

This guide will show you options for how to start using your remote MCP server with MCP clients that support remote connections. If you haven't yet created and deployed a remote MCP server, you should follow the [Build a Remote MCP Server](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) guide first.

## The Model Context Protocol (MCP) inspector

The [@modelcontextprotocol/inspector package ↗](https://github.com/modelcontextprotocol/inspector) is a visual testing tool for MCP servers.

1. Open a terminal and run the following command:  
Terminal window  
```  
npx @modelcontextprotocol/inspector  
```  
```  
🚀 MCP Inspector is up and running at:  
  http://localhost:5173/?MCP_PROXY_AUTH_TOKEN=46ab..cd3  
🌐 Opening browser...  
```  
The MCP Inspector will launch in your web browser. You can also launch it manually by opening a browser and going to `http://localhost:<PORT>`. Check the command output for the local port where MCP Inspector is running. In this example, MCP Inspector is served on port `5173`.
2. In the MCP inspector, enter the URL of your MCP server (for example, `http://localhost:8788/mcp`). Select **Connect**.  
You can connect to an MCP server running on your local machine or a remote MCP server running on Cloudflare.
3. If your server requires authentication, the connection will fail. To authenticate:  
   1. In MCP Inspector, select **Open Auth settings**.  
   2. Select **Quick OAuth Flow**.  
   3. Once you have authenticated with the OAuth provider, you will be redirected back to MCP Inspector. Select **Connect**.

You should see the **List tools** button, which will list the tools that your MCP server exposes.

## Connect your remote MCP server to Cloudflare Workers AI Playground

Visit the [Workers AI Playground ↗](https://playground.ai.cloudflare.com/), enter your MCP server URL, and click "Connect". Once authenticated (if required), you should see your tools listed and they will be available to the AI model in the chat.

## Connect your remote MCP server to Claude Desktop via a local proxy

You can use the [mcp-remote local proxy ↗](https://www.npmjs.com/package/mcp-remote) to connect Claude Desktop to your remote MCP server. This lets you test what an interaction with your remote MCP server will be like with a real-world MCP client.

1. Open Claude Desktop and navigate to Settings -> Developer -> Edit Config. This opens the configuration file that controls which MCP servers Claude can access.
2. Replace the content with a configuration like this:

```

{

  "mcpServers": {

    "my-server": {

      "command": "npx",

      "args": ["mcp-remote", "http://my-mcp-server.my-account.workers.dev/mcp"]

    }

  }

}


```

1. Save the file and restart Claude Desktop (command/ctrl + R). When Claude restarts, a browser window will open showing your OAuth login page. Complete the authorization flow to grant Claude access to your MCP server.

Once authenticated, you'll be able to see your tools by clicking the tools icon in the bottom right corner of Claude's interface.

## Connect your remote MCP server to Cursor

Connect [Cursor ↗](https://cursor.com/docs/context/mcp) to your remote MCP server by editing the project's `.cursor/mcp.json` file or a global `~/.cursor/mcp.json` file and adding the following configuration:

```

{

  "mcpServers": {

    "my-server": {

      "url": "http://my-mcp-server.my-account.workers.dev/mcp"

    }

  }

}


```

## Connect your remote MCP server to Windsurf

You can connect your remote MCP server to [Windsurf ↗](https://docs.windsurf.com) by editing the [mcp\_config.json file ↗](https://docs.windsurf.com/windsurf/cascade/mcp), and adding the following configuration:

```

{

  "mcpServers": {

    "my-server": {

      "serverUrl": "http://my-mcp-server.my-account.workers.dev/mcp"

    }

  }

}


```

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/test-remote-mcp-server/","name":"Test a Remote MCP Server"}}]}
```

---

---
title: Webhooks
description: Receive webhook events from external services and route them to dedicated agent instances. Each webhook source (repository, customer, device) can have its own agent with isolated state, persistent storage, and real-time client connections.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/guides/webhooks.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Webhooks

Receive webhook events from external services and route them to dedicated agent instances. Each webhook source (repository, customer, device) can have its own agent with isolated state, persistent storage, and real-time client connections.

## Quick start

* [  JavaScript ](#tab-panel-3000)
* [  TypeScript ](#tab-panel-3001)

JavaScript

```

import { Agent, getAgentByName, routeAgentRequest } from "agents";


// Agent that handles webhooks for a specific entity

export class WebhookAgent extends Agent {

  async onRequest(request) {

    if (request.method !== "POST") {

      return new Response("Method not allowed", { status: 405 });

    }


    // Verify the webhook signature

    const signature = request.headers.get("X-Hub-Signature-256");

    const body = await request.text();


    if (

      !(await this.verifySignature(body, signature, this.env.WEBHOOK_SECRET))

    ) {

      return new Response("Invalid signature", { status: 401 });

    }


    // Process the webhook payload

    const payload = JSON.parse(body);

    await this.processEvent(payload);


    return new Response("OK", { status: 200 });

  }


  async verifySignature(payload, signature, secret) {

    if (!signature) return false;


    const encoder = new TextEncoder();

    const key = await crypto.subtle.importKey(

      "raw",

      encoder.encode(secret),

      { name: "HMAC", hash: "SHA-256" },

      false,

      ["sign"],

    );


    const signatureBytes = await crypto.subtle.sign(

      "HMAC",

      key,

      encoder.encode(payload),

    );

    const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))

      .map((b) => b.toString(16).padStart(2, "0"))

      .join("")}`;


    return signature === expected;

  }


  async processEvent(payload) {

    // Store event, update state, trigger actions...

  }

}


// Route webhooks to the right agent instance

export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    // Webhook endpoint: POST /webhooks/:entityId

    if (url.pathname.startsWith("/webhooks/") && request.method === "POST") {

      const entityId = url.pathname.split("/")[2];

      const agent = await getAgentByName(env.WebhookAgent, entityId);

      return agent.fetch(request);

    }


    // Default routing for WebSocket connections

    return (

      (await routeAgentRequest(request, env)) ||

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

import { Agent, getAgentByName, routeAgentRequest } from "agents";


// Agent that handles webhooks for a specific entity

export class WebhookAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    if (request.method !== "POST") {

      return new Response("Method not allowed", { status: 405 });

    }


    // Verify the webhook signature

    const signature = request.headers.get("X-Hub-Signature-256");

    const body = await request.text();


    if (

      !(await this.verifySignature(body, signature, this.env.WEBHOOK_SECRET))

    ) {

      return new Response("Invalid signature", { status: 401 });

    }


    // Process the webhook payload

    const payload = JSON.parse(body);

    await this.processEvent(payload);


    return new Response("OK", { status: 200 });

  }


  private async verifySignature(

    payload: string,

    signature: string | null,

    secret: string,

  ): Promise<boolean> {

    if (!signature) return false;


    const encoder = new TextEncoder();

    const key = await crypto.subtle.importKey(

      "raw",

      encoder.encode(secret),

      { name: "HMAC", hash: "SHA-256" },

      false,

      ["sign"],

    );


    const signatureBytes = await crypto.subtle.sign(

      "HMAC",

      key,

      encoder.encode(payload),

    );

    const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))

      .map((b) => b.toString(16).padStart(2, "0"))

      .join("")}`;


    return signature === expected;

  }


  private async processEvent(payload: unknown) {

    // Store event, update state, trigger actions...

  }

}


// Route webhooks to the right agent instance

export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);


    // Webhook endpoint: POST /webhooks/:entityId

    if (url.pathname.startsWith("/webhooks/") && request.method === "POST") {

      const entityId = url.pathname.split("/")[2];

      const agent = await getAgentByName(env.WebhookAgent, entityId);

      return agent.fetch(request);

    }


    // Default routing for WebSocket connections

    return (

      (await routeAgentRequest(request, env)) ||

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

## Use cases

Webhooks combined with agents enable patterns where each external entity gets its own isolated, stateful agent instance.

### Developer tools

| Use case                 | Description                                                                |
| ------------------------ | -------------------------------------------------------------------------- |
| **GitHub Repo Monitor**  | One agent per repository tracking commits, PRs, issues, and stars          |
| **CI/CD Pipeline Agent** | React to build/deploy events, notify on failures, track deployment history |
| **Linear/Jira Tracker**  | Auto-triage issues, assign based on content, track resolution times        |

### E-commerce and payments

| Use case                   | Description                                                           |
| -------------------------- | --------------------------------------------------------------------- |
| **Stripe Customer Agent**  | One agent per customer tracking payments, subscriptions, and disputes |
| **Shopify Order Agent**    | Order lifecycle from creation to fulfillment with inventory sync      |
| **Payment Reconciliation** | Match webhook events to internal records, flag discrepancies          |

### Communication and notifications

| Use case             | Description                                                             |
| -------------------- | ----------------------------------------------------------------------- |
| **Twilio SMS/Voice** | Conversational agents triggered by inbound messages or calls            |
| **Slack Bot**        | Respond to slash commands, button clicks, and interactive messages      |
| **Email Tracking**   | SendGrid/Mailgun delivery events, bounce handling, engagement analytics |

### IoT and infrastructure

| Use case              | Description                                                  |
| --------------------- | ------------------------------------------------------------ |
| **Device Telemetry**  | One agent per device processing sensor data streams          |
| **Alert Aggregation** | Collect alerts from PagerDuty, Datadog, or custom monitoring |
| **Home Automation**   | React to IFTTT/Zapier triggers with persistent state         |

### SaaS integrations

| Use case             | Description                                                     |
| -------------------- | --------------------------------------------------------------- |
| **CRM Sync**         | Salesforce/HubSpot contact and deal updates                     |
| **Calendar Agent**   | Google Calendar event notifications and scheduling              |
| **Form Submissions** | Typeform, Tally, or custom form webhooks with follow-up actions |

## Routing webhooks to agents

The key pattern is extracting an entity identifier from the webhook and using `getAgentByName()` to route to a dedicated agent instance.

### Extract entity from payload

Most webhooks include an identifier in the payload:

* [  JavaScript ](#tab-panel-2976)
* [  TypeScript ](#tab-panel-2977)

JavaScript

```

export default {

  async fetch(request, env) {

    if (request.method === "POST" && url.pathname === "/webhooks/github") {

      const payload = await request.clone().json();


      // Extract entity ID from payload

      const repoFullName = payload.repository?.full_name;

      if (!repoFullName) {

        return new Response("Missing repository", { status: 400 });

      }


      // Sanitize for use as agent name

      const agentName = repoFullName.toLowerCase().replace(/\//g, "-");


      // Route to dedicated agent

      const agent = await getAgentByName(env.RepoAgent, agentName);

      return agent.fetch(request);

    }

  },

};


```

TypeScript

```

export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    if (request.method === "POST" && url.pathname === "/webhooks/github") {

      const payload = await request.clone().json();


      // Extract entity ID from payload

      const repoFullName = payload.repository?.full_name;

      if (!repoFullName) {

        return new Response("Missing repository", { status: 400 });

      }


      // Sanitize for use as agent name

      const agentName = repoFullName.toLowerCase().replace(/\//g, "-");


      // Route to dedicated agent

      const agent = await getAgentByName(env.RepoAgent, agentName);

      return agent.fetch(request);

    }

  },

} satisfies ExportedHandler<Env>;


```

### Extract entity from URL

Alternatively, include the entity ID in the webhook URL:

* [  JavaScript ](#tab-panel-2972)
* [  TypeScript ](#tab-panel-2973)

JavaScript

```

// Webhook URL: https://your-worker.dev/webhooks/stripe/cus_123456

if (url.pathname.startsWith("/webhooks/stripe/")) {

  const customerId = url.pathname.split("/")[3]; // "cus_123456"

  const agent = await getAgentByName(env.StripeAgent, customerId);

  return agent.fetch(request);

}


```

TypeScript

```

// Webhook URL: https://your-worker.dev/webhooks/stripe/cus_123456

if (url.pathname.startsWith("/webhooks/stripe/")) {

  const customerId = url.pathname.split("/")[3]; // "cus_123456"

  const agent = await getAgentByName(env.StripeAgent, customerId);

  return agent.fetch(request);

}


```

### Extract entity from headers

Some services include identifiers in headers:

* [  JavaScript ](#tab-panel-2974)
* [  TypeScript ](#tab-panel-2975)

JavaScript

```

// Slack sends workspace info in headers

const teamId = request.headers.get("X-Slack-Team-Id");

if (teamId) {

  const agent = await getAgentByName(env.SlackAgent, teamId);

  return agent.fetch(request);

}


```

TypeScript

```

// Slack sends workspace info in headers

const teamId = request.headers.get("X-Slack-Team-Id");

if (teamId) {

  const agent = await getAgentByName(env.SlackAgent, teamId);

  return agent.fetch(request);

}


```

## Signature verification

Always verify webhook signatures to ensure requests are authentic. Most providers use HMAC-SHA256.

### HMAC-SHA256 pattern

* [  JavaScript ](#tab-panel-2986)
* [  TypeScript ](#tab-panel-2987)

JavaScript

```

async function verifySignature(payload, signature, secret) {

  if (!signature) return false;


  const encoder = new TextEncoder();

  const key = await crypto.subtle.importKey(

    "raw",

    encoder.encode(secret),

    { name: "HMAC", hash: "SHA-256" },

    false,

    ["sign"],

  );


  const signatureBytes = await crypto.subtle.sign(

    "HMAC",

    key,

    encoder.encode(payload),

  );


  const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))

    .map((b) => b.toString(16).padStart(2, "0"))

    .join("")}`;


  // Use timing-safe comparison in production

  return signature === expected;

}


```

TypeScript

```

async function verifySignature(

  payload: string,

  signature: string | null,

  secret: string,

): Promise<boolean> {

  if (!signature) return false;


  const encoder = new TextEncoder();

  const key = await crypto.subtle.importKey(

    "raw",

    encoder.encode(secret),

    { name: "HMAC", hash: "SHA-256" },

    false,

    ["sign"],

  );


  const signatureBytes = await crypto.subtle.sign(

    "HMAC",

    key,

    encoder.encode(payload),

  );


  const expected = `sha256=${Array.from(new Uint8Array(signatureBytes))

    .map((b) => b.toString(16).padStart(2, "0"))

    .join("")}`;


  // Use timing-safe comparison in production

  return signature === expected;

}


```

### Provider-specific headers

| Provider | Signature Header      | Algorithm                    |
| -------- | --------------------- | ---------------------------- |
| GitHub   | X-Hub-Signature-256   | HMAC-SHA256                  |
| Stripe   | Stripe-Signature      | HMAC-SHA256 (with timestamp) |
| Twilio   | X-Twilio-Signature    | HMAC-SHA1                    |
| Slack    | X-Slack-Signature     | HMAC-SHA256 (with timestamp) |
| Shopify  | X-Shopify-Hmac-Sha256 | HMAC-SHA256 (base64)         |

## Processing webhooks

### The onRequest handler

Use `onRequest()` to handle incoming webhooks in your agent:

* [  JavaScript ](#tab-panel-2994)
* [  TypeScript ](#tab-panel-2995)

JavaScript

```

export class WebhookAgent extends Agent {

  async onRequest(request) {

    // 1. Validate method

    if (request.method !== "POST") {

      return new Response("Method not allowed", { status: 405 });

    }


    // 2. Get event type from headers

    const eventType = request.headers.get("X-Event-Type");


    // 3. Verify signature

    const signature = request.headers.get("X-Signature");

    const body = await request.text();


    if (!(await this.verifySignature(body, signature))) {

      return new Response("Invalid signature", { status: 401 });

    }


    // 4. Parse and process

    const payload = JSON.parse(body);

    await this.handleEvent(eventType, payload);


    // 5. Respond quickly

    return new Response("OK", { status: 200 });

  }


  async handleEvent(type, payload) {

    // Update state (broadcasts to connected clients)

    this.setState({

      ...this.state,

      lastEventType: type,

      lastEventTime: new Date().toISOString(),

    });


    // Store in SQL for history

    this

      .sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;

  }

}


```

TypeScript

```

export class WebhookAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    // 1. Validate method

    if (request.method !== "POST") {

      return new Response("Method not allowed", { status: 405 });

    }


    // 2. Get event type from headers

    const eventType = request.headers.get("X-Event-Type");


    // 3. Verify signature

    const signature = request.headers.get("X-Signature");

    const body = await request.text();


    if (!(await this.verifySignature(body, signature))) {

      return new Response("Invalid signature", { status: 401 });

    }


    // 4. Parse and process

    const payload = JSON.parse(body);

    await this.handleEvent(eventType, payload);


    // 5. Respond quickly

    return new Response("OK", { status: 200 });

  }


  private async handleEvent(type: string, payload: unknown) {

    // Update state (broadcasts to connected clients)

    this.setState({

      ...this.state,

      lastEventType: type,

      lastEventTime: new Date().toISOString(),

    });


    // Store in SQL for history

    this

      .sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;

  }

}


```

## Storing webhook events

Use SQLite to persist webhook events for history and replay.

### Event table schema

* [  JavaScript ](#tab-panel-2984)
* [  TypeScript ](#tab-panel-2985)

JavaScript

```

class WebhookAgent extends Agent {

  async onStart() {

    this.sql`

      CREATE TABLE IF NOT EXISTS events (

        id TEXT PRIMARY KEY,

        type TEXT NOT NULL,

        action TEXT,

        title TEXT NOT NULL,

        description TEXT,

        url TEXT,

        actor TEXT,

        payload TEXT,

        timestamp TEXT NOT NULL

      )

    `;


    this.sql`

      CREATE INDEX IF NOT EXISTS idx_events_timestamp

      ON events(timestamp DESC)

    `;

  }

}


```

TypeScript

```

class WebhookAgent extends Agent {

  async onStart(): Promise<void> {

    this.sql`

      CREATE TABLE IF NOT EXISTS events (

        id TEXT PRIMARY KEY,

        type TEXT NOT NULL,

        action TEXT,

        title TEXT NOT NULL,

        description TEXT,

        url TEXT,

        actor TEXT,

        payload TEXT,

        timestamp TEXT NOT NULL

      )

    `;


    this.sql`

      CREATE INDEX IF NOT EXISTS idx_events_timestamp

      ON events(timestamp DESC)

    `;

  }

}


```

### Cleanup old events

Prevent unbounded growth by keeping only recent events:

* [  JavaScript ](#tab-panel-2978)
* [  TypeScript ](#tab-panel-2979)

JavaScript

```

// Keep last 100 events

this.sql`

  DELETE FROM events WHERE id NOT IN (

    SELECT id FROM events ORDER BY timestamp DESC LIMIT 100

  )

`;


// Or delete events older than 30 days

this.sql`

  DELETE FROM events

  WHERE timestamp < datetime('now', '-30 days')

`;


```

TypeScript

```

// Keep last 100 events

this.sql`

  DELETE FROM events WHERE id NOT IN (

    SELECT id FROM events ORDER BY timestamp DESC LIMIT 100

  )

`;


// Or delete events older than 30 days

this.sql`

  DELETE FROM events

  WHERE timestamp < datetime('now', '-30 days')

`;


```

### Query events

* [  JavaScript ](#tab-panel-2990)
* [  TypeScript ](#tab-panel-2991)

JavaScript

```

import { Agent, callable } from "agents";


class WebhookAgent extends Agent {

  @callable()

  getEvents(limit = 20) {

    return [

      ...this.sql`

      SELECT * FROM events

      ORDER BY timestamp DESC

      LIMIT ${limit}

    `,

    ];

  }


  @callable()

  getEventsByType(type, limit = 20) {

    return [

      ...this.sql`

      SELECT * FROM events

      WHERE type = ${type}

      ORDER BY timestamp DESC

      LIMIT ${limit}

    `,

    ];

  }

}


```

TypeScript

```

import { Agent, callable } from "agents";


class WebhookAgent extends Agent {

  @callable()

  getEvents(limit = 20) {

    return [

      ...this.sql`

      SELECT * FROM events

      ORDER BY timestamp DESC

      LIMIT ${limit}

    `,

    ];

  }


  @callable()

  getEventsByType(type: string, limit = 20) {

    return [

      ...this.sql`

      SELECT * FROM events

      WHERE type = ${type}

      ORDER BY timestamp DESC

      LIMIT ${limit}

    `,

    ];

  }

}


```

## Real-time broadcasting

When a webhook arrives, update agent state to automatically broadcast to connected WebSocket clients.

* [  JavaScript ](#tab-panel-2980)
* [  TypeScript ](#tab-panel-2981)

JavaScript

```

class WebhookAgent extends Agent {

  async processWebhook(eventType, payload) {

    // Update state - this automatically broadcasts to all connected clients

    this.setState({

      ...this.state,

      stats: payload.stats,

      lastEvent: {

        type: eventType,

        timestamp: new Date().toISOString(),

      },

    });

  }

}


```

TypeScript

```

class WebhookAgent extends Agent {

  private async processWebhook(eventType: string, payload: WebhookPayload) {

    // Update state - this automatically broadcasts to all connected clients

    this.setState({

      ...this.state,

      stats: payload.stats,

      lastEvent: {

        type: eventType,

        timestamp: new Date().toISOString(),

      },

    });

  }

}


```

On the client side:

```

import { useAgent } from "agents/react";


function Dashboard() {

  const [state, setState] = useState(null);


  const agent = useAgent({

    agent: "webhook-agent",

    name: "my-entity-id",

    onStateUpdate: (newState) => {

      setState(newState); // Automatically updates when webhooks arrive

    },

  });


  return <div>Last event: {state?.lastEvent?.type}</div>;

}


```

## Patterns

### Event deduplication

Prevent processing duplicate events using event IDs:

* [  JavaScript ](#tab-panel-2988)
* [  TypeScript ](#tab-panel-2989)

JavaScript

```

class WebhookAgent extends Agent {

  async handleEvent(eventId, payload) {

    // Check if already processed

    const existing = [

      ...this.sql`

      SELECT id FROM events WHERE id = ${eventId}

    `,

    ];


    if (existing.length > 0) {

      console.log(`Event ${eventId} already processed, skipping`);

      return;

    }


    // Process and store

    await this.processPayload(payload);

    this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;

  }

}


```

TypeScript

```

class WebhookAgent extends Agent {

  async handleEvent(eventId: string, payload: unknown) {

    // Check if already processed

    const existing = [

      ...this.sql`

      SELECT id FROM events WHERE id = ${eventId}

    `,

    ];


    if (existing.length > 0) {

      console.log(`Event ${eventId} already processed, skipping`);

      return;

    }


    // Process and store

    await this.processPayload(payload);

    this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;

  }

}


```

### Respond quickly, process asynchronously

Webhook providers expect fast responses. Use the queue for heavy processing:

* [  JavaScript ](#tab-panel-2992)
* [  TypeScript ](#tab-panel-2993)

JavaScript

```

class WebhookAgent extends Agent {

  async onRequest(request) {

    const payload = await request.json();


    // Quick validation

    if (!this.isValid(payload)) {

      return new Response("Invalid", { status: 400 });

    }


    // Queue heavy processing

    await this.queue("processWebhook", payload);


    // Respond immediately

    return new Response("Accepted", { status: 202 });

  }


  async processWebhook(payload) {

    // Heavy processing happens here, after response sent

    await this.enrichData(payload);

    await this.notifyDownstream(payload);

    await this.updateAnalytics(payload);

  }

}


```

TypeScript

```

class WebhookAgent extends Agent {

  async onRequest(request: Request): Promise<Response> {

    const payload = await request.json();


    // Quick validation

    if (!this.isValid(payload)) {

      return new Response("Invalid", { status: 400 });

    }


    // Queue heavy processing

    await this.queue("processWebhook", payload);


    // Respond immediately

    return new Response("Accepted", { status: 202 });

  }


  async processWebhook(payload: WebhookPayload) {

    // Heavy processing happens here, after response sent

    await this.enrichData(payload);

    await this.notifyDownstream(payload);

    await this.updateAnalytics(payload);

  }

}


```

### Multi-provider routing

Handle webhooks from multiple services in one Worker:

* [  JavaScript ](#tab-panel-2998)
* [  TypeScript ](#tab-panel-2999)

JavaScript

```

export default {

  async fetch(request, env) {

    const url = new URL(request.url);


    if (request.method === "POST") {

      // GitHub webhooks

      if (url.pathname.startsWith("/webhooks/github/")) {

        const payload = await request.clone().json();

        const repoName = payload.repository?.full_name?.replace("/", "-");

        const agent = await getAgentByName(env.GitHubAgent, repoName);

        return agent.fetch(request);

      }


      // Stripe webhooks

      if (url.pathname.startsWith("/webhooks/stripe/")) {

        const payload = await request.clone().json();

        const customerId = payload.data?.object?.customer;

        const agent = await getAgentByName(env.StripeAgent, customerId);

        return agent.fetch(request);

      }


      // Slack webhooks

      if (url.pathname === "/webhooks/slack") {

        const teamId = request.headers.get("X-Slack-Team-Id");

        const agent = await getAgentByName(env.SlackAgent, teamId);

        return agent.fetch(request);

      }

    }


    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

};


```

TypeScript

```

export default {

  async fetch(request: Request, env: Env): Promise<Response> {

    const url = new URL(request.url);


    if (request.method === "POST") {

      // GitHub webhooks

      if (url.pathname.startsWith("/webhooks/github/")) {

        const payload = await request.clone().json();

        const repoName = payload.repository?.full_name?.replace("/", "-");

        const agent = await getAgentByName(env.GitHubAgent, repoName);

        return agent.fetch(request);

      }


      // Stripe webhooks

      if (url.pathname.startsWith("/webhooks/stripe/")) {

        const payload = await request.clone().json();

        const customerId = payload.data?.object?.customer;

        const agent = await getAgentByName(env.StripeAgent, customerId);

        return agent.fetch(request);

      }


      // Slack webhooks

      if (url.pathname === "/webhooks/slack") {

        const teamId = request.headers.get("X-Slack-Team-Id");

        const agent = await getAgentByName(env.SlackAgent, teamId);

        return agent.fetch(request);

      }

    }


    return (

      (await routeAgentRequest(request, env)) ??

      new Response("Not found", { status: 404 })

    );

  },

} satisfies ExportedHandler<Env>;


```

## Sending outgoing webhooks

Agents can also send webhooks to external services:

* [  JavaScript ](#tab-panel-2996)
* [  TypeScript ](#tab-panel-2997)

JavaScript

```

export class NotificationAgent extends Agent {

  async notifySlack(message) {

    const response = await fetch(this.env.SLACK_WEBHOOK_URL, {

      method: "POST",

      headers: { "Content-Type": "application/json" },

      body: JSON.stringify({ text: message }),

    });


    if (!response.ok) {

      throw new Error(`Slack notification failed: ${response.status}`);

    }

  }


  async sendSignedWebhook(url, payload) {

    const body = JSON.stringify(payload);

    const signature = await this.sign(body, this.env.WEBHOOK_SECRET);


    await fetch(url, {

      method: "POST",

      headers: {

        "Content-Type": "application/json",

        "X-Signature": signature,

      },

      body,

    });

  }

}


```

TypeScript

```

export class NotificationAgent extends Agent {

  async notifySlack(message: string) {

    const response = await fetch(this.env.SLACK_WEBHOOK_URL, {

      method: "POST",

      headers: { "Content-Type": "application/json" },

      body: JSON.stringify({ text: message }),

    });


    if (!response.ok) {

      throw new Error(`Slack notification failed: ${response.status}`);

    }

  }


  async sendSignedWebhook(url: string, payload: unknown) {

    const body = JSON.stringify(payload);

    const signature = await this.sign(body, this.env.WEBHOOK_SECRET);


    await fetch(url, {

      method: "POST",

      headers: {

        "Content-Type": "application/json",

        "X-Signature": signature,

      },

      body,

    });

  }

}


```

## Security best practices

1. **Always verify signatures** \- Never trust unverified webhooks.
2. **Use environment secrets** \- Store secrets with `wrangler secret put`, not in code.
3. **Respond quickly** \- Return 200/202 within seconds to avoid retries.
4. **Validate payloads** \- Check required fields before processing.
5. **Log rejections** \- Track invalid signatures for security monitoring.
6. **Use HTTPS** \- Webhook URLs should always use TLS.

* [  JavaScript ](#tab-panel-2982)
* [  TypeScript ](#tab-panel-2983)

JavaScript

```

// Store secrets securely

// wrangler secret put GITHUB_WEBHOOK_SECRET


// Access in agent

const secret = this.env.GITHUB_WEBHOOK_SECRET;


```

TypeScript

```

// Store secrets securely

// wrangler secret put GITHUB_WEBHOOK_SECRET


// Access in agent

const secret = this.env.GITHUB_WEBHOOK_SECRET;


```

## Common webhook providers

| Provider | Documentation                                                                                                  |
| -------- | -------------------------------------------------------------------------------------------------------------- |
| GitHub   | [Webhook events and payloads ↗](https://docs.github.com/en/webhooks)                                           |
| Stripe   | [Webhook signatures ↗](https://stripe.com/docs/webhooks/signatures)                                            |
| Twilio   | [Validate webhook requests ↗](https://www.twilio.com/docs/usage/webhooks/webhooks-security)                    |
| Slack    | [Verifying requests ↗](https://api.slack.com/authentication/verifying-requests-from-slack)                     |
| Shopify  | [Webhook verification ↗](https://shopify.dev/docs/apps/webhooks/configuration/https#step-5-verify-the-webhook) |
| SendGrid | [Event webhook ↗](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook)      |
| Linear   | [Webhooks ↗](https://developers.linear.app/docs/graphql/webhooks)                                              |

## Next steps

[ Queue tasks ](https://developers.cloudflare.com/agents/api-reference/queue-tasks/) Background task processing. 

[ Email routing ](https://developers.cloudflare.com/agents/api-reference/email/) Handle inbound emails in your agent. 

[ Agents API ](https://developers.cloudflare.com/agents/api-reference/agents-api/) Complete API reference for the Agents SDK. 

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/guides/","name":"Guides"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/guides/webhooks/","name":"Webhooks"}}]}
```

---

---
title: Limits
description: Limits that apply to authoring, deploying, and running Agents are detailed below.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/platform/limits.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Limits

Limits that apply to authoring, deploying, and running Agents are detailed below.

Many limits are inherited from those applied to Workers scripts and/or Durable Objects, and are detailed in the [Workers limits](https://developers.cloudflare.com/workers/platform/limits/) documentation.

| Feature                                                | Limit                                                                                        |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Max concurrent (running) Agents per account            | Tens of millions+ [1](#user-content-fn-1)                                                    |
| Max definitions per account                            | \~250,000+ [2](#user-content-fn-2)                                                           |
| Max state stored per unique Agent                      | 1 GB                                                                                         |
| Max compute time per Agent                             | 30 seconds (refreshed per HTTP request / incoming WebSocket message) [3](#user-content-fn-3) |
| Duration (wall clock) per step [3](#user-content-fn-3) | Unlimited (for example, waiting on a database call or an LLM response)                       |

---

Need a higher limit?

To request an adjustment to a limit, complete the [Limit Increase Request Form ↗](https://forms.gle/ukpeZVLWLnKeixDu7). If the limit can be increased, Cloudflare will contact you with next steps.

## Footnotes

1. Yes, really. You can have tens of millions of Agents running concurrently, as each Agent is mapped to a [unique Durable Object](https://developers.cloudflare.com/durable-objects/concepts/what-are-durable-objects/) (actor). [↩](#user-content-fnref-1)
2. You can deploy up to [500 scripts per account](https://developers.cloudflare.com/workers/platform/limits/), but each script (project) can define multiple Agents. Each deployed script can be up to 10 MB on the [Workers Paid Plan](https://developers.cloudflare.com/workers/platform/pricing/#workers) [↩](#user-content-fnref-2)
3. Compute (CPU) time per Agent is limited to 30 seconds, but this is refreshed when an Agent receives a new HTTP request, runs a [scheduled task](https://developers.cloudflare.com/agents/api-reference/schedule-tasks/), or an incoming WebSocket message. [↩](#user-content-fnref-3) [↩2](#user-content-fnref-3-2)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/platform/","name":"Platform"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/platform/limits/","name":"Limits"}}]}
```

---

---
title: Prompt Engineering
description: Learn how to prompt engineer your AI models &#38; tools when building Agents &#38; Workers on Cloudflare.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/platform/prompting.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# Prompt Engineering

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/platform/","name":"Platform"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/platform/prompting/","name":"Prompt Engineering"}}]}
```

---

---
title: prompt.txt
description: Provide context to your AI models &#38; tools when building on Cloudflare.
image: https://developers.cloudflare.com/dev-products-preview.png
---

[Skip to content](#%5Ftop) 

Was this helpful?

YesNo

[ Edit page ](https://github.com/cloudflare/cloudflare-docs/edit/production/src/content/docs/agents/platform/prompt.txt.mdx) [ Report issue ](https://github.com/cloudflare/cloudflare-docs/issues/new/choose) 

Copy page

# prompt.txt

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/agents/","name":"Agents"}},{"@type":"ListItem","position":3,"item":{"@id":"/agents/platform/","name":"Platform"}},{"@type":"ListItem","position":4,"item":{"@id":"/agents/platform/prompttxt/","name":"prompt.txt"}}]}
```
