Skip to main content

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.