AI teammates that each get their own computer and browser. Built entirely on Cloudflare.
You create bots, give each one a job, and they work for you, even when you're away. They ask before doing anything risky. When one needs a login, it hands you its browser.
Warning
Coworkers is still under heavy development and is experimental. Use it with caution.
Note
@cloudflare/think, @cloudflare/computer, the cf CLI and the
Cloudflare Vite plugin 2.0 are preview or beta. Expect breaking changes.
One Worker runs everything. There are three kinds of Durable Objects:
flowchart LR
UI["Web app<br/>TanStack Start + Kumo"]
W["Worker<br/>src/server.ts"]
Hub["UserHub<br/>1 per user"]
Bot["BotAgent<br/>1 per bot"]
Live["AppLive<br/>1 per bot-built app"]
UI -- "WebSocket" --> W
W --> Hub
W --> Bot
W -- "/apps/*" --> Live
Hub <-- "RPC" --> Bot
Bot --> AI["Workers AI /<br/>AI Gateway"]
Bot --> BR["Browser Run"]
Bot --> DW["Dynamic Workers"]
Hub --> MCP["Your apps<br/>(MCP / Executor)"]
| Durable Object | What it owns |
|---|---|
UserHub |
Your bot list, app connections, shared memory, push subscriptions |
BotAgent |
One bot: chat, memory, rules, routines, files, browser, secrets |
AppLive |
Live updates for a web app a bot built |
- Each bot keeps everything in its own SQLite. There's no separate database.
- You sign in to an app once, on the hub. Every bot uses that connection.
- The Worker only lets you reach your own hub and your own bots.
| Feature | How it works |
|---|---|
| Chat | Streaming, resumable turns. Names itself from your first task |
| Computer | Files in /workspace + a shell (curl, jq) in a Dynamic Worker. No container |
| Read the web | Quick reads (markdown, extract, links, screenshot, PDF, crawl). Kitesurf first, Chromium fallback |
| Drive a browser | A real Chromium session you can watch live |
| Take over | Bot hands you its browser to sign in. You click I'm done, it continues |
| Your apps | Calls tools on connected MCP servers or Executor |
| Rules | Per action type: allow, ask first, or block |
| Routines | Cron or one-off jobs. Can watch a URL/command and only wake when it changes |
| Check-ins | Wakes every few hours, speaks only if something's worth saying |
| Memory | Its own memory + one shared across all your bots. Both have version history |
| Open items | Tracks things it's waiting on until they're resolved |
| Other bots | Messages and files to other bots |
| Web apps | Builds small sites (trackers, dashboards) on its own data, shareable by link |
| Teach by demo | Records you doing a task in its browser, saves it as a skill |
| Secrets | You type an API key in a masked card. The bot uses it but never sees it |
| Wake-ups | Inbound email, webhooks, and push notifications to you |
sequenceDiagram
participant You
participant Bot as BotAgent
participant Model
participant Tools
You->>Bot: "Pull this week's leads"
Bot->>Bot: load memory, rules, open items, app tools
loop up to 40 steps
Bot->>Model: messages + tools
Model-->>Bot: tool call
alt rule is "ask first"
Bot-->>You: approval card
You->>Bot: approve
end
Bot->>Tools: run it
Tools-->>Bot: result
end
Bot-->>You: answer
- A loop guard stops the bot from repeating the same call over and over.
- Unanswered approvals get a push reminder after 4 hours and close after 24.
- Background turns (routines, check-ins, email, webhooks) can use a cheaper
model via
BACKGROUND_MODEL.
Each bot has one setting per action type:
| Action type | Default |
|---|---|
| Edit files | allow |
| Run commands | allow |
| Read the web | allow |
| Drive the browser | allow |
| Use connected apps | ask first |
| Message other bots | allow |
| Schedule routines | allow |
Nobody is around to approve a background turn. So by default it can only use app tools that are read-only.
flowchart LR
Chat["You type"] --> Bot["BotAgent"]
Cron["Routine / check-in"] --> Bot
Mail["Email to user.bot@domain"] --> Bot
Hook["POST /api/hooks/..."] --> Bot
Other["Another bot"] --> Bot
AppEdit["Someone edits a shared app"] --> Bot
Bot --> Push["Push notification to you"]
| Layer | What we use |
|---|---|
| Runtime | Cloudflare Workers, Durable Objects (SQLite) |
| Agents | agents SDK, @cloudflare/think, @cloudflare/computer |
| Models | Workers AI, or any provider through AI Gateway |
| Browser | Browser Run: Chromium, Live View, Kitesurf |
| Sandboxing | Dynamic Workers (shell, bot-built app servers) |
| Inbound | Email Routing, webhooks, Web Push |
| Frontend | TanStack Start, React 19, Tailwind v4, Kumo, Hugeicons |
| Tooling | cf CLI, Vite, Vitest, pnpm |
npm i -g cfpnpm installnpx wrangler loginpnpm devThen open http://localhost:3000.
- Why Wrangler login? Workers AI and Browser Run can't run locally, so dev
calls your real account for those two. Everything else runs on disk under
.cloudflare/state/. - More than one account? Prefix with
CLOUDFLARE_ACCOUNT_ID=<id>. - Secrets: copy
.dev.vars.exampleto.dev.vars. All of them are optional.
You need a Cloudflare account on the Workers Paid plan (Browser Run and Dynamic Workers require it).
Sign in with Cloudflare, pick a name, who can sign in and a model, and the
installer builds Coworkers into your account: the Worker, an AI Gateway, and
Cloudflare Access in front. Re-running it with the same name updates Coworkers
and keeps your bots. How it works and how to host it: installer/.
git clone https://github.com/fayazara/coworkers && cd coworkers
pnpm install
cf auth login
pnpm run setupscripts/setup.ts asks for the account, a name, who can sign in and a model,
then does what the installer does with the cf CLI:
- creates the AI Gateway and Web Push keys
- deploys once with Access required but not yet set up, so the new URL refuses every request instead of being open
- sets up Cloudflare Access: your allow list, plus a bypass for shared app
links (
/apps) and webhooks (/api/hooks), which carry their own tokens - deploys again with the Access application's AUD
Your choices go in coworkers.deploy.json and secrets in .dev.vars.production.
Both are git-ignored. To update later: git pull, then pnpm run setup
again. It remembers your answers, and bots and their data are kept.
Use pnpm run setup, not pnpm setup (a pnpm built-in). Add -- --yes to
skip the questions and reuse your saved answers, e.g. in CI.
Option C: every step by hand
The same result with the dashboard and cf, if you want to see each piece.
With more than one account, prefix cf commands with
CLOUDFLARE_ACCOUNT_ID=<id>.
1. Create the AI Gateway
cf ai-gateway gateways create --id coworkers --authentication --collect-logs \
--cache-invalidate-on-update --cache-ttl 0 \
--rate-limiting-interval 0 --rate-limiting-limit 02. Write coworkers.deploy.json in the repo root. Leaving out the Access
values deploys Coworkers open to everyone as the demo user, so start with
placeholders that lock it:
{
"name": "coworkers",
"gateway": "coworkers",
"accessTeamDomain": "locked.invalid",
"accessAud": "locked"
}3. Secrets and the first deploy
npx web-push generate-vapid-keys
cp .dev.vars.example .dev.vars.production # paste both keys in
pnpm run deploy --secrets-file .dev.vars.productionNote the https://<name>.<subdomain>.workers.dev URL it prints. It answers
401 for now.
4. Cloudflare Access. In Zero Trust (the free plan is enough):
- Access → Policies → Add a policy: action Allow, include Emails for everyone who should get in.
- Add another policy: action Bypass, include Everyone.
- Access → Applications → Add → Self-hosted: public hostname = your Coworkers host, no path, with the Allow policy. Use a public hostname, not a Worker destination: Worker destinations refuse WebSocket upgrades.
- Add a second self-hosted application: your host with path
apps, and your host with pathapi/hooks, with the Bypass policy. - Copy the first application's Application Audience (AUD) Tag from its overview. Your team domain is on Zero Trust's Settings page.
5. Unlock and deploy again. Put the real values in coworkers.deploy.json:
"accessTeamDomain": "<team>.cloudflareaccess.com",
"accessAud": "<aud tag>"pnpm run deploy --secrets-file .dev.vars.productionEither way, Coworkers then checks the Access token on every request and answers 401 without a valid one, so it stays closed even if Access is turned off by mistake. Each email gets its own bots.
Open Models (the brain icon at the bottom of the sidebar). Workers AI models need nothing more. For Anthropic, OpenAI, Google or xAI, add Unified Billing credits or your own provider key to the gateway, then press Test.
Set in cloudflare.config.ts. Everything has a working default.
coworkers.deploy.json, if present, overrides the name, MODEL,
AI_GATEWAY_ID and the two ACCESS_* values.
| Setting | What it does |
|---|---|
MODEL |
Default chat model. @cf/... = Workers AI, provider/model = catalog |
BACKGROUND_MODEL |
Model for routines, check-ins and email. Empty = the chat model |
FALLBACK_MODEL |
Workers AI model a turn retries on when a third-party model fails |
AI_GATEWAY_ID |
Gateway for every model call. Empty = default (created on first use) |
ACCESS_TEAM_DOMAIN |
With ACCESS_AUD, require a valid Access token on every request |
ACCESS_AUD |
The Access application's AUD tag |
QUICK_ACTION_ENGINE |
kitesurf (cheaper) or chromium for quick page reads |
EMAIL_DOMAIN |
Turns on inbound email. Each bot gets <user>.<bot>@<domain> |
BROWSER_RUN_ACCOUNT_ID |
With BROWSER_RUN_API_TOKEN (secret), turns on the crawl tool |
VAPID_* |
Push notification keys (npx web-push generate-vapid-keys) |
The Models page overrides MODEL and BACKGROUND_MODEL for your account,
and each bot can pick its own chat model in its settings.
src/
server.ts Worker entry: auth, /agents/*, /apps/*, inbound email
agents/
user-hub.ts one per user: bot list, connections, shared memory, push
bot-agent.ts one per bot: tools, rules, routines, computer, browser
prompt.ts the bot's system prompt
web-tools.ts quick web read tools
apps-runtime.ts serves bot-built apps + AppLive
app-store.ts app data and share links
teach.ts records a demo in the browser
secrets.ts credentials the bot never sees
guards.ts stops runaway loops
memory.ts memory helpers
models.ts model factory: AI Gateway, fallback, log metadata
routes/
_app/ bot thread, computer + settings panels, connections, models
api/hooks.*.ts webhook endpoint
components/ thread, Computer card, sidebar, panels
lib/ shared types, server functions, React contexts
installer/ "Deploy to Cloudflare" wizard (its own Worker)
scripts/setup.ts the same deploy, from your terminal with cf
cloudflare.config.ts Worker config and bindings
AGENTS.md read this first if you're an AI coding agent
| Command | What it does |
|---|---|
pnpm dev |
Dev server on :3000, running on workerd |
pnpm build |
Production build |
pnpm run setup |
Deploy with AI Gateway + Access (asks) |
pnpm run deploy |
Build and deploy |
pnpm test |
Vitest |
pnpm typecheck |
Regenerate binding types, then tsc |
pnpm format |
Prettier |