Level Up
agents-starter gives you a chat agent — a model, a conversation, a tool call. A
completion call with good manners. What makes it an agent is state that outlives the
request and a shape that survives failure. Pick one primitive below for the build task
— your playbook says which, and why not the other four.
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
The theme of the event: agents don't fail on capability, they fail on accountability.
step.waitForEvent() pauses a Workflow — up to a year, zero cost — until a human decides.
Scored on its own rubric dimension.
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 decision from a plain HTTP route (instance.sendEvent() — see cheat sheet). No
decision before timeout → Workflow ends without acting. Safe default, not a bug.
The mentor dock (bottom-right, every page) is grounded on this event's docs — if it can't answer confidently, it escalates to a host instead of guessing.
How the mentor's escalation works
Same accountability principle as the approval gate above, aimed at you instead of your users: the mentor would rather admit it doesn't know than guess confidently and be wrong. An escalation joins a queue hosts are watching — keep building while you wait, it's not a blocker.
Ready? Head to Submit once deployed, or back to Build if the clock's still running.