---
title: Agents SDK v0.8.0: readable state, idempotent schedules, typed AgentClient, and Zod 4
description: Agents SDK v0.8.0 exposes a readable state property on useAgent and AgentClient, makes schedule() idempotent for cron and opt-in for other types, adds full TypeScript inference to AgentClient, and drops Zod v3 in favor of Zod v4.
image: https://developers.cloudflare.com/changelog-preview.png
---

[Skip to content](#%5Ftop) 

# Changelog

New updates and improvements at Cloudflare.

[ Subscribe to RSS ](https://developers.cloudflare.com/changelog/rss/index.xml) [ View RSS feeds ](https://developers.cloudflare.com/fundamentals/new-features/available-rss-feeds/) 

![hero image](https://developers.cloudflare.com/_astro/hero.CVYJHPAd_26AMqX.svg) 

[ ← Back to all posts ](https://developers.cloudflare.com/changelog/) 

## Agents SDK v0.8.0: readable state, idempotent schedules, typed AgentClient, and Zod 4

Mar 23, 2026 

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

The latest release of the [Agents SDK ↗](https://github.com/cloudflare/agents) exposes agent state as a readable property, prevents duplicate schedule rows across Durable Object restarts, brings full TypeScript inference to `AgentClient`, and migrates to Zod 4.

#### Readable `state` on `useAgent` and `AgentClient`

Both `useAgent` (React) and `AgentClient` (vanilla JS) now expose a `state` property that reflects the current agent state. Previously, reading state required manually tracking it through the `onStateUpdate` callback.

**React (`useAgent`)**

* [  JavaScript ](#tab-panel-1236)
* [  TypeScript ](#tab-panel-1237)

JavaScript

```

const agent = useAgent({

  agent: "game-agent",

  name: "room-123",

});


// Read state directly — no separate useState + onStateUpdate needed

return <div>Score: {agent.state?.score}</div>;


// Spread for partial updates

agent.setState({ ...agent.state, score: (agent.state?.score ?? 0) + 10 });


```

Explain Code

TypeScript

```

const agent = useAgent<GameAgent, GameState>({

  agent: "game-agent",

  name: "room-123",

});


// Read state directly — no separate useState + onStateUpdate needed

return <div>Score: {agent.state?.score}</div>;


// Spread for partial updates

agent.setState({ ...agent.state, score: (agent.state?.score ?? 0) + 10 });


```

Explain Code

`agent.state` is reactive — the component re-renders when state changes from either the server or a client-side `setState()` call.

**Vanilla JS (`AgentClient`)**

* [  JavaScript ](#tab-panel-1238)
* [  TypeScript ](#tab-panel-1239)

JavaScript

```

const client = new AgentClient({

  agent: "game-agent",

  name: "room-123",

  host: "your-worker.workers.dev",

});


client.setState({ score: 100 });

console.log(client.state); // { score: 100 }


```

TypeScript

```

const client = new AgentClient<GameAgent>({

  agent: "game-agent",

  name: "room-123",

  host: "your-worker.workers.dev",

});


client.setState({ score: 100 });

console.log(client.state); // { score: 100 }


```

State starts as `undefined` and is populated when the server sends the initial state on connect (from `initialState`) or when `setState()` is called. Use optional chaining (`agent.state?.field`) for safe access. The `onStateUpdate` callback continues to work as before — the new `state` property is additive.

#### Idempotent `schedule()`

`schedule()` now supports an `idempotent` option that deduplicates by `(type, callback, payload)`, preventing duplicate rows from accumulating when called in places that run on every Durable Object restart such as `onStart()`.

**Cron schedules are idempotent by default.** Calling `schedule("0 * * * *", "tick")` multiple times with the same callback, expression, and payload returns the existing schedule row instead of creating a new one. Pass `{ idempotent: false }` to override.

Delayed and date-scheduled types support opt-in idempotency:

* [  JavaScript ](#tab-panel-1240)
* [  TypeScript ](#tab-panel-1241)

JavaScript

```

import { Agent } from "agents";


class MyAgent extends Agent {

  async onStart() {

    // Safe across restarts — only one row is created

    await this.schedule(60, "maintenance", undefined, { idempotent: true });

  }

}


```

TypeScript

```

import { Agent } from "agents";


class MyAgent extends Agent {

  async onStart() {

    // Safe across restarts — only one row is created

    await this.schedule(60, "maintenance", undefined, { idempotent: true });

  }

}


```

Two new warnings help catch common foot-guns:

* Calling `schedule()` inside `onStart()` without `{ idempotent: true }` emits a `console.warn` with actionable guidance (once per callback; skipped for cron and when `idempotent` is set explicitly).
* If an alarm cycle processes 10 or more stale one-shot rows for the same callback, the SDK emits a `console.warn` and a `schedule:duplicate_warning` diagnostics channel event.

#### Typed `AgentClient` with `call` inference and `stub` proxy

`AgentClient` now accepts an optional agent type parameter for full type inference on RPC calls, matching the typed experience already available with `useAgent`.

* [  JavaScript ](#tab-panel-1244)
* [  TypeScript ](#tab-panel-1245)

JavaScript

```

const client = new AgentClient({

  agent: "my-agent",

  host: window.location.host,

});


// Typed call — method name autocompletes, args and return type inferred

const value = await client.call("getValue");


// Typed stub — direct RPC-style proxy

await client.stub.getValue();

await client.stub.add(1, 2);


```

Explain Code

TypeScript

```

const client = new AgentClient<MyAgent>({

  agent: "my-agent",

  host: window.location.host,

});


// Typed call — method name autocompletes, args and return type inferred

const value = await client.call("getValue");


// Typed stub — direct RPC-style proxy

await client.stub.getValue();

await client.stub.add(1, 2);


```

Explain Code

State is automatically inferred from the agent type, so `onStateUpdate` is also typed:

* [  JavaScript ](#tab-panel-1242)
* [  TypeScript ](#tab-panel-1243)

JavaScript

```

const client = new AgentClient({

  agent: "my-agent",

  host: window.location.host,

  onStateUpdate: (state) => {

    // state is typed as MyAgent's state type

  },

});


```

TypeScript

```

const client = new AgentClient<MyAgent>({

  agent: "my-agent",

  host: window.location.host,

  onStateUpdate: (state) => {

    // state is typed as MyAgent's state type

  },

});


```

Existing untyped usage continues to work without changes. The RPC type utilities (`AgentMethods`, `AgentStub`, `RPCMethods`) are now exported from `agents/client` for advanced typing scenarios.`agents`, `@cloudflare/ai-chat`, and `@cloudflare/codemode` now require `zod ^4.0.0`. Zod v3 is no longer supported.

#### `@cloudflare/ai-chat` fixes

* **Turn serialization** — `onChatMessage()` and `_reply()` work is now queued so user requests, tool continuations, and `saveMessages()` never stream concurrently.
* **Duplicate messages on stop** — Clicking stop during an active stream no longer splits the assistant message into two entries.
* **Duplicate messages after tool calls** — Orphaned client IDs no longer leak into persistent storage.

#### `keepAlive()` and `keepAliveWhile()` are no longer experimental

`keepAlive()` now uses a lightweight in-memory ref count instead of schedule rows. Multiple concurrent callers share a single alarm cycle. The `@experimental` tag has been removed from both `keepAlive()` and `keepAliveWhile()`.

#### `@cloudflare/codemode`: TanStack AI integration

A new entry point `@cloudflare/codemode/tanstack-ai` adds support for [TanStack AI's ↗](https://tanstack.com/ai) `chat()` as an alternative to the Vercel AI SDK's `streamText()`:

* [  JavaScript ](#tab-panel-1246)
* [  TypeScript ](#tab-panel-1247)

JavaScript

```

import {

  createCodeTool,

  tanstackTools,

} from "@cloudflare/codemode/tanstack-ai";

import { chat } from "@tanstack/ai";


const codeTool = createCodeTool({

  tools: [tanstackTools(myServerTools)],

  executor,

});


const stream = chat({ adapter, tools: [codeTool], messages });


```

Explain Code

TypeScript

```

import { createCodeTool, tanstackTools } from "@cloudflare/codemode/tanstack-ai";

import { chat } from "@tanstack/ai";


const codeTool = createCodeTool({

  tools: [tanstackTools(myServerTools)],

  executor,

});


const stream = chat({ adapter, tools: [codeTool], messages });


```

#### Upgrade

To update to the latest version:

Terminal window

```

npm i agents@latest @cloudflare/ai-chat@latest


```