Level Up
agents-starter gives you a chat agent: a model, a conversation, a tool call. That's a
completion call with good manners. What turns it into an agent is state that outlives
the request, and a shape that survives failure. These five primitives are how. Pick
one for the build task — the playbooks show which one fits which use case, and why the
other four don't.
1. A Workflow
Use it when a step must survive a crash, a retry, or a human going to lunch. Every
step.do() checkpoints its result — if the Workflow restarts, completed steps never
re-run.
import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from 'cloudflare:workers';
export class MyWorkflow extends WorkflowEntrypoint<Env> {
async run(event: WorkflowEvent<{ id: string }>, step: WorkflowStep) {
const data = await step.do('fetch the record', async () => {
return { id: event.payload.id, status: 'pending' };
});
await step.do(
'call an unreliable API',
{ retries: { limit: 3, delay: '5 seconds', backoff: 'exponential' } },
async () => {
// any 5xx here retries automatically, without re-doing step 1
},
);
return data;
}
}
2. A schedule
Use it when the trigger is a clock, not an event — a nightly sweep, a delayed follow-up. The Agents SDK wraps a Durable Object alarm so you never manage timers by hand.
import { Agent } from 'agents';
export class MyAgent extends Agent<Env> {
async onStart() {
// runs once a day, survives restarts (idempotent by default for cron)
await this.schedule('0 6 * * *', 'dailySweep', { reason: 'fleet-check' });
}
async dailySweep(payload: { reason: string }) {
// do the scheduled work — this.sql and this.env are both available here
}
}
3. Browser Rendering
Use it when the source of truth lives on a page you don't control — a competitor's price, a partner's status page. No local browser to manage; Cloudflare runs headless Chrome for you.
// wrangler.jsonc — needs compatibility_date >= 2026-03-24
{ "browser": { "binding": "BROWSER" } }
const screenshot = await env.BROWSER.quickAction('screenshot', {
url: 'https://example.com/status',
});
// also available: "markdown", "content", "json", "scrape", "links", "snapshot"
4. AI Search over your own docs
Use it when an answer must be grounded and citable, not just plausible. Point it at your own (synthetic) policy docs or runbooks and the model retrieves before it answers, instead of guessing from training data.
// wrangler.jsonc — needs compatibility_date >= 2026-03-27
{ "ai_search": [{ "binding": "AI_SEARCH", "instance_name": "my-instance" }] }
const instance = env.AI_SEARCH.get('my-instance');
const results = await instance.search({
messages: [{ role: 'user', content: 'what is the chargeback window?' }],
});
// results include the source chunk, so your answer can cite it
5. Persistent Durable Object state
Use it when two requests must never see two different versions of the same fact — a case file, a running total, anything where "eventually consistent" is the wrong trade. Every Agent instance is single-threaded per id, with embedded SQLite built in.
import { Agent } from 'agents';
export class MyAgent extends Agent<Env, { openCases: number }> {
initialState = { openCases: 0 };
async fileCase(id: string) {
this.sql`INSERT INTO cases (id, status) VALUES (${id}, 'open')`;
this.setState({ openCases: this.state.openCases + 1 });
}
}
Bonus: the human approval gate
This is the theme of the whole event: agents don't fail on capability, they fail on
accountability. step.waitForEvent() pauses a Workflow — for up to a year, at zero
cost while waiting — until a named human sends a decision. It's a scored rubric dimension
on its own, and it's the entire point of the Banking playbook.
async run(event: WorkflowEvent<{ id: string }>, step: WorkflowStep) {
const caseFile = await step.do('prepare the case', async () => {
// build the thing a human needs to see, not the thing you'd auto-execute
return { id: event.payload.id, recommendation: 'refund' };
});
const decision = await step.waitForEvent<{ approved: boolean; by: string }>(
'await human decision',
{ type: 'case-decision', timeout: '2 hours' },
);
await step.do('record the outcome', async () => {
// store BOTH caseFile.recommendation and decision — the contrast is the audit trail
});
}
Send the event from a plain HTTP route once a human clicks approve or decline — see the
cheat sheet (in the sidebar) for the instance.sendEvent() call. If nobody decides before
the timeout, the Workflow times out without acting — that's the safe default, not a
bug to route around.
If your agent gets stuck
The mentor dock (bottom-right, once it's live) is grounded on this event's own docs. If it can't answer confidently, it escalates to a host instead of guessing — the same accountability principle, aimed at you instead of your users.
Ready? Head to Submit once you've deployed, or back to Build if the clock's still running.