What is the Agents SDK?
Agents SDK is Cloudflare’s framework for building stateful, durable AI agents on Workers. The core primitive is the Agent class — a Durable Object subclass with built-in support for:
- Persistent state —
this.setState()andthis.statebacked by the DO’s SQLite storage. State survives between requests automatically. - RPC methods — Public methods on your Agent class are callable from your Worker via the DO stub. No REST API boilerplate needed.
- Lifecycle hooks —
onStart(),onConnect(),onClose()for managing the agent’s lifecycle. - Scoped tools — Define what tools and capabilities each agent has access to. Different agents can have different tool sets.
The key insight: an Agent is a Durable Object with opinions. Instead of writing raw DO code with storage APIs and WebSocket handlers, the Agent class gives you a structured pattern for state, communication, and orchestration.
Why agents instead of plain Workers AI calls?
In lesson 09, you called env.AI.run() directly from the Worker. That works, but it’s stateless — every investigate call starts from scratch. The Worker doesn’t remember what it analyzed before, can’t hold intermediate results, and can’t run multi-step workflows.
An Agent-based approach gives you:
- Durable state — The agent remembers what it has investigated. If you investigate an incident, leave, and come back, the results are still there.
- Composability — You can add sub-agents, tools, and multi-step flows. The Coordinator pattern in lesson 11 becomes natural.
- Lifecycle — The agent can run workflows that span multiple requests. Start an investigation now, receive results later.
- Scoped tools — Each agent only has access to what it needs. The triage agent can call Workers AI; the response agent can read playbooks. Separation of concerns.
The Agent class pattern
Here’s the simplified structure of the TriageAgent class:
import { Agent } from "agents-sdk";
interface State {
status: string;
triageCard: object | null;
}
export class TriageAgent extends Agent<Env, State> {
initialState: State = { status: "idle", triageCard: null };
async investigate(incidentId: string) {
this.setState({ ...this.state, status: "investigating" });
// Load incident data from D1
const incident = await this.env.INCIDENTS_DB
.prepare("SELECT * FROM incidents WHERE id = ?")
.bind(incidentId)
.first();
// Call Workers AI with the incident context
const result = await this.env.AI.run(
"@cf/meta/llama-3.3-70b-instruct-fp8-fast",
{
messages: [
{ role: "system", content: "You are a security analyst..." },
{ role: "user", content: JSON.stringify(incident) },
],
}
);
// Persist the result in agent state
this.setState({
...this.state,
status: "triaged",
triageCard: result,
});
return result;
}
}
Key things to notice:
extends Agent<Env, State>— The generic parameters type the environment bindings and the agent’s state shape.initialState— Defines the default state for a new agent instance. When the agent is first created,this.statereturns this value.this.setState()— Persists state to the DO’s SQLite storage. The state survives between requests and even between Worker restarts.this.env— The agent has access to the same environment bindings as the Worker. It can call D1, Workers AI, or any other binding.investigate()— This is an RPC method. The Worker calls it via the DO stub.
How the Worker routes to the agent
In src/server.ts, the investigate handler now routes through the agent instead of calling Workers AI directly:
// Get a stub for this incident's TriageAgent
const agentId = env.TRIAGE_AGENT.idFromName(incidentId);
const stub = env.TRIAGE_AGENT.get(agentId);
// Call the agent's investigate method via RPC
const result = await stub.investigate(incidentId);
The idFromName(incidentId) call is critical — it creates a deterministic ID from the incident ID. This means every request for the same incident routes to the same agent instance. Call it once, call it a hundred times — the same agent handles it, with the same persisted state.
Step 1: Review the TriageAgent class
Open sirt-workshop-app/src/agents/triage-agent.ts and read through the code. Identify:
- The
initialStatedefinition - The
investigate()method - Where
this.setState()is called and what state transitions happen - How
this.env.AIis used inside the agent
The agent wraps the same Workers AI call from lesson 09, but now it’s inside a durable context with persistent state.
Step 2: Change STATE to state-5-agent
Open sirt-workshop-app/wrangler.jsonc and update the STATE variable:
"vars": {
"STATE": "state-5-agent"
}
This tells the Worker to route investigate requests through the TriageAgent instead of making direct Workers AI calls. The state-5-agent code path creates an agent stub and calls its investigate() RPC method.
Step 3: Redeploy
Deploy the updated configuration:
npx wrangler deploy
Wrangler will create the TriageAgent’s Durable Object class during deployment. You should see the deployment succeed with the new class registered.
Step 4: Verify the agent-driven flow
- Open your app and navigate to the incident queue.
- Click on any incident and click “Investigate.”
- The analysis should appear as before — same quality, same structure.
The difference is under the hood: the analysis now runs through a durable agent lifecycle. The TriageAgent persists its state, so if you reload the page after investigating, the results are still there. In lesson 09, that state would have been lost.
Try this: investigate an incident, go back to the queue, then re-open the same incident. The triage results should still be visible without making another AI call. That’s the agent’s durable state at work.
What comes next
Right now, the TriageAgent makes a single Workers AI call — the same pattern as lesson 09, just wrapped in an agent. In lesson 11, you’ll replace that single call with a Coordinator + 4 parallel sub-agents pattern. The agent’s investigate() method will fan out to four specialized analyzers (Command-Line, Identity, Network, Activity), collect their results in parallel, and synthesize them into a unified triage card.
The Agents SDK makes this decomposition natural — each sub-agent analysis can be a separate function with its own system prompt, and the Coordinator orchestrates them with Promise.all.