Built on Cloudflare
coil docs
Agent threads with their own computer, running on your own Cloudflare account.
What coil is
coil gives every agent thread its own Linux computer in the cloud. The thread has an agent, terminals, a real Chrome window, and a disk. You and the agent use the same ones, from any device.
coil is one Cloudflare Worker. It is a working showcase of the Developer Platform primitives, each doing the job it is built for:
| Workers | The front door: UI, API, MCP, and sign-in. |
|---|---|
| Durable Objects | One per thread. Runs the Pi agent, keeps the transcript, schedules loops, and survives restarts mid-turn. |
| Containers | One Linux computer per thread, with a disk that is snapshotted on pause. |
| Workers AI | Open models, on the account that signs in. |
| AI Gateway | Frontier models such as Claude, with the account’s logs, limits, and billing. |
| Sign in with Cloudflare | OAuth, so each person brings their own account. |
Bring your own Cloudflare
On an instance in cloudflare mode, people sign in with their Cloudflare account. coil then runs their model calls on their Workers AI and AI Gateway. The instance owner never pays for anyone else’s inference, and never needs their keys.
This is the part that makes coil a template, not only an app. Anyone can ship a product on Cloudflare that runs on its users’ inference. Every new user is a Cloudflare account doing real work on Workers AI and AI Gateway.
What happens when you sign in
- coil sends you to Cloudflare’s consent screen. You pick one account.
- coil receives a token limited to these scopes:
user-details.read,account-settings.read,ai.read,ai.write,aig.read,aig.run, andoffline_accessto stay signed in. - coil lists the account’s AI Gateways and uses one with authentication turned on. It prefers the instance’s
COIL_USER_GATEWAY, thendefault, then the first one. - Each turn, coil refreshes the token if needed and calls the gateway with it. If the grant is gone, the turn fails. It never falls back to the instance’s own AI.
Errors you might see
no AI Gateway with authentication turned on | Turn on authentication for a gateway in that account, then sign in again. |
|---|---|
402 Insufficient unified billing credits | The gateway works, but the account has no credits for provider models. Add credits, or pick a Workers AI model. |
x-api-key header is required | The gateway has authentication turned off, so it ignored the Cloudflare token. Turn it on. |
Sign in with Cloudflare again | The grant was revoked or expired. Sign in again. |
Sign out and revoke
Sign out at the bottom of the sidebar ends the session, revokes coil’s token with Cloudflare, and deletes the stored grant. You can also revoke coil at any time from your Cloudflare profile.
Check which account a session uses at /oauth/me.
Quickstart
- Sign in. Choose the Cloudflare account coil should use.
- Start a thread. Optionally give it a Git repository; coil clones it into
/workspace. - Pick a model from the chip under the message box. Models that start with
gateway/go through your AI Gateway. - Ask for something real. For example: “Build a counter page, serve it on port 3000, open it in the browser and click plus three times.”
- Watch the computer on the right: terminals, files, and the live Chrome window the agent drives.
- Pause when you are done. The disk is saved. Come back later and the thread resumes.
Threads and computers
| A computer per thread | /workspace is a git repository. bash, git, node, python3, curl, ripgrep, and jq are installed. The image’s git refuses force-push. |
|---|---|
| Terminals | As many as you want. Each is a tmux session the agent can use too. Reload the page and the shell is still there. |
| A real browser | Headed Chrome in the same computer, shown over VNC. It reaches the agent’s localhost. The agent drives the same window you see. |
| Pause | Saves the disk and stops the computer. Idle threads pause on their own after 9 minutes. |
| Fork | Starts a new thread from this disk plus a summary of the conversation. |
| Compact | Records a summary and a disk snapshot together, so they always match. |
| Child threads | The agent can hand a side task to a child thread with its own computer, and wait for its answer. |
| Loops | Run a prompt on a schedule until a gate command exits 0, a run count is reached, or you stop it. |
| Pi extensions | Load vanilla Pi extensions into a thread from a Git URL or a path. They run inside the computer, unchanged. |
Models
Pick a model per thread from the chip under the message box, or with the thread_model capability.
@cf/... | Workers AI models, run on the signed-in account. |
|---|---|
gateway/claude-opus-5-5 and other gateway/* | Provider models through AI Gateway on the signed-in account. |
access instance, models run on the instance’s own account instead, through its AI binding or a configured gateway.Drive coil from an agent
Everything in the UI is also a capability. The same list drives MCP, the coil_run code tool, REST, and the reference below.
MCP
Point an MCP client at /mcp. Call coil_describe for a TypeScript declaration of the coil object, then coil_run with the body of an async function:
const t = await coil.thread_create({ title: "demo" });
await coil.thread_exec({ name: t.name, command: "echo '<button>+</button>' > index.html && (busybox httpd -p 3000 &)" });
await coil.browser_open({ name: t.name, url: "localhost:3000" });
const page = await coil.browser_snapshot({ name: t.name });
await coil.thread_pause({ name: t.name });
return { thread: t.name, page };
REST
curl -X POST https://<host>/api/capabilities/thread_send \
-H 'content-type: application/json' \
-d '{"name":"thread-abc123","text":"run the tests"}'
Signing in
Every call must be signed in. Behind Cloudflare Access, send an Access token in the cf-access-token header. With Sign in with Cloudflare, send the session cookie from /oauth/login. Without it, the API and /mcp answer 401.
Plain text for agents is at /llms.txt.
Deploy your own
One command per instance. Each instance has one config file that never leaves your machine.
You need
- Bun and Docker (builds
linux/amd64). - A Cloudflare account on Workers Paid with Containers and Workers AI, and
npx wrangler login. - A zone in that account for the hostname.
- The nix-cf-proof base image in the account’s registry.
Ship
git clone https://github.com/acoyfellow/coil && cd coil && bun install
cp deploy/example.conf deploy/mine.conf
$EDITOR deploy/mine.conf
bun run ship -- deploy/mine.conf
ship runs the type check, lint, and tests, builds the computer image, deploys, and fails unless the URL answers with the sign-in mode you chose.
Choose how people sign in
COIL_AUTH=access | A Zero Trust Access application in front. Everyone admitted shares the instance’s AI. |
|---|---|
COIL_AUTH=cloudflare | Sign in with Cloudflare. Each person’s inference runs on their own account. No Access application on the host. |
Sign in with Cloudflare setup
- Create an OAuth client under Manage Account → OAuth clients: response type
code, grantsauthorization_codeandrefresh_token, auth methodclient_secret_basic, redirect URL<your host>/oauth/callbackon https. - Add the scopes Workers AI (Edit, Read), AI Gateway (Run, Read), User Details (Read), and Account Settings (Read).
- Set
COIL_AUTH=cloudflareandCOIL_OAUTH_CLIENT_IDin your config. - Store the secrets on the instance account:
CLOUDFLARE_ACCOUNT_ID=<id> npx wrangler secret put OAUTH_CLIENT_SECRET -c wrangler.mine.local.jsonc
openssl rand -hex 32 | CLOUDFLARE_ACCOUNT_ID=<id> npx wrangler secret put SESSION_SECRET -c wrangler.mine.local.jsonc
Limits
- Passkeys and security keys do not work in the cloud browser. Password and TOTP logins work and stay logged in.
- A resumed computer gets its disk back, not its processes. Terminals reopen in the same folders. Servers the agent started are gone until something starts them again.
- The browser comes back on its last page, not its state. Chrome reopens the last web page it had open, with its cookies. The page reloads, so anything held only in the page starts over.
- Agent commands stop after 120 seconds. Start servers in the background. API and MCP commands stop after 10 minutes.
- A Pi extension tool call stops after about 280 seconds. Only tools load; extension commands and shortcuts are reported as unsupported.
- Computers have internet access. coil is not a sandbox for hostile code.
- The browser view is 1280×800, scaled to fit.
API reference
Every capability is an MCP tool, a coil_run call, and POST https://coil.coey.dev/api/capabilities/<name>.
Threads
thread_list
List your threads, newest activity first.
- No input.
- Returns: Array of { name, title, phase, busy, repo, updatedAt }
thread_create
Create a thread: its own agent and its own Linux computer. repo is an optional git URL.
name: Optional thread name; one is made up if emptyrepo: Optional git URL to clone into /workspacetitle: Optional title shown in the sidebarsize: Container size One of lite, standard-1, standard-2, standard-3, standard-4.- Returns: The thread view: { name, title, phase, messages, surfaces, ... }
thread_send
Send a message to the thread agent and wait for the turn to finish. Reuse requestId to retry safely.
name(required): Thread name, for example "fix-login-3a9c2e"text(required): The messagerequestId: Optional idempotency key- Returns: { status, answer }
thread_status
Phase, transcript, compaction pairs, turns, and recent commands of a thread.
name(required): Thread name, for example "fix-login-3a9c2e"- Returns: The thread view
thread_fork
Start a new thread from this thread's disk and a summary of its conversation.
name(required): Thread name, for example "fix-login-3a9c2e"forkName: Optional name for the fork- Returns: The new thread view
thread_message
Send a message from one thread to another. The receiver starts a turn now, or queues it until its current turn ends. Returns at once.
from(required): The sending threadto(required): The receiving threadtext(required): The message- Returns: { queued }
thread_wait
Wait until a thread finishes its turn, up to seconds, and return its last answer.
name(required): Thread name, for example "fix-login-3a9c2e"seconds: At most 600; default 120- Returns: { done, answer }
thread_context_budget
Set a smaller context window for a thread, in tokens, or 0 for the model limit. The thread compacts at 75% of it and continues from its HANDOFF: note.
name(required): Thread name, for example "fix-login-3a9c2e"tokens(required): Context size in tokens; 0 clears it- Returns: The usage, with contextWindow
model_list
List the models you can pick: the gateway models plus every Workers AI text model with tool use on your account, newest first. Refreshed from Cloudflare every hour.
- No input.
- Returns: Model ids with label, context window, price per million tokens, and vision support
thread_model
Choose the model for a thread, for example gateway/claude-opus-5-5 or a Workers AI model id. With Sign in with Cloudflare it runs on your own account.
name(required): Thread name, for example "fix-login-3a9c2e"model(required): A model id from the model list- Returns: The usage, with the context window of the new model
thread_stop
Stop the agent turn now. A running tool call ends at once and the thread stays awake.
name(required): Thread name, for example "fix-login-3a9c2e"- Returns: null
Shell
thread_exec
Run a shell command in the thread computer, in /workspace.
name(required): Thread name, for example "fix-login-3a9c2e"command(required): Run with bash -lc- Returns: { exitCode, stdout, stderr, seq }
Files
thread_read_file
Read a file from the thread computer.
name(required): Thread name, for example "fix-login-3a9c2e"path(required): Relative to /workspace- Returns: The file contents as text
thread_write_file
Write a text file into /workspace on the thread computer, for example to upload a local pi extension folder file by file.
name(required): Thread name, for example "fix-login-3a9c2e"path(required): Relative to /workspace; must stay inside itcontent(required): The full text of the file- Returns: { path, bytes }
Browser
browser_open
Load a URL in the thread's browser. localhost is the thread computer.
name(required): Thread name, for example "fix-login-3a9c2e"url(required): For example localhost:3000- Returns: Page title and URL
browser_snapshot
List clickable elements as refs (e1, e2, ...) plus the page text.
name(required): Thread name, for example "fix-login-3a9c2e"- Returns: Text with one line per element: ref, role, label
browser_click
Click an element by its ref from browser_snapshot.
name(required): Thread name, for example "fix-login-3a9c2e"ref(required): For example e3- Returns: What was clicked and the page now open
browser_type
Type text, optionally into an element ref first.
name(required): Thread name, for example "fix-login-3a9c2e"text(required): Text to typeref: Optional element to click first- Returns: How many characters were typed
browser_eval
Run a JavaScript expression in the page and return the result.
name(required): Thread name, for example "fix-login-3a9c2e"expression(required): For example document.title- Returns: The result as text
Computer
thread_compact
Summarise the conversation and save the disk, as one recorded pair.
name(required): Thread name, for example "fix-login-3a9c2e"- Returns: The saved pair: { at, reason, snapshot, summaryEntry }
thread_pause
Save the disk and turn the computer off. The next call resumes it.
name(required): Thread name, for example "fix-login-3a9c2e"- Returns: The saved pair, or null if it was already off
thread_resize
Change the container size. A running computer is paused and restarted at the new size.
name(required): Thread name, for example "fix-login-3a9c2e"size(required): Container size One of lite, standard-1, standard-2, standard-3, standard-4.- Returns: { size, restarted }
Pi extensions
thread_extensions
Load stock pi extensions into the thread. Each source is an https git URL (optionally #ref) or a ./path inside /workspace, for example an uploaded folder. Their tools run inside the thread computer and the agent can call them from its next turn. An empty list removes them.
name(required): Thread name, for example "fix-login-3a9c2e"sources(required): For example ["https://github.com/owner/repo", "./uploads/my-extension"]- Returns: { tools: [{ name, description, extension }], errors, unsupported }
Warm bases
base_prepare
Clone a repo, run a setup command once, and snapshot it. New threads on that repo start warm.
repo(required): Git URLsetup: Optional setup command, for example npm ci- Returns: The base record