Cloudflare Computer now helps agents recover from interruptions and wait for approval
`@cloudflare/computer` ↗︎ gives your agent a workspace to run code in. From v0.7.0, that code can resume after a deploy or crash, and pause on an external action, for example to approve a tool call.
Set `onInterrupt: "replay"` on `WorkerJavaScriptBackend`, or on a single run.
``` import { DurableObject } from "cloudflare:workers"; import { Workspace } from "@cloudflare/computer"; import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
export class MyAgent extends DurableObject { workspace = new Workspace({ storage: this.ctx.storage, backends: [ new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", }), ], }); } ```
``` import { DurableObject } from "cloudflare:workers"; import { Workspace } from "@cloudflare/computer"; import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
export class MyAgent extends DurableObject<Env> { workspace = new Workspace({ storage: this.ctx.storage, backends: [ new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", }), ], }); } ```
When recovery is triggered, the JavaScript runs again from the beginning, reusing recorded results for completed calls without printing previously recorded output again. If a call's result was not recorded before the interruption, recovery may repeat the call — even if its action already succeeded. For retry handling, including `unsafeToRepeat()` for operations that cannot safely repeat, refer to Calls that must not run twice.
Recovery starts on the next call to `workspace.runtime`. To recover without waiting for traffic, call `workspace.runtime.recover()` from a Durable Object alarm.
!Image 1: A run pauses at an approval gate, then resumes when approved or fails when rejected
Expose your application functions through custom modules. A function can call `ctx.interrupt()` to pause the run until your application supplies an approval decision.
For example, to provide a GitHub module that allows an agent to post a comment on an issue with approval, an implementation of `postComment` could look like this:
``` new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", modules: { // The agent's code imports this with `import { postComment } from "ws:github"`. "ws:github": { postComment: async ([issue, body], ctx) => { const { approved } = await ctx.interrupt({ issue, body, }); if (!approved) throw new Error("The comment was rejected."); return github.comment(issue, body, { idempotencyKey: ctx.callId }); }, }, }, }); ```
``` new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", modules: { // The agent's code imports this with `import { postComment } from "ws:github"`. "ws:github": { postComment: async ([issue, body], ctx) => { const { approved } = await ctx.interrupt<{ approved: boolean }>({ issue, body, }); if (!approved) throw new Error("The comment was rejected."); return github.comment(issue, body, { idempotencyKey: ctx.callId }); }, }, }, }); ```
When the agent's code calls `postComment()`, the run pauses at `ctx.interrupt()` until your app responds. The agent's code calls it like any other function:
``` import { postComment } from "ws:github";
export default async function ({ issue }) { return postComment(issue, "Hello world"); } ```
The time spent waiting does not count toward its timeout. The wait also survives deploys. Fetch the pending calls with `workspace.runtime.pending()` and resolve them with `respond()`:
``` for (const call of await workspace.runtime.pending()) { await workspace.runtime.respond(call.id, call.callId, { approved: true }); } ```
``` for (const call of await workspace.runtime.pending()) { await workspace.runtime.respond(call.id, call.callId, { approved: true }); } ```
The run continues once all pending calls are resolved.
For setup, current limitations, and how replay works, refer to Resume runs after a restart.