shiichan

Surprise! Agents SDK v0.8.0 makes state readable, schedules idempotent, and AgentClient fully typed

Hi, it's me! Cloudflare just shipped an update to the Agents SDK, and it's the kind of release that quietly fixes a bunch of "yeah, that's annoying" problems. Let's dig in.

Cloudflare Changelog developers.cloudflare.com

What was announced?

Cloudflare's Changelog posted the Agents SDK v0.8.0 release, dated March 23, 2026. There are four main pillars.

  • state is now readable directly as a property (on both React's useAgent and vanilla JS's AgentClient)
  • schedule() is now idempotent, so duplicate rows no longer pile up across Durable Object restarts
  • AgentClient gained a type parameter, bringing full TypeScript inference
  • The dependencies moved to Zod 4 (Zod 3 is no longer supported)

On top of that, there are bug fixes in @cloudflare/ai-chat, keepAlive() losing its experimental tag, and TanStack AI support in @cloudflare/codemode.

The story so far

Before this release, both useAgent and AgentClient required you to manually track state through the onStateUpdate callback and re-store it in your own state variable. You just wanted to read the current state, but you had to write your own sync logic every time.

schedule() had a trickier problem: calling it from a place that runs on every Durable Object restart, like onStart(), created a brand-new row each time. Leave that unchecked and your schedule rows just keep piling up.

AgentClient also didn't get the type inference that useAgent already had, so method name autocomplete and argument/return type checking were weak. And the dependency was still on Zod v3.

What changes

First, state. You can now read the value directly from agent.state (React) or client.state (vanilla JS), so you no longer need to duplicate it with onStateUpdate. state is reactive — it triggers a re-render whether the change comes from the server or from a client-side setState() call.

schedule() now deduplicates based on the combination of type, callback, and payload. Cron schedules are idempotent by default with no extra work. Delayed and date-based schedules just need one option flipped on to avoid accumulating extra rows across restarts.

AgentClient also accepts a type parameter now, so RPC method names autocomplete and arguments/return values are properly inferred. There's also a new stub proxy for calling methods even more directly.

Dive Deep

Here's what reading state looks like.

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 }

state starts as undefined right after connecting, and gets populated once the server sends the initial state or setState() is called. That's why it's safest to read it with optional chaining. The onStateUpdate callback keeps working exactly as before, so nothing in your existing code needs to change.

The idempotent option for schedule() is typically used like this inside onStart().

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 safety nets were added as well.

  • Calling schedule() inside onStart() without the idempotent option logs a console.warn (once per callback; skipped for cron or when the option is set explicitly)
  • If an alarm cycle finds 10 or more stale one-shot rows for the same callback, it logs a console.warn and also fires a diagnostics channel event

Here's what the typed AgentClient looks like.

const client = new AgentClient({
	agent: "my-agent",
	host: window.location.host,
});

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

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

When you pass the agent's type as a parameter, the state type is inferred automatically too, so the argument passed to onStateUpdate is properly typed. Existing untyped code keeps working, so migrating isn't required. For advanced use cases, a full set of type utilities is now exported from agents/client as well.

One change that's easy to overlook: agents, @cloudflare/ai-chat, and @cloudflare/codemode now all require Zod 4, and Zod 3 is no longer supported. This is the one breaking change in this release, so it's worth checking your Zod version before upgrading.

A few smaller improvements are worth noting too.

  • @cloudflare/ai-chat: message handling is now queued, so a user's request, tool continuations, and message saving never stream concurrently. It also fixes a bug where clicking stop split the assistant's message in two, and a bug where messages could duplicate after tool calls
  • keepAlive() and keepAliveWhile(): these switched from using schedule rows to an in-memory reference count and officially lost their experimental tag. Multiple concurrent callers now share a single alarm cycle
  • @cloudflare/codemode: a new entry point lets you use TanStack AI's chat() as an alternative to the Vercel AI SDK's streamText()

Upgrading is just one command.

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

Wrap-up

  • state can now be read directly as a property on useAgent / AgentClient, removing the need to duplicate it with onStateUpdate
  • schedule() deduplicates by type, callback, and payload — cron is idempotent by default, and delayed/date schedules just need one option flipped on
  • AgentClient now accepts a type parameter for full RPC type inference and autocomplete, plus a new stub proxy
  • Dependencies now standardize on Zod 4 (Zod 3 is unsupported — check this before upgrading)
  • @cloudflare/ai-chat streaming fixes, keepAlive() graduating from experimental, and TanStack AI support in @cloudflare/codemode all arrived together

If you're running long-lived agents on Durable Objects with Cloudflare Workers, this is a practical update you can put to use today.