# 🤖 Coworkers
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.
## The big picture
One Worker runs everything. There are three kinds of Durable Objects:
```mermaid
flowchart LR
UI["Web app
TanStack Start + Kumo"]
W["Worker
src/server.ts"]
Hub["UserHub
1 per user"]
Bot["BotAgent
1 per bot"]
Live["AppLive
1 per bot-built app"]
UI -- "WebSocket" --> W
W --> Hub
W --> Bot
W -- "/apps/*" --> Live
Hub <-- "RPC" --> Bot
Bot --> AI["Workers AI /
AI Gateway"]
Bot --> BR["Browser Run"]
Bot --> DW["Dynamic Workers"]
Hub --> MCP["Your apps
(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.
## What a bot can do
| Feature | How it works |
| -------------- | ---------------------------------------------------------------------------- |
| Chat | Streaming, resumable turns. Names itself from your first task |
| Computer | Files in `/workspace`, a shell (`curl`, `jq`) and JavaScript scripts in Dynamic Workers. No container. Scripts survive deploys and can pause for your OK before touching your apps |
| 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 |
## How a turn works
```mermaid
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`.
## Rules
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.
## Ways to wake a bot
```mermaid
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"]
```
## Stack
| 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, scripts, 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 |
## Run it locally
```bash
npm i -g cf
```
```bash
pnpm install
```
```bash
npx wrangler login
```
```bash
pnpm dev
```
Then 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=`.
- **Secrets:** copy `.dev.vars.example` to `.dev.vars`. All of them are
optional.
## Deploy
You need a Cloudflare account on the **Workers Paid** plan (Browser Run and
Dynamic Workers require it).
### Option A: the installer
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/`](installer/README.md).
### Option B: one command
```bash
git clone https://github.com/fayazara/coworkers && cd coworkers
pnpm install
cf auth login
pnpm run setup
```
`scripts/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:
1. creates the AI Gateway and Web Push keys
2. deploys once with Access required but not yet set up, so the new URL
refuses every request instead of being open
3. sets up Cloudflare Access: your allow list, plus a bypass for shared app
links (`/apps`) and webhooks (`/api/hooks`), which carry their own tokens
4. 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=`.
**1. Create the AI Gateway**
```bash
cf ai-gateway gateways create --id coworkers --authentication --collect-logs \
--cache-invalidate-on-update --cache-ttl 0 \
--rate-limiting-interval 0 --rate-limiting-limit 0
```
**2. 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:
```json
{
"name": "coworkers",
"gateway": "coworkers",
"accessTeamDomain": "locked.invalid",
"accessAud": "locked"
}
```
**3. Secrets and the first deploy**
```bash
npx web-push generate-vapid-keys
cp .dev.vars.example .dev.vars.production # paste both keys in
pnpm run deploy --secrets-file .dev.vars.production
```
Note the `https://..workers.dev` URL it prints. It answers
401 for now.
**4. Cloudflare Access.** In [Zero Trust](https://one.dash.cloudflare.com/)
(the free plan is enough):
1. **Access → Policies → Add a policy:** action **Allow**, include
**Emails** for everyone who should get in.
2. **Add another policy:** action **Bypass**, include **Everyone**.
3. **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.
4. **Add a second self-hosted application:** your host with path `apps`, and
your host with path `api/hooks`, with the Bypass policy.
5. 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`:
```json
"accessTeamDomain": ".cloudflareaccess.com",
"accessAud": ""
```
```bash
pnpm run deploy --secrets-file .dev.vars.production
```
Either 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.
### After deploying: models
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](https://developers.cloudflare.com/ai-gateway/features/unified-billing/)
credits or your own provider key to the gateway, then press **Test**.
## Configuration
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 `.@` |
| `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.
## Project layout
```
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
```
## Scripts
| 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 |