This is the abridged developer documentation for Cowork Studio # Overview > Build apps for Cowork Studio — widgets that use the window.cowork SDK, routines run by the Cowork daemon, the cowork.json manifest, and the cowork CLI. A Cowork workspace carries its own **apps**. An app is a folder `.cowork/apps//` with a `cowork.json` manifest, React **widgets** (`.jsx` / `.tsx`, the UI, drawn in the IDE) that call the `window.cowork` SDK, and **routines** (`.ts`, privileged backend code run by the local Cowork daemon: secrets, files, signed-in APIs). Copy the folder and you have copied the app. [Build your first app](/docs/guides/building-apps/)The pattern every app follows, starting from the CRUD template. [cowork.json](/docs/reference/cowork-json/)Every manifest field: routines, file routing, accounts, capabilities, the status bar. [The window.cowork SDK](/docs/reference/sdk/interfaces/coworksdk/)What a widget can call: routines, dialogs, accounts, notifications, capture, state. [The CLI](/docs/reference/cli/)Every cowork command: routines, apps, publishing, secrets, team servers. ## I want to… [Section titled “I want to…”](#i-want-to) | … | In a widget (`window.cowork`) | In a routine (`ctx`) | Also | | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | Ask the user (a dialog, a confirm) | [`ui.dialog` / `ui.confirm`](/docs/reference/sdk/interfaces/coworksdk/#ui), options in [`CoworkDialog`](/docs/reference/sdk/interfaces/coworkdialog/) | [`consent.request`](/docs/reference/sdk/interfaces/coworkroutinecontext/#consent) | | | Use an API key, or sign in to Google / Microsoft | [`credentials.get` / `connect`](/docs/reference/sdk/interfaces/coworksdk/#credentials) — widgets never see the key | [`credential.fetch` / `fields`](/docs/reference/sdk/interfaces/coworkroutinecontext/#credential) | [`credential` in cowork.json](/docs/reference/cowork-json/#credential), [`cowork credential`](/docs/reference/cli/#cowork-credential) | | Run backend code | [`routines.run`](/docs/reference/sdk/interfaces/coworksdk/#routines) | [Writing routines](/docs/guides/routines/) | [`cowork routine run`](/docs/reference/cli/#cowork-routine) | | Keep data fresh | [`routines.subscribe`](/docs/reference/sdk/interfaces/coworksdk/#routines) — never `setInterval` | [`unchanged()`](/docs/reference/sdk/interfaces/coworkroutinecontext/#unchanged) for reads | | | Store the app's data | [`state`](/docs/reference/sdk/interfaces/coworksdk/#state) (per view, small) | [`data`](/docs/reference/sdk/interfaces/coworkroutinecontext/#data), [`db`](/docs/reference/sdk/interfaces/coworkroutinecontext/#db) | | | Notify the user | [`notify`](/docs/reference/sdk/interfaces/coworksdk/#notify), `badge` | [`notify`](/docs/reference/sdk/interfaces/coworkroutinecontext/#notify), `badge` | | | Record the screen or audio | [`capture`](/docs/reference/sdk/interfaces/coworksdk/#capture) | — panes only | [`capture` in cowork.json](/docs/reference/cowork-json/#capture) | | Hand work to an agent | | [`queue`](/docs/reference/sdk/interfaces/coworkroutinecontext/#queue) | [`cowork queue`](/docs/reference/cli/#cowork-queue) | | Open a file type in my app | [`openedFile`](/docs/reference/sdk/interfaces/coworksdk/#openedfile) | | [`fileRouting`](/docs/reference/cowork-json/#filerouting) | | Let AI agents call my routines | | mark each routine `readOnly` / `destructive` / `reachesOthers` ([`routines` in cowork.json](/docs/reference/cowork-json/#routines)) | [Agents and grants](/docs/guides/permissions/), [`cowork agent`](/docs/reference/cli/#cowork-agent) | | Publish or install an app | | | [Publish and install](/docs/guides/publish-and-install/), [`cowork push`](/docs/reference/cli/#cowork-push) | ## How these docs are made [Section titled “How these docs are made”](#how-these-docs-are-made) Nothing under **Guides** or **Reference** is edited by hand. Every Cowork build regenerates them from what ships: * **Guides**: the `cowork-studio` Agent Skill that Cowork installs for Claude Code and Codex. * **SDK types**: the `.cowork/local/cowork.d.ts` every workspace gets, rendered by TypeDoc. * **cowork.json** and **workspace.json**: `cowork.schema.json` and `workspace.schema.json`, the schemas the editor validates them against. * **CLI**: the CLI's usage table — every command, including the group verbs only `cowork --help` prints. * **Commands**: the `contributes.commands` of Cowork's extensions. ## For AI agents [Section titled “For AI agents”](#for-ai-agents) * [`/docs/llms.txt`](/docs/llms.txt), [`llms-full.txt`](/docs/llms-full.txt) (everything) and [`llms-small.txt`](/docs/llms-small.txt) follow [llmstxt.org](https://llmstxt.org). * On a machine with Cowork, the same guides are the `cowork-studio` skill (`~/.claude/skills/cowork-studio/`), matched to the installed version, and every workspace has the types at `.cowork/local/cowork.d.ts`. # Building Cowork apps > Cowork Studio is an IDE (VS Code-based) where a workspace can carry its own apps. Cowork Studio is an IDE (VS Code-based) where a workspace can carry its own **apps**. An app is a folder `.cowork/apps//` holding a **`cowork.json`** manifest plus its files: `.jsx` widgets (UI, rendered in the IDE) and `.ts` routines (privileged backend code run by the local Cowork daemon). Copying the folder copies the app — there is nothing to register elsewhere. The details, one page each: [manifest](/docs/guides/manifest/) · [widgets + SDK](/docs/guides/widgets-and-sdk/) · [routines](/docs/guides/routines/) · [publish / install / audit](/docs/guides/publish-and-install/) · [agents and grants](/docs/guides/permissions/) · [courses](/docs/guides/courses/). Read the one you need before writing code. ## Make an app — start from the CRUD template [Section titled “Make an app — start from the CRUD template”](#make-an-app--start-from-the-crud-template) ```bash cowork template install cowork/app-sidebar-workbench --name clients # → .cowork/apps/clients/ ``` That is the reference app every Cowork app follows: a list in the sidebar, a page per record (`fileRouting`), and `clients-list` / `-read` / `-save` / `-remove` routines over one file per record. Its `README.md` walks through the pattern below. Change what a record is (or point the routines at a service) instead of starting from a blank file. ### The pattern — follow it in every app [Section titled “The pattern — follow it in every app”](#the-pattern--follow-it-in-every-app) 1. **Widgets show, routines do.** Files, keys, the network → a routine; the widget calls it. 2. **Data in `ctx.data`** (`.cowork/data//`): `read(name, fallback)`, `write(name, value)`, `list(subfolder)`, `remove(name)`, `path(name)`. Never `fs` + hand-built paths. 3. **Fresh with `cowork.routines.subscribe`, never `setInterval`**: one loop per app, paused while no pane is visible, stopped by `NotConnected` until `refresh()`. 4. **Fail with a name**: `throw ctx.error("NotFound", "…")`; the widget checks `e.name`, never the message. Show `e.runId` with the error (`cowork routine runs show `). 5. **Mark reads** with `ctx.unchanged()`; **log changes** with `ctx.log(...)`. 6. **The app's panes talk** with `cowork.app.post(msg)` / `cowork.app.onMessage(cb)`. 7. **Credentials belong to a capability**: its cowork.json declares `"credential"` (a sign-in, a key, or either); its routines call the service with `ctx.credential.fetch(url)` (bearer, refresh, retries) or read a key with `ctx.credential.fields()`. An app declares `"uses"`, calls those routines and touches no credential (`NotGranted`); its panes connect with `cowork.credentials.get/connect(name)` (one sign-in per provider, Manage › Components › Credentials). A package declaring neither `kind` nor `uses` predates capabilities and keeps its old access until it declares one. 8. **Paint at once**: last answer in `cowork.state`, skeleton rows until the first one. A failed ACTION keeps its own error (cleared by the next action or Dismiss) — the next good read clears only a failed read. 9. **Mark what each capability routine changes** (`readOnly`, `destructive`, `reachesOthers`, `description`, `input`): agents get a capability's routines as tools — reads on by default, the rest only when granted ([agents and grants](/docs/guides/permissions/)). ### From scratch [Section titled “From scratch”](#from-scratch) 1. Create `.cowork/apps//cowork.json` (the folder name follows the same lowercase-kebab rule as `name`, and is normally the same; `name` = the id): ```json { "name": "weather", "version": "0.1.0", "title": "Weather", "description": "Today's forecast.", "icon": "cloud", "color": "#1A73E8", "entry": "weather-sidemenu.jsx", "display": "sidebar", "routines": { "weather-forecast": "forecast.ts" } } ``` 2. Write the widget `weather-sidemenu.jsx` (a default-exported React component calling `cowork.routines.subscribe("weather-forecast", …)`) and the routine `forecast.ts` (`export default async (args, ctx) => {...}` returning JSON). 3. Test: `cowork routine run weather-forecast --args '{"city":"Lisbon"}'`, then click the new icon on the activity bar (saving a `.jsx` hot-reloads its pane). 4. Share: `cowork push .cowork/apps/weather --to `. ## cowork.json in one screen [Section titled “cowork.json in one screen”](#coworkjson-in-one-screen) | key | notes | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` **req.** | `^[a-z0-9][a-z0-9-]{0,62}$` (lowercase letters, digits, `-`) — store id + activity-bar id; not `apps`, `data`, `courses`, `local` or a Windows device name (`con`, `nul`…). Never rename a published app. | | `version` **req.** | semver `X.Y.Z`. `cowork push` publishes it (and bumps the patch when needed). | | `title`, `description` | Display name (default `name`); store blurb. | | `icon` | A **codicon** id (`mail`, `graph`, `database`, `rocket` …) **or** an image file in the app folder (`icon.png`; .png/.svg/.webp/.jpg ≤ 256 KB) — the image fills the rail tile, full color, and `cowork push` sends it to the store with each version. Never a URL. | | `color` | Hex tile color `#rrggbb`; the glyph turns white. | | `entry` | `.jsx`/`.tsx` UI. Present → an activity-bar icon. Omit for a routines-only app — or a **capability** (`"kind": "capability"`: routines + a `SKILL.md`, no UI) that apps name in `"uses": {"gmail": "0.2.0"}` (name → lowest version, same major); a template install pulls it. Or `"web": {"index": "web/index.html"}` instead of `entry`: a **web app** — a prebuilt static site opened unmodified in an editor tab, files routed to its index (desktop app or browser). Or `"web": {"url": "https://…"}`: a **site app**, a web site installed as an app (`cowork app install ` or the browser's install icon; desktop app only). See [manifest](/docs/guides/manifest/). | | `display` | `sidebar` (default: panel under the icon) · `tab` (editor tab) · `modal` (dialog) · `right` (a pane of the right side bar, beside Chat — no rail icon). | | `open` | `sidebar` only: an app file or `{"routine": id, "args": {}}` returning `{path}` — opened in the editor on first click. | | `routines` | `{ "-": "file.ts" }` or `{ "-": { "file": "file.ts", "readOnly": true, "description": "…", "input": {…}, "timeout": 120 } }` — ids are workspace-global, **prefix them with the app name**. In a capability each routine is a tool for agents: `readOnly` (changes nothing), `destructive`, `reachesOthers`, a `description` and its arguments as a JSON Schema `input`. | | `fileRouting` | `{ "**/*.ext": "viewer.jsx" }` — workspace-relative glob → the widget that opens matching files. A glob ending in `/` routes a folder instead (`"Notebook/": "notebook.jsx"`): clicking it in the Explorer opens the widget. | | `statusBar` | `{"routine": id, "args"?: {}, "interval"?: 60}` — one footer item drawn from what one of the app's routines returns (`{text, icon?, tooltip?, tone?, click?: {args, confirm?}, menu?, data?}` or null); a click re-runs it with `click.args`. See "Footer item" in [manifest](/docs/guides/manifest/). | | `credential` | **Capabilities only.** How its routines reach their service: `{"oauth": {"provider": "google", "scopes": ["https://www.googleapis.com/auth/gmail.modify"]}}` (a sign-in, shared by every capability of that provider — connecting asks for what they all need), `{"key": {"fields": ["token"], "url": "https://…", "hint": "…"}}` (fields the person pastes), or both. An app's own `credential` is ignored (reported). | | `requires` | `["desktop"]` if it needs the desktop app (e.g. `ctx.browser`); `["web"]`; or omit. | | `tags` | Store filters. | All manifest paths are relative to the app folder and must stay inside it. A bad `name` or `version` = not an app; any other bad key is dropped and the app still works. The IDE validates the file as you type. **`.cowork/workspace.json` is workspace STATE only** — `startup.home` (the window's start app; default: the app named `home`), `disabled` switches (`{"apps": [".cowork/apps/weather"], "routines": ["weather-forecast"]}`), `publishTo`, `rail.folders`, `deployments` (routines' schedules / webhooks: `cowork deploy add`, see [routines](/docs/guides/routines/)); it never travels. **What an app is lives only in its `cowork.json`.** Under `.cowork/`: `apps//` (a Keep-both copy beside it: `-v1-2-3`), `data//` (`ctx.data`; a table is `data/.db`), `courses//`, `lessons//`, git-ignored `local/`. Never create `Cowork/`, `Webapps/`, `Routines/` or `Courses/` in the workspace root. ## Widgets (.jsx) — the hard rules [Section titled “Widgets (.jsx) — the hard rules”](#widgets-jsx--the-hard-rules) * One file, one module, `export default` a React component. * **No relative imports** (`./util` fails). Each widget is standalone; duplicate small helpers, move shared/privileged logic into a routine. * **Only allowlisted imports**: `react`, `react-dom`, `@/components/ui/*` (stock shadcn/ui), `@/lib/utils` (`cn`), `lucide-react`, `recharts`, `@xyflow/react`, `@xterm/xterm` (+ fit / web-links addons), `radix-ui`, `sonner`, `cmdk`, `vaul`, `react-resizable-panels`, `react-day-picker`, `embla-carousel-react`, `input-otp`, `clsx`, `tailwind-merge`, `class-variance-authority`. Nothing else — no npm installs, no CDN. * **No secrets, no filesystem, no credentialed network in a widget.** It runs in a webview. Anything needing a key, a file or an authenticated API → a routine. * **Theme with tokens**: `bg-background text-foreground`, `bg-card`, `text-muted-foreground`, `border`, `bg-primary text-primary-foreground`. They follow the workbench light/dark theme; `dark:` variants follow it too. Don't hardcode colors. * `alert`/`confirm`/`prompt` don't work — use `cowork.ui.dialog(...)` / `cowork.ui.confirm(...)` or inline UI. `window.cowork` essentials (full table in [sdk](/docs/guides/widgets-and-sdk/)): ```js await cowork.routines.run(id, args, { timeoutMs }) // → the routine's return value; rejects with e.name / e.runId const s = cowork.routines.subscribe(id, args, { interval: 60, onData, onError }) // keep it fresh — s.refresh(), s.close() cowork.app.post(msg) / cowork.app.onMessage(cb) // this app's other panes (sidebar ↔ tabs) await cowork.credentials.get("google") / .connect("google") // { state, account?, missingScopes, … } — the shared sign-in cowork.state.get() / cowork.state.set(value) // persisted per view — cache for instant paint cowork.openFile("path/in/workspace") // opens through fileRouting cowork.openedFile?.workspaceRelativePath // when opened for a routed file (path only — read it in a routine) cowork.log(...) // "Cowork" Output panel cowork.visible / cowork.onVisibility(cb) // on screen? (subscribe already gates on it) cowork.tables.query({ db, sql }) // SQLite .db files (read-only query, exec for writes) cowork.queue.send("tasks", { body }) / cowork.journal.add({ skill, text }) // agent task queues + the skill journal cowork.agent.setContext(v) / cowork.agent.select(t) // feed the IDE's AI agent cowork.ui.dialog(opts) / cowork.ui.confirm({ title, body }) / cowork.chat.ask(text) / cowork.openExternal(url) / cowork.capture.record(opts) // capture: after Cowork's dialog cowork.browser.open(url) / cowork.browser.status() // the page beside the IDE (desktop app); else openExternal cowork.statusBar.get(app)?.data / .onChange(cb) / .refresh(app) / .click(app) // footer items await cowork.identity.currentUser() // { uid, email } or null ``` ## Routines (.ts) — the rules [Section titled “Routines (.ts) — the rules”](#routines-ts--the-rules) ```ts export default async (args: { city: string }, ctx: CoworkRoutineContext) => { ctx.unchanged(); // a read: the audit keeps only that it ran // the `weather` capability ("credential": {"key": {"fields": ["key"]}}): its fields, or NotConnected naming where to connect const { key } = ctx.credential.fields(); const res = await fetch(`https://api.example.com/forecast?q=${encodeURIComponent(args.city)}&key=${key}`); if (res.status === 429) throw ctx.error("RateLimited", "The forecast service is busy — try again in a minute."); if (!res.ok) throw new Error(`forecast ${res.status}`); const forecast = await res.json(); ctx.data.write("last-forecast.json", forecast); // .cowork/data/weather/ — never inside the app folder return forecast; // JSON-serializable, or the caller gets null }; ``` * A fresh Bun process per run (full Node/Bun APIs). Default timeout 30 s (widgets may pass up to 300 s). Throw to fail. * `ctx`: `workspaceRoot`, `unchanged()`, `error(name, message)`, `data` (the app's data folder), `identity` (who it runs for), `credential.fetch/token/fields/status/start/disconnect` (the capability's connection), `secrets.get/list/set` (its cached values), `log`, `db(path)`, `queue(name)`, `journal.add`, `auth.getState/getIdToken`, `command.run` (consent-gated shell), `consent.request`, `browser.*` (desktop only), `notify` / `badge` (an OS notification + badge count; widgets: `cowork.notify/badge`). Details: [routines](/docs/guides/routines/). * **Every run is audit-logged** (7 days, on this computer): its inputs, and — for a run that failed or changed something — a 200-character result preview and its last log lines. Call `ctx.unchanged()` in a read so it records only that it ran. Never pass a secret as an argument: a key is connected by the person — `cowork.credentials.connect(name, { token })` from the app's pane, Manage › Components › Credentials, or `cowork credential connect --field token=…` ("Connecting" in [routines](/docs/guides/routines/)). Names under `cowork.` are reserved; a capability reaches its own connection and `.*`, an app none (`NotGranted`). * **Fail with a name**: `throw ctx.error("NotConnected", "Paste your token in the sidebar.")` — the widget's promise rejects with `e.name === "NotConnected"` (and `e.runId`, for `cowork routine runs show `). `ctx.credential.*` already throws `NotConnected`. * Run it: `cowork routine run --args '{"k":"v"}' [--workspace ]` (prints the result). * On a server (no IDE): `cowork start --headless` runs them — schedules and webhooks (`cowork deploy add`) too; a command a routine runs is approved ahead of time with `cowork consent allow ''`. See [routines](/docs/guides/routines/) ("Without the IDE"). ## Agents and grants [Section titled “Agents and grants”](#agents-and-grants) Agents (Claude Code, Codex, any MCP client) reach a workspace's **capabilities** through one door on the Cowork daemon, each with its own login (`cowork agent create `, or Manage Workspace › Permissions › Agent access, which prints the `claude mcp add` line to run in the workspace folder). A capability's routines are its tools; mark each one in its cowork.json: ```json "routines": { "invoices-list": { "file": "list.ts", "readOnly": true, "description": "Lists open invoices" }, "invoices-approve": { "file": "approve.ts", "reachesOthers": true, "description": "Approves an invoice and emails the supplier", "input": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"] } } } ``` * A `readOnly` routine is granted to every agent by default (and its client runs it without asking); everything else is off until the owner grants it — per workspace, a default per routine and an override per agent (`cowork agent grant invoices/invoices-approve allow --agent claude`). * Only capabilities are offered to agents; the owner (panes, `cowork routine run`, schedules, webhooks) is never gated by grants. Every agent call is audited with the agent and its task. * A routine needing a connection that is not connected is unavailable to agents whatever is granted. Details, commands and examples: [agents and grants](/docs/guides/permissions/). ## Lifecycle [Section titled “Lifecycle”](#lifecycle) ```bash cowork team-server list # where you can publish / install from cowork push [dir] [--to [--remember]] [--dry-run] # publish the manifest version (bumps the patch if needed); --remember = default target cowork app list [--json] # every app here: version, source, pinned|auto, on/off cowork app install /[@X.Y.Z] # a version PINS it; without, it follows the store cowork app update [] [--replace|--keep-both|--ai-merge|--skip] # a name unpins cowork app versions / cowork app enable|disable | remove cowork app unpublish /[@X.Y.Z] --yes # the whole app, or one older version cowork routine list | run [--args ''] # every verb: [--workspace ] cowork routine runs [show ] [--team-server ] # executions, newest first ``` * Versions only move forward; a store newer than your copy refuses the push ("update first"). * Clean, unpinned installs **auto-update**; an edited app is never overwritten (the store asks). Keep user data outside the app folder so the app stays clean. * Every routine run and lifecycle action is appended to the local audit log `~/.cowork-studio/workspaces/-<8-hex hash>/audit/.jsonl` (Windows: under `%LOCALAPPDATA%\Cowork`; who, which app + version, device, org, agent, inputs; kept 7 days). See [lifecycle](/docs/guides/publish-and-install/). ## Courses [Section titled “Courses”](#courses) A course is `.cowork/courses//course.md` (front matter `title`, `level`, `lessons:` — lesson ids, in order; the body is its description). A lesson, in any number of courses, is a folder: `.cowork/lessons//lesson.md` (front matter `title`, `minutes`, `video` = a YouTube/Vimeo/Loom link; the body is markdown) plus the pictures it shows, referenced relatively. Check it with `cowork publish .cowork/courses/ --dry-run`, publish with `--team-server `. Full format: [courses](/docs/guides/courses/). ## Test loop [Section titled “Test loop”](#test-loop) 1. `cowork routine run --args '…'` for each routine — fix errors here first. 2. Save the widget → its pane/tab hot-reloads. New app or changed `cowork.json` → the icon appears/updates on the activity bar (reload the window if it doesn't). 3. `cowork app open .cowork/apps//.jsx [--as tab|dialog|sidemenu]` opens a widget directly. 4. Logs: the "Cowork" Output panel (`cowork.log`, `ctx.log`, routine stdout/stderr). Check the rail: icon, color, `title`. 5. Tests inside an app (`.cowork/apps//*.test.ts`): `bun test` skips dot-folders, so name the path: `bun test ./.cowork/apps/`. ## Checklist before you finish [Section titled “Checklist before you finish”](#checklist-before-you-finish) * [ ] `cowork.json` has a valid `name` + semver `version`; paths are inside the folder. * [ ] Routine ids are prefixed with the app name; every routine returns JSON. * [ ] No relative imports and only allowlisted imports in `.jsx`. * [ ] No `setInterval` polling — `cowork.routines.subscribe`; no `/Not connected/` matching — `e.name`. * [ ] App data through `ctx.data`; reads call `ctx.unchanged()`; sign-ins and keys declared in the capability's `credential`, never the app's. * [ ] Every capability routine says what it changes (`readOnly` / `destructive` / `reachesOthers`) and has a `description`; a routine taking arguments has an `input` schema. * [ ] No secrets/keys in widgets, in `cowork.json`, in routine args, or committed files. * [ ] Colors via theme tokens; the UI fills its pane and scrolls inside. * [ ] Nothing new added to `.cowork/workspace.json` except state (`disabled`, `publishTo`, `rail`, `startup`; `deployments` via `cowork deploy add`). * [ ] Ran each routine once from the CLI; opened the app once. * [ ] A short `SKILL.md` says how an agent USES the app (see "Folder layout" in [manifest](/docs/guides/manifest/)). # Courses — markdown files > A course is course.md plus the lessons it lists: people and agents write them as files, the team server publishes the course, and subscribers take it in the Academy (lessons in order, video on top, progress kept). A course is **course.md** plus the **lessons** it lists: people and agents write them as files, the team server publishes the course, and subscribers take it in the **Academy** (lessons in order, video on top, progress kept). A lesson is a workspace's own folder, so one lesson can be in several courses. ```plaintext .cowork/courses/onboarding/ the folder name is the course's store name (lowercase, digits, -) course.md front matter (its lessons: line) + the description people read before subscribing assets.json optional, like any published folder: {"item": {"tags": ["sales"]}} .cowork/lessons/ welcome/ a lesson: the folder name is its id ("welcome") lesson.md front matter + the lesson in markdown home.png a picture it shows: ![The Home screen](home.png) first-steps/ lesson.md shots/editor.webp subfolders are fine: ![The editor](shots/editor.webp) ``` ## course.md [Section titled “course.md”](#coursemd) ```markdown --- title: Sales Onboarding level: Beginner lessons: welcome, first-steps --- Everything a new rep needs in their first week — the tools, the pipeline, the first call. ``` * `title` **required**; `level` = `Beginner` | `Intermediate` | `Advanced` (optional). * `lessons` — the lesson ids it teaches, comma-separated, **in order**: the line is the course. Each id is a folder in `.cowork/lessons/`. Add a lesson to a course by adding its id; another course may list the same one. * The body is the course's description (markdown, ≤ 2000 characters) — the listing shows it. ## lesson.md [Section titled “lesson.md”](#lessonmd) ```markdown --- title: Your first call minutes: 8 video: https://www.youtube.com/watch?v=M7lc1UVf-VE --- What to say in the first two minutes, and what to log after. ## Before the call - Read the account notes. ![The account page](account.png) ## After the call Log it in the CRM. See [the call guide](https://example.com/guide). ``` * `title` **required**; `minutes` a whole number (optional); `video` optional — a **YouTube, Vimeo or Loom** link, played above the lesson. * The body is the lesson. `##` headings are its sections; lists, tables, `code`, quotes all render. * Front matter is plain `key: value` lines between `---` lines (quotes optional). ## Rules (publish checks every one and names the file) [Section titled “Rules (publish checks every one and names the file)”](#rules-publish-checks-every-one-and-names-the-file) * Every id on the `lessons:` line is a folder `.cowork/lessons//` holding `lesson.md`; ids are lowercase letters, digits and `-`, unique in the line. Reordering the line reorders the course without losing anyone's progress; renaming a lesson's folder makes it a new lesson (fix the line of every course that lists it). * Published, the course is one folder: each lesson travels as `NN-/` beside course.md (NN = its place). A course folder may also hold its own `NN-/` lesson folders — then the line must list each of them too. * Pictures are `.png .jpg .jpeg .gif .webp .svg` files **in the lesson's own folder** (or its subfolders), referenced relatively — or an `https://` image. Never `../`, never another lesson's. * Links go to `https://` pages or `#headings` — a relative link can't open from the Academy. * At most 40 lessons; the whole course ≤ 25 MB (keep video out of it — link it). * No scripts, no iframes: the Academy sanitizes the markdown. ## Make, check, publish [Section titled “Make, check, publish”](#make-check-publish) ```bash # Create → Courses → New course in the IDE writes .cowork/courses//course.md and a first lesson, # .cowork/lessons/-welcome/ — or write the files yourself. cowork publish .cowork/courses/onboarding --dry-run # checks it: prints the outline, or the problem cowork publish .cowork/courses/onboarding --team-server acme # publish (admins of that team server) cowork team-server store acme # it is listed as a course ``` In the IDE: **Manage Workspace → Components → Courses & Lessons** lists both; a course's page adds and removes its lessons, a lesson's page puts it in courses or deletes it (from every course). **Publish… → A course** publishes one. Publishing again makes the next version; subscribers get it within a minute. A course is **subscribed**, never installed: on the team server's page → **Subscribe** → it appears in the Academy. If a course was unpublished and is published again under the same name, it comes back as the same course: subscriptions and progress resume. # cowork.json — the app manifest (full reference) > A folder .cowork/apps// containing a cowork.json is a Cowork app. A folder `.cowork/apps//` containing a `cowork.json` is a Cowork app. Apps are discovered ONE level deep under `.cowork/apps/`, in folder-name order. A folder counts only when its own name is lowercase kebab (`^[a-z0-9][a-z0-9-]{0,62}$`, not `apps`/`data`/`courses`/`local`, not a Windows device name such as `con` or `nul`) — dotted, uppercase, symlinked and dot-prefixed folders are skipped. A Keep-both copy of an update sits beside yours as `-v1-2-3`. Every path inside the manifest is RELATIVE TO THE APP FOLDER, uses forward slashes, and may not escape it (no leading `/`, no `..`, no `\`, no drive letters). The IDE validates `.cowork/apps/*/cowork.json` against the manifest schema as you type — no `$schema` key is needed (unknown keys, `$schema` included, are ignored). ## Keys [Section titled “Keys”](#keys) | key | type | rules | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | string, **required** | `^[a-z0-9][a-z0-9-]{0,62}$`. Store id AND activity-bar id. Unique per workspace (a second folder claiming it is reported broken) and per team-server store. Never rename a published app — a new name is a new app. | | `version` | string, **required** | semver `X.Y.Z` (optional `-pre` / `+build`), no leading `v`. | | `title` | string ≤ 200 | Display name (rail tooltip, pane title, store card). Default: `name`. | | `description` | string ≤ 2000 | One or two sentences for the store card. | | `icon` | string | A codicon id (`mail`, `calendar`, `rocket`, `graph`, `database`, `terminal` … — ) or `$(mail)`, **or** an image file inside the app folder (`icon.png`, `assets/logo.svg`; `.png`/`.svg`/`.webp`/`.jpg`, ≤ 256 KB, not a symlink). An image fills the rail tile in full color (centered, never cropped; `color` shows behind transparent parts) — square art with its own padding looks best. Manage Workspace and the Team Servers store show it too (`cowork push` puts it on the store listing; a codicon clears an old image there). Never a URL. Absent → a letter tile. | | `color` | string | Hex tile color `#rgb` / `#rrggbb` (e.g. `#EA4335`); the glyph turns white. | | `entry` | string | The UI, `.jsx` or `.tsx`. Present → the app gets its own activity-bar icon. Absent → a routines-/routes-only app (no icon). | | `web` | `{ "index": "<.html file>", "open"?: "drop:", "download"?: { "url", "sha256" } }` | Makes the folder a **web app** instead of a JSX one — see "Web apps" below. Never together with `entry` (the `entry` wins, `web` is dropped with an error). | | `display` | `sidebar` \| `tab` \| `modal` | What clicking the icon does with `entry`. Default `sidebar`; a web app is always `tab` (another value is dropped with an error). | | `open` | string \| `{ "routine": id, "args"?: {…} }` | **`sidebar` only.** What to open in the EDITOR the first time the icon is clicked in a session: an app file, or a routine that returns `{ "path": "" }` (for content that must be fetched/materialized first). Ignored (with an error) for `tab`/`modal`. | | `routines` | `{ id: file }` or `{ id: { "file": file, "timeout"?: s, "readOnly"?, "destructive"?, "reachesOthers"?, "description"?, "input"? } }` | id `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` → `.ts` / `.js` / `.mjs` file. ≤ 100. `timeout` (optional): how long one run may take, whole seconds 1–300 (default 30). The rest says what the routine is as a **tool** — what an agent sees and is granted ([agents and grants](/docs/guides/permissions/)): `readOnly: true` it changes nothing (on for every agent by default; never together with the next two), `destructive: true` what it changes cannot be undone, `reachesOthers: true` it reaches people beyond the account (sends, posts, pays); `description` one line (≤ 400 chars); `input` its arguments as a JSON Schema `{"type": "object", "properties": {…}, "required": […]}` (≤ 8 KB). A bad tool key is reported and left at its safe default (a write, no description); the routine stays. A bare `"id": "file.ts"` is a write with no description. A `permission` key is ignored (reported). | | `fileRouting` | `{ glob: appFile }` | A WORKSPACE-relative glob (`**/*.maillist.json`) → an app file in this folder that renders matching files. ≤ 50. A glob ending in `/` routes FOLDERS (`"Notebook/"`, `"Projects/*/"`): clicking the folder in the Explorer opens the app, and a folder route never matches a file (nor a file route a folder); its value must be a `.jsx`/`.tsx`. An `.html` value opens the file in the app's web app, so it must be its `web.index` (any other `.html` value is dropped with an error). | | `credential` | `{ "oauth"?: { "provider", "scopes", "ownClient"? }, "key"?: { "fields", "url"?, "hint"? } }` | **A capability's** connection to its service — a sign-in, a pasted key, or either (one at least). `oauth`: `provider` an OAuth preset (`google`, `microsoft`), the `scopes` its routines need (≤ 32); every capability declaring a provider shares ONE sign-in, named after it (`google`), and connecting asks for what they all need together; `ownClient: true` signs in through the user's own client instead, under the capability's own name. `key`: the `fields` the person pastes (≤ 8, lowercase letters and `_`, e.g. `["token"]`), stored as `.`; `url` (https) where to get them, `hint` (≤ 200 chars) one line on how. The person connects once — Manage Workspace › Components › Credentials, an app's pane (`cowork.credentials.connect`), or `cowork credential connect`; routines use `ctx.credential.*`. An app declares none (ignored, with a problem line); the old `accounts` / `secrets` keys are ignored (reported). | | `capture` | `["screen" \| "system-audio" \| "mic" \| "screenshot", …]` | What the app's panes may capture with `cowork.capture` (any subset; an unknown entry drops the key with a problem line). A recording with video needs `screen`, its sound `system-audio` and/or `mic`; `capture.shot` needs `screenshot`. Declaring is not consent: every capture still asks the person in Cowork's dialog. Manage Workspace › the app's page lists it. An app's only (a capability has no panes). | | `kind` | `app` (default) \| `capability` | A `capability` is a package of routines with no UI that apps use — and agents, through Cowork's MCP door, as granted — a Gmail capability is the `gmail-*` routines + its `SKILL.md`. It carries no `entry` or `fileRouting` and no `uses` of its own (reported if it does; `kind` never changes what the folder registers). Manage Workspace lists it under Components › Capabilities instead of Apps. Credentials belong to a capability: its routines reach its own connection (`credential`) and `.*` secrets; an app's reach none (`NotGranted`). A package that declares neither `kind` nor `uses` predates capabilities and keeps its old access until it declares one. | | `uses` | `{ "": "" }` | The capabilities an app needs, each with the lowest version it works with; met by a switched-on package of that name at the same major and that version or above. ≤ 8, never the app's own name. A template install of an app brings the capabilities it `uses` along from the same team server; a missing, off or too-old one is listed in Components › Capabilities, and a routine call it would have answered fails with an error naming it. Declaring `uses` puts an app under the capability model (as `"kind": "app"` does): its widgets and routines reach another package's routines only through a capability named here (`Forbidden` otherwise, naming the line to add). | | `requires` | `[]` \| `["web"]` \| `["desktop"]` | Where it can run. `["desktop"]` = the Cowork Studio desktop app only (e.g. it uses `ctx.browser`). Empty = anywhere. | | `tags` | string\[] ≤ 20, each ≤ 40 chars | Store search/filters. | | `banner` | string | The store listing's wide picture: an image file in the app folder (`.png`/`.svg`/`.webp`/`.jpg`, ≤ 1 MB, not a symlink). `cowork push` uploads it when it changed. | | `statusBar` | `{ "routine": id, "args"?: {…}, "interval"?: s }` | One item in the footer (status bar), drawn from what `routine` — one of THIS app's routines — returns. `interval`: seconds between refreshes while the window has focus, 15–3600, default 60. See "Footer item" below. | | `screenshots` | string\[] ≤ 5 | The store listing's screenshots, in order — image files in the app folder (≤ 1 MB each). `cowork push` uploads the changed ones and removes any the listing holds beyond them; leave the key out to manage screenshots in the Team Servers store instead. | Validation: an invalid `name` or `version` makes the folder "not an app" (shown as broken, never registered). Any other bad key is dropped with an error and the rest of the app keeps working. Unknown keys are ignored. ## Display modes [Section titled “Display modes”](#display-modes) * **`sidebar`** — `entry` renders in a side panel under the app's icon. Optional `open` puts something in the editor area too (once per session, so the user isn't yanked around). * **`tab`** — clicking the icon opens `entry` in an editor tab (no side panel), every click. * **`modal`** — clicking opens `entry` in a resizable in-workbench dialog. ## Web apps [Section titled “Web apps”](#web-apps) A web app is a prebuilt static site — an `index.html` with its scripts, styles, `.wasm` and fonts, e.g. a Rust/WASM editor's release build — that Cowork serves **unmodified** on its own origin and draws in an editor tab of the normal editor groups (no URL bar, no browser history): ```json { "name": "wordcraft", "version": "0.10.0", "title": "WordCraft", "icon": "icon.png", "web": { "index": "web/index.html" }, "fileRouting": { "**/*.docx": "web/index.html" } } ``` * `web.index` is the page, inside the folder; the folder that holds it is what gets served (dotfiles and anything outside it never are). Unzip the site's release into `web/` as it ships. * Or keep the build out of the app: `"download": {"url": "https://…/site.zip", "sha256": ""}` fetches the pinned release on the first open (checked against the sha256 before it is unpacked, cached per version). `index` is then relative to the BUILD — a zip whose files sit in one top folder has it stripped — so it is usually `"index.html"`, and routes name `"index.html"` too. A build copied into the folder as `web/` still wins. * `display` is always `tab`; there is no `entry`, no widget and no `window.cowork` SDK — the page is the app. Routines and `SKILL.md` work as in any app. * A routed file (`**/*.docx` above) opens in the web app's tab. Open the app directly with `cowork app open .cowork/apps//web/index.html`. * The app gets the routed file the web's standard ways — its open picker, a file ``, `launchQueue`; saving under the file's own name writes it back, any other name asks where. An app that opens files only by a drop sets `"open": "drop:"` (e.g. `"drop:#canvas"`): the file is dropped on that element. * It opens in the Cowork Studio desktop app and in Cowork in a browser tab or the PWA alike: the daemon serves it on its own `http://-.app.localhost:/` origin. Like a JSX app it gets an activity-bar icon. In a browser tab, closing it with unsaved changes does not ask yet. ## Site apps [Section titled “Site apps”](#site-apps) A site app is a **web site installed as an app**: `web.whatsapp.com`, a dashboard, any https page. It draws in an editor tab (no address bar) on your signed-in browser accounts, so the site is already logged in, and its notifications and unread count become the app's. It is a normal workspace app, a folder `.cowork/apps//` with a `cowork.json`, so it can be pushed like any other: ```json { "name": "whatsapp", "version": "1.0.0", "title": "WhatsApp", "icon": "icon.png", "web": { "url": "https://web.whatsapp.com/", "scope": "https://web.whatsapp.com/" } } ``` * `web.url` is an https page (http only on localhost) with no credentials in it. `scope` is an https prefix on the same origin; it defaults to `url` up to its last `/`. A manifest has `web.index` (a web app) or `web.url` (a site app), not both, and `fileRouting` cannot point at a site app. * **Install** from the in-IDE browser: the address bar shows an install icon when the page has a Web App Manifest, and the menu offers "Install \…" (or "Install page as app…" for any https page). Or run `cowork app install `: the name, icon and color come from the site's own web app manifest (the page's title and icon when it has none). `cowork app remove ` deletes the files the install wrote; if cowork.json no longer points at the installed url, or a file changed, it keeps it and says so. Open one with `cowork app open .cowork/apps//cowork.json`. * **In the right side bar**: `"display": "right"` puts the site in a tab of its own in the right side bar, beside Chat and Claude Code — ChatGPT, Claude, Gemini next to your files. `"display": "sidebar"` puts it in a pane of the left side bar, opened from its activity-bar icon; with neither it opens in an editor tab. `cowork app install https://chatgpt.com/ --title ChatGPT --display right` installs it there (chatgpt.com and claude.ai turn scripts away, so the CLI installs them from the address with no icon — `--title` names them); for an installed one, set the key in its cowork.json. The page keeps running while its pane is hidden (another tab of the side bar, the side bar closed), so a long reply keeps streaming. In the right side bar any title works (ChatGPT, whose name starts with Chat, gets no rail icon as a tab app). A JSX app can be `"display": "right"` too: its sidebar UI then lives in the right side bar (`open` is a sidebar app's only — it runs from a rail click). ```json { "name": "chatgpt", "version": "1.0.0", "title": "ChatGPT", "display": "right", "web": { "url": "https://chatgpt.com/" } } ``` * **Notifications** the site raises reach Cowork like an app's: a banner (muted per app), the bell, routines started with `cowork deploy add --notifications `, and `cowork.onNotification`. The site's unread count (`navigator.setAppBadge`) shows on the dock or taskbar icon. An OS banner click goes to the chat or page; the bell's Open shows the app but does not click for you. * Agents can look at and use a site app's tab (`apps list|screenshot|click|type|press`) with the person's consent, which names the site. The WhatsApp template tells agents never to read or send chats. **Limits** * Desktop app only — sites refuse to be shown inside another page. In Cowork in Chrome, its installed web app or on a phone, opening a site app shows "\ is part of the desktop app" (Get the Desktop App / Not Now) and the tab closes; a site app in a side bar says so in its pane. * In the right side bar the app's tab shows its title, not its icon or color. Dragging the tab to another side bar is remembered for you and wins over `display`. * A site notifies only while its tab is open: closing the tab stops it (a window hidden to the tray keeps it running), and there is no Web Push. Only the active workspace's site apps run; switching workspaces closes the others. At most 6 banners per app per minute. * A notification a site's own background worker raises is shown by the system without the app's name or mute. The page's own notifications are the ones Cowork attributes. * The unread count is on the dock or taskbar, not on the app's rail icon, and a site that does not set a badge gets the `(N)` from its page title. The rail icon itself arrives with the next engine release; until then open a site app right after installing it, from Manage Workspace, with `cowork app open` or from its notifications. * No address bar on a page outside `scope`. Switching accounts or a crash reload loses the page's state. * Ignored from the site's manifest: shortcuts, file and protocol handlers, share target, window-controls overlay. A site that checks the browser's user agent may need a fix from Cowork's side. * Install refuses `localhost` and private-network addresses when the start page is public. ## Examples [Section titled “Examples”](#examples) A mail client — sidebar list + a routine-materialized editor view + a routed file type: ```json { "name": "gmail", "version": "1.0.0", "title": "Gmail", "description": "Read and triage your inbox without leaving Cowork.", "icon": "mail", "color": "#EA4335", "entry": "gmail-sidemenu.jsx", "display": "sidebar", "open": { "routine": "gmail-open-inbox", "args": { "folder": "INBOX" } }, "routines": { "gmail-open-inbox": "open-inbox.ts", "gmail-fetch": "fetch.ts", "gmail-send": "send.ts" }, "fileRouting": { "**/*.maillist.json": "mail-list.jsx" }, "tags": ["email", "google"] } ``` A dashboard in a tab: ```json { "name": "sales-dashboard", "version": "0.1.0", "title": "Sales", "icon": "graph", "color": "#0F9D58", "entry": "dashboard.jsx", "display": "tab", "routines": { "sales-load": "load.ts" } } ``` A quick-capture dialog: ```json { "name": "quick-note", "version": "0.1.0", "icon": "note", "entry": "quick-note-modal.jsx", "display": "modal" } ``` A capability — a key to connect, and routines marked as tools for agents: ```json { "name": "invoices", "version": "1.0.0", "title": "Invoices", "kind": "capability", "credential": { "key": { "fields": ["token"], "url": "https://billing.example.com/settings/api", "hint": "Settings › API › New token" } }, "routines": { "invoices-list": { "file": "list.ts", "readOnly": true, "description": "Lists open invoices" }, "invoices-approve": { "file": "approve.ts", "reachesOthers": true, "description": "Approves an invoice and emails the supplier", "input": { "type": "object", "properties": { "id": { "type": "string", "description": "The invoice id" } }, "required": ["id"] } } } } ``` A headless app (routines + a viewer for a file type, no icon): ```json { "name": "csv-tools", "version": "1.2.0", "routines": { "csv-summarize": "summarize.ts" }, "fileRouting": { "**/*.csv": "csv-viewer.jsx" } } ``` ## Footer item [Section titled “Footer item”](#footer-item) `statusBar` puts one item in the footer, left, after `Workspace: <name>`. Its routine returns what to show — or `null` to hide it: ```ts export default async (args, ctx) => args.action === "disconnect" ? (await disconnect(), null) // a click's work; the item refreshes after : (await isConnected()) ? { text: "jotas@34.135.78.28", icon: "remote", tooltip: "Connected over SSH", click: { args: { action: "disconnect" }, confirm: "Disconnect from the VM?" }, data: { connected: true } } : { text: "Connect to VM", icon: "plug", click: { args: { action: "connect" } }, data: { connected: false } }; ``` * `text` (required), `icon` (a codicon; `sync~spin` spins), `tooltip`, `tone`: `"warning"` | `"error"` (colors the item). * `click: { args, confirm? }` — clicking runs the SAME routine with `args` (after a modal when `confirm` is set) while the item spins; a thrown error shows as a notification. `menu: [{ label, args, confirm? }]` instead lets the user pick one action. Either way the item refreshes after. * `data` — free-form (≤ 16 KB) for the app's widgets: `cowork.statusBar.get("<name>").data`. * It refreshes when it appears, every `interval` seconds while the window has focus, when focus returns, after a click, and on `cowork.statusBar.refresh(name)`. A failing refresh shows the app's title as a warning (the error in its tooltip; a click retries). * Each refresh is a routine run (audited, tagged `status-bar`): keep the routine cheap — a local check, not a network round trip — and the interval as long as you can live with. ## Folder layout [Section titled “Folder layout”](#folder-layout) ```plaintext .cowork/apps/gmail/ cowork.json gmail-sidemenu.jsx ← entry (naming: *-sidemenu.jsx / *-modal.jsx is convention only) mail-list.jsx ← fileRouting target open-inbox.ts fetch.ts send.ts ← routines README.md ← optional; travels with the app SKILL.md ← optional; how an agent uses the app (linked as .claude/skills/<name>) ``` `SKILL.md` tells an agent how to USE the app: * its files and data; * setup and environment (test vs production); * the routines to run; * the rules that bite. Its front matter is `name` (the app's `name`) and a one-line `description` ending in "Use when …". Cowork links every enabled app that has one as the project skill `.claude/skills/<name>`, pointing at the app folder, so Claude Code knows the app from the start of a session. Keep it short: the README stays the full reference. Everything in the folder travels when the app is pushed/installed. Keep generated data (caches, exports, `.db` files that hold user data) OUT of the app folder — write them elsewhere in the workspace (`ctx.data` writes to `.cowork/data/<app>/`; `cowork table create <name>` makes `.cowork/data/<name>.db`), so an update never collides with user data and an edited app stays "clean". ## Collisions [Section titled “Collisions”](#collisions) * Routine ids are ONE namespace per workspace: first app folder wins. A fileRouting glob two apps claim (Docs and WordCraft both `**/*.docx`) asks the user once which app opens it and remembers the answer in `.cowork/workspace.json` `openWith`; until then the first switched-on app is the route. **Prefix routine ids with the app name** (`gmail-fetch`, not `fetch`). ## `.cowork/workspace.json` — workspace STATE, not registrations [Section titled “.cowork/workspace.json — workspace STATE, not registrations”](#coworkworkspacejson--workspace-state-not-registrations) The file holds state only (it never travels with an app or a published workspace); the keys: ```json { "publishTo": "<team-server slug>", "startup": { "home": "gmail" }, "disabled": { "apps": [".cowork/apps/gmail-v1-2-3"], "routines": ["gmail-send"] }, "rail": { "folders": [{ "id": "google", "title": "Google", "color": "#0090ff", "members": ["workbench.view.cowork.app.gmail", "workbench.view.cowork.app.calendar"] }] }, "deployments": { "gmail-sync": { "target": "cloudflare-worker", "mode": "local", "schedule": "0 * * * *" } } } ``` * `disabled.apps` takes an app FOLDER (switches off its icon, routes and routines — including files a later update adds) or a single file; `disabled.routines` takes routine ids (listed but refuse to run). Off-only switches; safe to commit. * `rail.folders` groups activity-bar icons into folders (members are view-container ids; an app's is `workbench.view.cowork.app.<name>`). Users make them by dragging one icon onto another. A folder holds icons of one rail group — app icons and the system icons never share one. * `publishTo` is the default team server for `cowork push` — set it with `cowork push --to <ts> --remember` (or "Use as this workspace's default" when publishing from Manage Workspace). * `startup.home` names the app the window opens on (default: the app named `home`); `startup.left` / `right` (booleans) open or close the side bars at start. * Nothing else registers anything: containers, file routes, routines and open modes come from the app's `cowork.json`, so the app is self-contained and updates re-register it automatically. # Permissions — agents and grants > An agent (Claude Code, Codex, any MCP client) reaches a workspace's capabilities through one door on the Cowork daemon, signed in with its own login. An **agent** (Claude Code, Codex, any MCP client) reaches a workspace's **capabilities** through one door on the Cowork daemon, signed in with its own login. What it may run is **granted**, one routine at a time, by the person who owns the workspace. Everything else — the app's panes, a terminal's `cowork routine run`, schedules, webhooks, `ctx.routines.run` — is the owner, and grants never gate it. ## What a capability author marks [Section titled “What a capability author marks”](#what-a-capability-author-marks) A capability's routines ARE its tools: each entry in `routines` says what it does and what it changes, as an MCP tool says it. ```json { "name": "clickup", "version": "1.1.0", "kind": "capability", "credential": { "key": { "fields": ["token"], "url": "https://app.clickup.com/settings/apps", "hint": "Settings › Apps › API Token" } }, "routines": { "clickup-tasks": { "file": "tasks.ts", "readOnly": true, "description": "Lists the tasks of a list", "input": { "type": "object", "properties": { "list": { "type": "string", "description": "The list id" } }, "required": ["list"] } }, "clickup-comment": { "file": "comment.ts", "reachesOthers": true, "description": "Comments on a task (its watchers are notified)" }, "clickup-delete": { "file": "delete.ts", "destructive": true, "description": "Deletes a task" } } } ``` * `readOnly: true` — it changes nothing: no write at the service, no money spent, no user or project code run, nothing written in the workspace but its own cache (no view state, no export, no file a person could have edited). Granted to every agent by default, and an agent's client may run it without asking — so when in doubt it is not a read. A read cannot also be `destructive` or `reachesOthers` (then `readOnly` is ignored, with a line). * `destructive: true` — what it changes cannot be undone. `reachesOthers: true` — it reaches people beyond the account (sends, posts, pays). * `description` (≤ 400 characters) and `input` (a JSON Schema `{"type": "object", …}` of its arguments, ≤ 8 KB) are what the agent reads before calling it — write them for an agent. * A bare `"id": "file.ts"` is a write tool with no description: off for agents until granted. * Risk is derived, never stored: a read is low; destructive or reaching others is high; the rest medium. Manage Workspace shows it beside each routine. * Only `"kind": "capability"` packages are offered to agents — an app's own routines and loose routines are not. The capability's `SKILL.md` is the guide an agent gets with its tools. ## How the owner grants [Section titled “How the owner grants”](#how-the-owner-grants) Per workspace: a **default** for each `<capability>/<routine>`, and an **override** per agent. The rule: the agent's own setting, else the workspace default, else on only for a read. A routine whose connection is not connected (its `credential`) is unavailable whatever is granted. * **Manage Workspace › Permissions › Agent access** — Add agent (the token is shown once, with the lines that connect a client), and per agent: each capability's routines with Default / Allow / Deny. **Capability defaults** — the workspace default, one switch per routine. * **The CLI** (a headless server has no page): ```bash cowork agent create claude # prints the token ONCE + the connect lines cowork agent list # name, id, last seen, revoked cowork agent grants [--agent claude] # every routine: the default and each agent's answer cowork agent grant clickup/clickup-comment allow --agent claude cowork agent grant clickup/* deny # the default for every agent without its own setting cowork agent grant clickup/clickup-comment default --agent claude # back to the rule cowork agent token claude # a new token; the old one stops working cowork agent revoke claude --yes ``` Connect the agent from the workspace folder (Claude Code's local scope: private to that folder, never in git — the URL names the workspace): ```bash claude mcp add --transport http cowork 'http://127.0.0.1:47913/api/mcp?ws=%2Fpath%2Fto%2Fws' --header "Authorization: Bearer cwa_…" ``` (Codex: the `[mcp_servers.cowork]` block `cowork agent create` prints, in `~/.codex/config.toml`.) The agent then starts a task, lists the capabilities it may use, and calls `read` (a granted read) or `run` (anything else granted); a call that is not granted says so and names who to ask. ## What gets recorded [Section titled “What gets recorded”](#what-gets-recorded) Every agent call is a routine run like any other — the same off switch, the same audit record — naming the agent and its task: Manage Workspace › Observability › Executions shows **Who** (you, or the agent) and the task; `cowork routine runs` lists them. A call that is not granted is recorded `denied` and never starts. ## Who is this run for [Section titled “Who is this run for”](#who-is-this-run-for) `ctx.identity` (routines) is `{ user: {uid, email} | null, organization: {id, slug, name} | null }` — who is signed into Cowork, frozen at the start of the run; use it to stamp who did what. `cowork.identity.currentUser()` is the same user in a widget. `cowork status` and `cowork org` show the organization. ## Known limit [Section titled “Known limit”](#known-limit) Grants are policy on the owner's own machine: a process holding the daemon token is the owner and can make agents or grants. They keep agents to what you allowed; they are not a wall against the machine's owner. # Publish, install, update, pin, remove — and the audit log > Apps are shared through a team server: members + a store. Apps are shared through a **team server**: members + a store. Publishing and installing need a signed-in Cowork account (sign in inside Cowork; `cowork status` shows the account). Pushing needs an admin role on that team server. ```bash cowork team-server list # your team servers (slug, your role) cowork team-server store <team-server> # what it publishes ``` ## Push (publish a version) [Section titled “Push (publish a version)”](#push-publish-a-version) ```bash cowork push # the current folder: an app folder, or a workspace (every .cowork/apps/<name>/) cowork push .cowork/apps/gmail # one app cowork push .cowork/apps/gmail --to acme # an explicit team server cowork push --to acme --remember # …and make it this workspace's default (workspace.json#publishTo) cowork push --dry-run # what would be published / bumped; nothing uploaded or written cowork push --json # machine-readable rows ``` Each app prints one row: `published vX`, `bumped vA → vB`, `up to date vX`, or `failed: …` (exit 1; the other apps still push). * **Target**: `--to <team-server>` > the store the folder came from (its install receipt) > `.cowork/workspace.json#publishTo` > an error listing your team servers. `--to <ts> --remember` stores that team server as `publishTo` (machine-local, never packaged) once it resolves — even if the push itself then fails; an unknown team server is never stored and `--dry-run` writes nothing. * **Version** comes from `cowork.json`. Unchanged files → "up to date" (nothing published). Changed files but that version already published → the patch is bumped (`1.2.3` → `1.2.4`) and written back to `cowork.json`. Store newer than your copy → refused: update first. Versions only move forward — never lower `version` by hand. * After a push the folder is tracked like an install, so a collaborator's later push reaches your copy automatically while you haven't edited it. * The team server keeps the newest 10 versions of each app. An admin can withdraw a bad older one (`cowork app unpublish <ts>/<app>@<ver>`); the latest can't be withdrawn — push a fixed version first. Copies already installed (even pinned to it) keep working. ## Install and manage (in a workspace) [Section titled “Install and manage (in a workspace)”](#install-and-manage-in-a-workspace) ```bash cowork app list [--json] # every app here: version, local | <ts>@<ver> (pinned or auto), on/off, newer version out cowork app install acme/gmail # the store's latest → .cowork/apps/gmail/, follows the store cowork app install acme/gmail@1.2.0 # a version the store still keeps, PINNED (auto-update leaves it) cowork app versions acme/gmail # the versions the store keeps (yours marked) cowork app update # every clean, unpinned update that verifies cowork app update gmail # take the latest now (unpins) cowork app update gmail --keep-both # an EDITED app needs one choice (below) cowork app disable gmail # switch the whole app folder off (workspace.json#disabled); files kept cowork app enable gmail cowork app remove gmail # uninstall: files still exactly as installed are deleted, edited ones kept cowork app unpublish acme/gmail --yes # delete it from the store (admins) cowork app unpublish acme/gmail@1.2.0 --yes # withdraw ONE older version (admins; never the latest) # `app versions` then notes a copy pinned here to a withdrawn version cowork app open .cowork/apps/gmail/mail-list.jsx --as tab # open a widget in the running workbench (tab|dialog|sidemenu) ``` Every verb takes `--workspace <dir>` (default: the current folder). `<app>` is the folder (`.cowork/apps/gmail`), its basename, or the manifest `name` (the folder that owns it). An updated app you edited needs one of: `--replace` (the store wins; files you ADDED are never touched), `--keep-both` (yours stays exactly as it is; the new version lands WHOLE in `.cowork/apps/<app>-v<ver>/` (`gmail-v1-2-0`), switched off — compare the two, then delete the one you don't want), `--ai-merge` (Claude merges; needs the `claude` CLI; if it can't, it keeps both) or `--skip` (dismiss that version). Keep both and skip dismiss the version: the next one asks again, and `--replace` can still take it later. Two folders with one `name` (Keep both's copy beside yours): the name belongs to the first folder that is switched ON, and the other is listed but registers nothing. To try the new version, switch yours off and the copy on (`cowork app disable .cowork/apps/gmail` + `cowork app enable .cowork/apps/gmail-v1-2-0`) — its routines, rail icon and skill take over; both share `.cowork/data/<name>/`. `remove` only handles installed apps — a local app (one you made) is yours: delete its folder. `install` / `update` refuse unverified content unless you pass `--allow-unverified` — don't, unless the user explicitly asks. The same actions exist in the IDE (Manage Workspace and the Team Servers store page). ## Auto-update [Section titled “Auto-update”](#auto-update) Cowork checks installed apps every few minutes (and at start, after sign-in, after a push). A newer store version is applied automatically only when the installed app is **clean** (every file still matches what was installed), **verified**, and **not pinned**. An edited app is never overwritten — the Manage Workspace icon counts it and a toast names it; you choose in Manage Workspace or on the store page. Kill switch: `COWORK_NO_AUTO_UPDATE=1`. Consequences for authors and agents: * Editing an installed app's files turns its auto-update off (until you `cowork app update \<app> --replace|--keep-both|--ai-merge|--skip`). To change someone else's app, prefer pushing a new version from the source. * Keep user data outside the app folder, or the app is "edited" forever. ## The audit log [Section titled “The audit log”](#the-audit-log) Every routine run (from a widget, the CLI or an agent, a webhook, a schedule) and every app lifecycle action (push / install / update / remove) appends one JSON line to ```plaintext <Cowork root>/workspaces/<workspace path, non-alphanumerics → "-">-<8-hex hash>/audit/<YYYY-MM-DD>.jsonl ``` (`<Cowork root>` is `~/.cowork-studio` on macOS/Linux and `%LOCALAPPDATA%\Cowork` on Windows; e.g. `~/.cowork-studio/workspaces/-home-ana-work-fa27a359/audit/2026-09-26.jsonl`, dates in UTC; the hash keeps paths like `/a/b-c` and `/a-b/c` apart — find the folder with `ls ~/.cowork-studio/workspaces/ | grep <name>`.) A line: ```json {"ts":"2026-09-26T14:03:11.412Z","capability":"gmail-fetch","input":{"folder":"INBOX"}, "status":"ok","durationMs":812, "context":{"executionId":"…","userId":"…","organizationId":"…","deviceId":"…", "workspace":"/home/ana/work","source":"cli","agent":"claude-code", "appId":"gmail","appVersion":"1.2.3","kind":"capability"}} ``` * `capability` is the routine id for a run (`gmail-fetch`) and `"cowork.app.push"` / `"cowork.app.install"` / `"cowork.app.update"` / `"cowork.app.remove"` for a lifecycle action (an automatic update is `"cowork.app.update"` with source `system`). * `source`: `app` (a widget), `cli` (the CLI — `agent` says if Claude Code / Codex ran it), `mcp` (an agent through Cowork's MCP door — `principal` names its login, `taskId` its task), `webhook`, `schedule`, `system`, `deploy`. * `status` is `ok`, `error`, or `denied` (refused before it started: an agent the routine is not granted to — see [agents and grants](/docs/guides/permissions/)). * `input` is the routine's arguments (capped at 16 KB). Values under credential-looking keys (`authorization`, `cookie`, hook/capability tokens, `*password`, `*secret`, `*api_key`, `*token` fields) are replaced by `"[redacted]"` — but never pass secrets as args anyway. * `context.caller` (when present) is what the caller CLAIMED (app / source / agent); `appId`/`appVersion`/`kind` (`app` | `capability`) are derived by Cowork from the routine file that actually ran; `context.caller.chain` lists the routines that called it. `cowork routine runs --app <name>` lists one package's runs; `show <run-id>` prints the routines that called it (`via daily → send-report`). * `agent` is filled when the run came from Claude Code or Codex. * Local only, files are private to the user (0600), deleted after 7 days. A run that failed or changed something also keeps a result preview (200 characters, credential-named keys redacted) and its last log lines (up to 2,000 characters); a read that called `ctx.unchanged()` keeps neither. Nothing else of the result or the log is recorded. * To answer "what ran here recently?", read the day files with `jq`, e.g. `jq -c 'select(.status!="ok")' ~/.cowork-studio/workspaces/<encoded>/audit/*.jsonl`. # Routines (.ts) — the privileged half of an app > A routine is a .ts (or .js / .mjs) file whose default export is called with (args, ctx). A routine is a `.ts` (or `.js` / `.mjs`) file whose default export is called with `(args, ctx)`. It runs as its own Bun process spawned by the Cowork daemon — full Node/Bun runtime (`fetch`, `node:fs`, `node:path`, `Bun.*`), started fresh for every run (so an edit applies on the next run). It is the ONLY place that can read secrets, touch the filesystem or call authenticated APIs. Widgets call it; you run it from the CLI; agents call a capability's routines as tools, as granted ([agents and grants](/docs/guides/permissions/)). ```ts // .cowork/apps/gcal/events.ts — in the `gcal` CAPABILITY, whose cowork.json is // { "name": "gcal", "version": "0.1.0", "kind": "capability", // "routines": { "gcal-events": { "file": "events.ts", "readOnly": true, "description": "Lists upcoming events" } }, // "credential": { "oauth": { "provider": "google", "scopes": ["https://www.googleapis.com/auth/calendar.readonly"] } } } export default async (args: { max?: number }, ctx: CoworkRoutineContext) => { ctx.unchanged(); // a read const res = await ctx.credential.fetch( // the shared Google sign-in: bearer, // refresh on 401, retries on 429 / 5xx `https://www.googleapis.com/calendar/v3/calendars/primary/events?maxResults=${args.max ?? 10}&orderBy=startTime&singleEvents=true&timeMin=${new Date().toISOString()}`, ); if (!res.ok) throw new Error(`Calendar ${res.status}`); // the caller's promise rejects with this const { items } = await res.json(); ctx.log("fetched", items.length, "events"); // → Output panel only return items.map((e: any) => ({ id: e.id, title: e.summary, start: e.start?.dateTime ?? e.start?.date })); }; // must be JSON-serializable ``` (Credentials belong to a capability: only its routines reach the connection its cowork.json `credential` declares — an app's routine gets `NotGranted`, and an app's own `credential` is ignored. The app declares `"uses": {"gcal": "0.1.0"}`, calls `cowork.routines.run("gcal-events")` and signs in once with `cowork.credentials.connect("google")` (or Manage Workspace › Components › Credentials) — until then the routine throws `NotConnected`, with a message that says so.) ## Rules [Section titled “Rules”](#rules) * `export default` a function (sync or async). The return value must be JSON-serializable (else the caller gets `null` + a warning). Throw to fail — the caller's promise rejects with the message. Throw `ctx.error(name, message)` to give the failure a kind the widget can branch on (`e.name === "NotConnected"`, `"RateLimited"` …); every rejection also carries `e.runId`. * Timeout: 30 s by default. A routine that needs longer declares it in `cowork.json` (`{"file": "x.ts", "timeout": 120}`, seconds, ≤ 300) — apps, `cowork routine run`, schedules and webhooks all honor it; a widget can still pass `{ timeoutMs }` per call. * **Each run leaves one record**, kept 7 days in the local audit log: status, duration, error, and — when it failed or changed something — a result preview (200 chars, credential-named keys redacted) and the last lines it logged (`console.log` / `ctx.log` also stream to the "Cowork" Output panel). A read that calls `ctx.unchanged()` records only that it ran. `cowork routine runs` pages through them. A run of an app installed from a team server is also reported to that server — never its input or result; a failed run's last lines, with credential-looking strings cut out. * **An execution is a run that changed something, or failed** — like an n8n execution. A run that only read (a list, a status, a poll that found nothing) calls `ctx.unchanged()`: it stays in the local record but is never reported and is left out of the run lists (`cowork routine runs --unchanged` shows it). A widget that polls a read routine then adds no noise. Call it per run, from the branch that knows: `if (action === "list") ctx.unchanged()`. * **Inputs ARE recorded** in the local audit log (7 days). Never pass a secret as an argument — read it inside the routine (`ctx.credential.fields()`, `ctx.secrets.get`). * Routine ids are one namespace per workspace — prefix with the app name. * A routine may import sibling files inside its own app folder (normal module resolution). Keep them inside the folder so they travel with the app. No duplicate top-level declarations (the runner refuses the file). * Read files relative to `ctx.workspaceRoot`; refuse paths that escape it (example below). * A disabled routine (`.cowork/workspace.json#disabled.routines`, or its app folder disabled) refuses to run. * In a capability, say what each routine changes (`readOnly`, `destructive`, `reachesOthers`, `description`, `input` — [manifest](/docs/guides/manifest/)): agents run it only as granted, reads on by default. A `readOnly` routine must really change nothing (it may cache what it read in its own `<name>.*` secrets or `ctx.data`). ## `ctx` [Section titled “ctx”](#ctx) | member | what | | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `workspaceRoot` | Absolute workspace path. | | `identity` | Who this run is for: `{ user: {uid, email} \| null, organization: {id, slug, name} \| null }`, frozen at the start of the run. Use it to act on WHO (stamp an approver). | | `credential.fetch(url, init?)` → `Response` | The capability's connection — the one its cowork.json `credential` declares. A sign-in sends its bearer; a 401 refreshes once and retries; only to the service's API hosts (`googleapis.com`, `graph.microsoft.com`), https — a pre-authenticated upload URL takes plain `fetch`. A key sends nothing by itself: put its header in `init.headers` (from `fields()`). A 429 / 5xx waits and retries `retries` times (0–4, default 2; `retries: 0` for a write that must never repeat); `json` sends a JSON body. Resolves with the Response whatever its status — name the failure yourself (`if (!res.ok) throw ctx.error(…)`). Throws `NotConnected` naming where the person connects it; a capability declaring no `credential` gets `NotConnected` saying so, an app's routine `NotGranted`. | | `credential.token({ force? })` / `credential.fields()` | A sign-in's access token, refreshed when needed (`force` after the service answered 401) — for an SDK that wants one; prefer `fetch` — or a key's first field. `fields()` → the key's fields by name (`{ token }`), or `NotConnected`. | | `credential.status()` / `credential.start()` / `credential.disconnect()` | `{ connected, account?: { email } }` — never a token; begin a sign-in yourself → `{ authUrl }`; forget the connection, revoked at the service when it can be — for every capability that shares it. Connecting is the person's: Manage Workspace › Components › Credentials, an app's pane (`cowork.credentials.connect`), or `cowork credential connect`. | | `secrets.get(name)` / `list()` / `set(name, value)` | The signed-in Cowork account's encrypted secret store, within the package's grants: a capability's routine reaches its own `<name>.*` (and reads `cowork.openrouter`), an app's routine nothing — anything else throws `NotGranted` naming the package and the name asked (a package that declares neither `kind` nor `uses` keeps its old, unrestricted access). For what a routine caches beside its connection (`clickup.user_id`): `get` → `undefined` if unset; `list` → the names it may reach; `set` stores one. Names `^[A-Za-z_][A-Za-z0-9_.-]*$`; the `cowork.` prefix is reserved — of it a routine reads only `cowork.openrouter`, a preprovisioned OpenRouter key (OpenAI-compatible, `https://openrouter.ai/api/v1`) present when signed in. | | `log(...parts)` | A line in the Output panel (and, for a run that changed something, in its audit record). | | `error(name, message)` | An `Error` with that name, to throw: `throw ctx.error("NotFound", "…")`. The widget's promise rejects with `e.name === "NotFound"` — branch on it, never on the message. Your error carries `e.runId`; Cowork's own `NotFound` (no such routine) comes before any run and has none. | | `data` | The app's own data folder, `.cowork/data/<app name>/` (the app is the `.cowork/apps/<dir>` folder whose cowork.json names it; a `.gitignore` keeps it out of git): `read(name, fallback)` (JSON; `fallback` when missing or broken), `write(name, value)` (atomic, creates subfolders) → the workspace-relative path, `list(subfolder?)`, `remove(name)` → bool, `path(name)`, `dir`, `fileName(title, fallback)`. Names stay inside the folder (`..` and absolute paths throw). Keep user data here, never in the app folder. | | `unchanged()` | This run changed nothing (a read, an empty poll): not an execution — kept locally, never reported, hidden from the run lists. Ignored if the run then throws. | | `db(path)` → `{ query, exec, transaction }` | A workspace SQLite `.db` (WAL). `query` returns rows, `exec` changes them (`{ changes, lastInsertRowid }`) — `query` does not refuse a write, so keep writes in `exec`; `transaction(fn)` must be SYNCHRONOUS. | | `queue(name)` → `{ send, receive, ack, nack, approve, reject, reply, retry, peek, list, counts }` | A task queue in the workspace store (`.cowork/local/queues.db`), synchronous — the same tasks as `cowork queue …` and the Tasks view. A task has a `subject`, a `status` (`todo` · `in_progress` · `needs_review` · `done` · `failed`), `from`, `to` (an agent's name, `owner`, or `anyone`), `review`, the request (`body`, `files`) and a thread of `entries`. `send(body, { subject?, to?, as?, review?, ref?, meta?, delayS?, files? })` (files `{ path }` copied, or `{ name, text \| base64 }`; a repeated `ref` answers the first task) creates the queue; `receive({ as?, mine?, leaseS? })` → the oldest task for `as` (or for anyone) with a `receipt`, or `null`; finish with `ack(id, receipt, { result?, files?, review?, to? })` (the result and files — a `{ path }` only inside the workspace — become your entry; `review` asks the owner, `to` hands it on) or give it back with `nack(id, receipt, { delayS?, error? })`; `approve(id)`, `reject(id, comment)` (Send back), `reply(id, body, { to?, as?, files? })`, `retry(id)`; `list({ view?, agent?, limit?, before? })`; `counts()` per view. Use it to hand work to an agent, never to start one. | | `journal.add({ skill, kind?, text, source? })` | Leave a learning / error / note for a skill (`source` defaults to `routine:<file>`); an agent later folds open entries into the skill (`cowork journal entries <skill> --open`). | | `routines.run(id, args?, { timeoutMs? })` | Run another routine of this workspace and await its result — see [Calling another routine](#calling-another-routine). | | `notify({ title, body?, tag?, urgent? })` | An OS notification from this routine's app — what `cowork.notify` does in a widget (same fields, cuts and click), and the way to notify with **no pane open**: the routine that polls keeps what it saw last in `ctx.data`, and on something new calls `await ctx.notify({ title: "New email", body: from, tag: "inbox" })`. **The desktop app notifies reliably** — even with its window closed (tray); **a browser tab is best effort** — only while a tab on this workspace is open, and a backgrounded tab is throttled to about one check a minute. Only the **active** workspace's notifications show. One nobody showed within a minute is dropped (the next poll repeats it); muted or past 6 a minute → dropped, still resolves. A **capability's** notifications go to the one app that uses it: a click opens that app, and either one's Notifications switch (the app's or the capability's, in Manage Workspace) mutes them. A routine outside a `.cowork/apps/<name>/` package gets `BadRequest`. Absent in deployed routines (see below). | | `badge(count)` | This app's count on the OS badge — as `cowork.badge`: the latest call wins, `0` clears; report it on every poll (`await ctx.badge(unread)`). The desktop app's dock / taskbar shows the **active** workspace's count (a browser tab: a `(N) `title prefix); another workspace's waits until it is active. | | `auth.getState()` / `auth.getIdToken()` | Who is signed into Cowork → `{ authenticated, user: { uid, email, displayName } \| null }`; a short-lived Firebase ID token (or null). | | `command.run(cmd, { title, description, value, timeoutMs })` | Run a shell command after a user consent modal (login shell PATH, in the workspace folder) → `{ code, stdout, stderr }`. `value` = a stable consent label. | | `consent.request({ value, title, description, command })` | Consent only → `{ granted, ticket? }` (hand the ticket to a widget's `stream.open`). | | `browser.*` | Drive the Cowork Studio desktop app's embedded browser: `open`, `navigate`, `snapshot`, `find`, `click`, `fill`, `type`, `press`, `waitFor`, `read`, `screenshot`, `tabs`, `evaluate(js, { tabIndex })` (runs in the page — uses its logged-in session). `open(url)` opens a new tab; `open()` with no URL shows the browser (its last-active tab). `toggle()` is deprecated — it does what `open()` does; use `open()`. Desktop only → set `"requires": ["desktop"]`. | ### Calling another routine [Section titled “Calling another routine”](#calling-another-routine) `await ctx.routines.run("gmail-send", { to, subject, body })` runs `gmail-send` like any other caller would — the same off switch, a concurrency slot, its own run record — and resolves with what it returns. An id your app declares is your app's own routine; a capability's routines answer by their ids while it is installed and on (name it in `uses` so installing your app brings it). Calls nest two deep (a → b → c; c calling another throws `RoutineDepth`), a routine already in the chain throws `RoutineCycle`, and the nested run never outlives yours (`timeoutMs` is capped by what is left). A routine that threw rejects with its own `e.name` (`NotConnected` …) and `e.runId`; a refusal rejects as `NotFound`, `RoutineDisabled` or `TooManyRoutines` — at once, never waited out (a `NotFound` refusal has no `e.runId`: nothing ran). The called run's audit record names the chain (`caller.chain`). A routine reached this way is **on demand**, not active: only an armed schedule or webhook makes one active. Write the id as a literal (`ctx.routines.run("gmail-send", …)`) where the step runs it: Manage › Workflows draws each one as a chip under its step, on the owning capability's tile (a click opens that routine's board). Only calls written inside a step's `run` (a `defineRoutine` flow) or anywhere in a plain routine's file are drawn — not a helper elsewhere in a flow's file, not one in another file, not a computed id (`ctx.routines.run(id)`) and not a call through a destructured or renamed ctx (`const { routines } = ctx`). A skill's step that runs `cowork routine run <id>` draws the same chip, and makes the skill an agent workflow — when it lives outside every package (`.claude/skills/…`): an app's or a capability's own SKILL.md says how to use the package, and is never listed as one. **Deployed routines** (`cowork deploy`, a container or a Cloudflare Worker) get a smaller `ctx`: `workspaceRoot`, `secrets.get/list` (from env), `log`, `unchanged` (does nothing), a signed-out `auth`, an anonymous `identity` (`user: null`) — and `db`: SQLite under the container's `/data` volume, or Cloudflare D1 on a Worker deployed with `--db <name>` (`cowork deploy run` creates that database when your Cloudflare account has none by that name — no `wrangler d1 create`; deployments may share one), where `query`/`exec` return Promises and `transaction()` throws. **`await` every `ctx.db` call** (harmless on the synchronous local runner) and one routine runs everywhere. Nothing else is there — no `credential`, `command`, `consent`, `browser`, `routines`, `notify` / `badge`, `data`, `queue`, `journal`, `error`, `secrets.set`; `cowork deploy build` names any the routine uses. ## Connecting [Section titled “Connecting”](#connecting) A capability declares what it connects with in its cowork.json `credential` — a sign-in (`"oauth": {"provider": "google", "scopes": […]}`), a key (`"key": {"fields": ["token"], "url", "hint"}`), or both. The person connects once, and every capability sharing the connection is connected: * **Manage Workspace › Components › Credentials** — Connect (a sign-in opens the service's consent; a key shows a form from `fields`, with `hint` and a link to `url`), Disconnect. * **From the app's pane** — on `NotConnected`, its Connect form calls `cowork.credentials.connect("acme", { token })` (a key) or `cowork.credentials.connect("google")` (a sign-in), then `refresh()`es its subscription. `cowork.credentials.get(name)` says whether it is connected — the key never reaches the pane again. * **From a terminal** (a headless server): `cowork credential list`, `cowork credential connect acme --field token=…` (a sign-in prints the address to open in a browser), `cowork credential disconnect acme`. The routine reads it — `ctx.credential.fields()` / `ctx.credential.fetch(url)` — and throws `NotConnected` naming where to connect while it is not: ```ts // acme-me.ts — in the `acme` capability ("credential": {"key": {"fields": ["token"]}}) export default async (_args: {}, ctx: CoworkRoutineContext) => { ctx.unchanged(); const { token } = ctx.credential.fields(); // NotConnected until the person connects it const res = await ctx.credential.fetch("https://api.acme.example/v1/me", { headers: { authorization: `Bearer ${token}` } }); if (!res.ok) throw ctx.error("BadKey", `Acme refused this key (${res.status}).`); return await res.json(); }; ``` **Other secrets** — your own, by name, for a routine that is not a capability's connection: ```bash cowork secrets set openai.key --stdin <<< "$KEY" # stdin keeps it out of shell history cowork secrets list # names only ``` Values are readable only by routines and `cowork secrets get` — never by widgets or daemon routes. ## Running [Section titled “Running”](#running) ```bash cowork routine run gmail-fetch --args '{"folder":"INBOX","limit":5}' # prints the JSON result cowork routine run gmail-fetch --workspace /path/to/ws # from outside the workspace cowork routine list cowork deploy add nightly-sync --schedule "0 6 * * *" # give it a trigger (below) cowork routine active nightly-sync on # run it on its own: arms its schedule or webhook cowork routine active nightly-sync off # stop it (the workspace stays armed for the others) ``` `cowork routine run` runs a routine **as the owner** — outside any agent's grants. An agent calls a capability's routines through Cowork's MCP door (`read` / `run` with `capability`, `tool`, `params`), as granted ([agents and grants](/docs/guides/permissions/)). **A trigger makes a routine run on its own**, and it is the workspace's, not the app's — there is no cowork.json key, and a pushed app carries none. `cowork deploy add <id> --schedule "*/5 * * * *"` (a 5-field cron) or `--webhook` writes it to `.cowork/workspace.json#deployments` (names only — commit it). `--mode local`, the default, means this computer's daemon fires the schedule (in local time); `--mode deployed` leaves the schedule to the copy `cowork deploy run` puts on a Worker or container (in UTC) — nothing fires it here. A webhook is answered here in either mode (`cowork deploy hooks` prints its URL). `--hook-token` makes it ask for a token: `deploy add` mints it and prints it, and a caller sends it as the `x-cowork-hook-token` header (missing or wrong → 403). A deployed copy can also accept `Authorization: Bearer` tokens (`cowork deploy run <id> --consumer-tokens-file <path>`). A trigger fires only while this workspace's triggers are armed — `cowork routine active \<id> on` arms them. **A notifications trigger** starts the routine when an app raises a notification: `cowork deploy add <id> --notifications <app>[,<app>|*]` (app names as `cowork app list` prints them; `*` = every app's). The routine receives the notification as its args — `{ app, appTitle, title, body?, tag?, urgent?, at }` — and it runs here only, in either mode. It needs the Active switch (`cowork routine active <id> on`, or Manage › Workflows), which is also the person's consent to hand those apps' titles and bodies to the routine; adding apps to the list later switches it off until the switch is used again. A muted app still triggers. A routine that a notification started cannot trigger another by its own `ctx.notify` (it reaches the person, starts nothing). Treat the body as untrusted text. `routine list` ends each row with its state: `active · <cron>` / `active · webhook` run on their own; `on demand` runs only when something calls it; `… · not armed` has a trigger that is declared but not armed; `off` is switched off. `routine active <id> on|off` is the Active switch (Manage › Workflows and the Monitor draw the same one): `on` needs a trigger (`cowork deploy add`, above) and arms that routine plus this workspace's local triggers; `off` stops that routine only (a webhook keeps its URL). Arming the workspace also fires ITS OTHER opted-in schedules and webhooks — the first switch in a workspace whose triggers were off turns them all on (`cowork deploy triggers off` stops them all). A switched-off routine (`cowork app disable`, the switch above its board) is refused until it is switched on, and so is one your organization does not let you run. An app does not switch routines on: the SDK gives an app's pane no `setActive` — arming triggers is the user's click in Manage › Workflows or the Monitor, or this command. (The daemon route is protected by the same capability token as the secret store, so this is a rule an app follows, not a wall: don't call `/api/deploy/active` from app code.) `--json` prints `{"ok":true,"id","active","state"}`. ### An agent on a queue [Section titled “An agent on a queue”](#an-agent-on-a-queue) Cowork starts no agent for a queue. An agent — a Claude Code session, a script — takes work by looping `cowork queue receive <queue> --as <name> --wait 600 --json` (exit 3 = nothing came), doing the task, then `cowork queue ack <queue> <id> --receipt <r> --result "<what it did>"` (`nack` gives it back). A widget or routine addresses it with `queue.send(q, { body, to: "<name>" })` (`owner` = you, `anyone`, the default, = the first taker); `cowork queue receivers` shows who took a task lately. ### Without the IDE (a server) [Section titled “Without the IDE (a server)”](#without-the-ide-a-server) The same routines run on a box with no IDE — a VM, a container — where an agent (Claude Code) with a shell runs them as the owner (grants gate only the MCP door): ```bash cowork start --headless # the runner, in the foreground (systemd / Docker) cowork login # once; paste the address the browser ends on cowork app install <team-server>/<app> --workspace /srv/work # then it follows the store cowork consent allow '<value>' --workspace /srv/work # each command a routine runs cowork routine run <id> --args '{…}' --workspace /srv/work cowork routine runs --workspace /srv/work # what ran, newest first (show <run-id> = its log) ``` Pull the executions where the code went: `docker exec <runner> cowork routine runs` (or over ssh). With a team server, every runner reports its apps' runs to it on its own: owners and admins read them on the team server's page (Executions) or with `cowork routine runs --team-server <ts>`. A routine that calls a Python (or any other) tool runs it with `ctx.command.run`, so the runner needs that tool: the Docker runner image adds Python with `--build-arg PYTHON=1`, and `docker exec <runner> pip install -r /work/requirements.txt` installs the packages once onto the `/data` volume (they survive a new image). No window can show the approval prompt there, so an unapproved `ctx.command.run` / `ctx.consent.request` is refused at once with the exact `cowork consent allow …` line to run. `browser.*` needs the desktop app. A routine used as a sidebar app's `open` returns `{ "path": "<workspace-relative file>" }`; Cowork opens that file (through fileRouting). This is how an app materializes content first (fetch → write `INBOX.maillist.json` → route it to the list widget). A routine that renders data for a routed file type reads the file itself — widgets only get `cowork.openedFile.path`: ```ts import { readFileSync } from "node:fs"; import { resolve, sep } from "node:path"; export default async ({ path }: { path: string }, ctx: CoworkRoutineContext) => { const abs = resolve(ctx.workspaceRoot, path); if (!abs.startsWith(ctx.workspaceRoot + sep)) throw new Error("path escapes the workspace"); return JSON.parse(readFileSync(abs, "utf8")); }; ``` # Widgets (.jsx / .tsx) and the window.cowork SDK > Types for everything below are scaffolded into .cowork/local/cowork.d.ts in a Cowork workspace (ambient — nothing to import). Types for everything below are scaffolded into `.cowork/local/cowork.d.ts` in a Cowork workspace (ambient — nothing to import). Read that file when you need an exact signature. ## The module rules [Section titled “The module rules”](#the-module-rules) * One file = one module. `export default` a React component (function). Transpiled by the daemon (Bun), evaluated in a webview with a `require` shim. * **No relative imports.** `import x from './helpers'` fails ("not in the cowork webapp allowlist"). Every widget is standalone: duplicate tiny helpers, and put shared or privileged logic in a ROUTINE (`cowork.routines.run`). * **Only these imports resolve** (anything else throws at load): * `react`, `react/jsx-runtime`, `react-dom`, `react-dom/client` * `@/components/ui/<name>` — the stock shadcn/ui set: accordion, alert, alert-dialog, aspect-ratio, avatar, badge, breadcrumb, button, calendar, card, carousel, chart, checkbox, collapsible, command, context-menu, dialog, drawer, dropdown-menu, hover-card, input, input-group, input-otp, label, menubar, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner, switch, table, tabs, textarea, toggle, toggle-group, tooltip * `@/lib/utils` (`cn`), `clsx`, `tailwind-merge`, `class-variance-authority` * `lucide-react` (icons), `recharts` (charts), `@xyflow/react` (node/edge canvases, CSS included), `@xterm/xterm` + `@xterm/addon-fit` + `@xterm/addon-web-links` (terminals, CSS included) * `radix-ui`, `react-day-picker`, `sonner`, `vaul`, `cmdk`, `react-resizable-panels`, `embla-carousel-react`, `next-themes`, `input-otp` * No npm installs, no CDN scripts, no `fetch` to third-party APIs with credentials — the webview has no secrets and no filesystem. Network + secrets + files → a routine. * Tailwind classes work (compiled at runtime). Copy-pasted shadcn block JSX works unchanged. ## Theming [Section titled “Theming”](#theming) * Use the shadcn semantic tokens — `bg-background`, `text-foreground`, `bg-card`, `text-muted-foreground`, `border`, `bg-primary text-primary-foreground`, `bg-accent` … They follow the workbench theme (light/dark) live with zero code. * `dark:` variants follow the WORKBENCH theme (not the OS). * Don't hardcode colors (`#fff`, `bg-white`, `text-black`). A hardcoded-light UI gets an automatic dark filter on dark themes, which looks worse than tokens. * Fill the pane: the root element is sized by the host; use `h-full` / flex layouts and let inner regions scroll. ## `window.cowork` (also the global `cowork`) [Section titled “window.cowork (also the global cowork)”](#windowcowork-also-the-global-cowork) | member | what | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `routines.run(id, args?, { timeoutMs? })` | Run a routine; resolves its return value, rejects with its error — `e.name` is the kind the routine threw (`ctx.error("NotFound", …)`, `NotConnected` from `ctx.credential`), `e.runId` the run (`cowork routine runs show <id>`). Cowork's own `NotFound` (no such routine, or its capability is not installed) comes before any run and has no `e.runId`; a `NotFound` the routine threw always has one. Identical calls in flight share one run; a busy daemon (`TooManyRoutines`) is waited out. Default timeout 30 s, max 300 s. | | `routines.subscribe(id, args, { interval?, timeoutMs?, onData?, onError? })` → `{ refresh(): Promise, close() }` | Keep a routine's answer fresh — **use this instead of `setInterval`.** Runs now, then every `interval` seconds (min 5, default 60) while a pane of this app is on screen. Every pane of the app asking the same routine with the same args shares ONE loop (the sidebar and its tab poll once); identical answers are not re-sent; a pane opening later gets the last answer at once. `NotConnected` pauses the loop until `refresh()` (call it after the user connects) or (inside the IDE) until a pane of the app is shown again; other errors back off. `refresh()` always answers — from a run that starts after the call, so it sees a change you just made — even when nothing changed, and resolves once the answer reached `onData`/`onError` — await it for a spinner. Subscribe at mount and keep it open; `close()` on unmount. | | `app.post(msg)` / `app.onMessage(cb)` | This app's own panes (sidebar, tabs, dialog) talking: `post` reaches every OTHER open pane of the same app in this workspace; `onMessage` returns the unsubscribe. E.g. a page posts `{ type: "changed", id }` after a save and the sidebar calls its subscription's `refresh()`. | | `credentials.get(name)` / `credentials.connect(name, fields?)` | A connection of the capabilities this app `uses` — declared in their cowork.json `credential` (an app declares none); a sign-in is named after its service (`google`, shared by every Google capability), a key after its capability. `get` → `{ name, kind: "oauth" \| "key" \| "both", state: "connected" \| "needs-scopes" \| "not-connected", account?: { email }, missingScopes, usedBy, fields?, url?, hint? }` — ready when `state === "connected"`; never a token or a key. `connect` signs in (opens the service's consent for what every capability using it needs; the sign-in finishes in the browser — then `refresh()`) or, with `fields` (`{ token }`), saves a key; one that takes either signs in unless `fields` are given. A sign-in rejects `NotHere` in a machine's (remote) window. | | `routines.runs(id?, {app?})` | Bounded run history (metadata only); `app` narrows to one package's runs. Each run names its package (`app`, `kind`) and, when nested, `chain` — the routines that called it. | | `state.get()` / `state.set(v)` | Per-view persisted state (survives hide/show + reload). Use it as a stale-while-revalidate cache: paint the cached value, refresh underneath. Keep it BOUNDED. | | `openFile(path)` | Open a workspace file (routes through fileRouting). | | `openExternal(url)` | Programmatic external open (trust prompt). Plain `<a href="https://…">` is already routed — no need to call it for links. | | `browser.open(url)` / `browser.status()` | The desktop app's in-IDE browser: `open` shows an http(s) page **beside the IDE**, in a browser tab of the desktop app's browser group (a tab already on that site is reused, else a new tab) → `{ embedded: true }`; without it (web, code-server) it falls back to `openExternal` → `{ embedded: false }`. `status()` → `{ open, tabs: [{ url, title, active }] }` or `null` — poll it (only while `visible`) to follow the user, e.g. pick an id out of the URL they reached. For setup guides: put the guide in a TAB (a routed file) — it sits in the files group beside the browser group where its pages open. | | `notify({ title, body?, tag?, urgent? })` | An OS notification from this app — a banner from the desktop app (even with its window closed), or from a browser tab on this workspace (best effort). Only the **active** workspace's notifications show. Rejects in a machine's (remote) window. `title` is required and shows exactly as sent — put the app's name in it if you want it there (≤ 100 characters; `body` ≤ 300, `tag` ≤ 64 — longer is cut). A `tag` replaces this app's not-yet-shown notification with the same tag; `urgent: true` keeps the banner up and asks for attention (dock bounce, flashing taskbar). A click opens the app. Muted by the user (Manage Workspace › the app's Notifications switch) or past 6 a minute → dropped, and it still resolves; rejects `BadRequest` without a title. To notify while no pane is open (new mail), call `ctx.notify` from the routine that polls — a routine outside a `.cowork/apps/<name>/` package gets `BadRequest`, and a capability's notifications go to the one app that uses it (the click opens that app; its switch or the capability's mutes them). Every notification also appears in the workbench's Notifications center (the bell) as `<App>: <title> — <body>`; the banner shows only while the window is not focused. | | `onNotification(cb)` | Subscribe to THIS app's notifications — its own `cowork.notify` and its routines' / capabilities' `ctx.notify`, as they are raised: `cb({ app, appTitle, title, body?, tag?, urgent?, at })`; returns an unsubscribe function. Live in every pane of the app; a muted app delivers none. An event, not a poll. `n.app` says which app it was posted as, not who posted it. Another app's notifications are for a routine ([notifications trigger](/docs/guides/routines/)). | | `capture.record({ video?, audio?, live?, file? })` | Records the screen (`video: "screen"`, the default, or `"window"`; `false` = sound only) with the sound the computer plays (`audio.system`) and/or the microphone (`audio.mic`). Declare what you use in cowork.json `"capture"`. Every recording first asks the person in **Cowork's dialog**, which names your app, what it asks for and what it gets; nothing starts without that Allow, and a recording shows a pill with **Stop** (the desktop app's own window, or a bar in a browser tab). Resolves once recording with a session `{ id, file, onAudio(cb), onStop(cb), stop() }`; the file lands at `file` (default `Recordings/<date-time>-<app>.webm`, inside the workspace) when it stops — Stop, `stop()`, the system ending the share, or the window closing (what was recorded is kept). `live: true` also streams the mix to `onAudio` as 16 kHz mono `Int16Array` frames of 20 ms — **the app hears what the person hears**; say so in your UI. One capture per workspace. Rejects by name: `Denied` (the person said no, or the OS refused), `NotDeclared`, `Busy`, `DiskFull` (under 2 GB free), `Unsupported` (a page that cannot capture — `cowork app serve`, plain http on another machine). Panes only: a routine cannot capture. | | `capture.shot({ source?, file?, image? })` | One picture of a screen or window, after the same dialog. `image: true` hands the PNG back as `image` (a `Blob`); otherwise (or also, with `file`) it is saved and `path` says where (default `Recordings/<date-time>-<app>.png`). Needs `"capture": ["screenshot"]`. | | `capture.current()` | This app's recording in progress, or `null` — re-attach `onAudio` / `onStop` after the pane remounts. | | `badge(count)` | This app's count on the OS badge, summed over the apps: on the desktop app's dock / taskbar icon for the **active** workspace only; in a browser tab, a `(N) `prefix on its title. Another workspace's count waits until that workspace is active. State, not an event: the latest call wins, `0` clears, nothing expires — report it on every poll (a restart clears it). A muted app's count leaves the sum. From a routine: `ctx.badge(n)`. | | `openedFile` | Set when this widget was opened for a routed file or folder: `{path, workspaceRelativePath, basename, ext, matchedGlob}` (a folder: `matchedGlob` ends in `/` — tell it by that; its `ext` is `""`). The widget gets the PATH, not the content — read/parse (or list) it in a routine. | | `workspaceRoot` | Absolute workspace path. | | `log(...parts)` | A line in the "Cowork" Output panel. | | `visible` / `onVisibility(cb)` | Whether the pane/tab is on screen (the Page Visibility API lies inside webviews). `routines.subscribe` already gates on it; use it for anything else you do while visible. | | `tables.schema/query/exec({ db, … })` | SQLite `.db` files (workspace-relative); `query` is read-only, ≤ 500 rows per page. | | `queue.send(q, { body, subject?, to?, as?, review?, ref?, meta?, delayS?, files? })` / `receive(q, { as?, mine?, leaseS? })` / `ack(q, id, receipt, { result?, files?, review?, to? })` / `nack(q, id, receipt, { delayS?, error? })` / `approve(q, id)` / `reject(q, id, comment)` / `reply(q, id, body, { to?, as?, files? })` / `retry(q, id)` / `tasks({ queue?, view?, agent?, q? })` / `items(q, { view?, agent? })` / `peek` / `list()` / `summary()` / `receivers(q?)` / `remove` / `purge` | The workspace's task queues (`.cowork/local/queues.db`, the Tasks view) — work for agents and scripts (`cowork queue receive --as <name>` in a `/loop`). A task: `subject`, `status` (`todo` · `in_progress` · `needs_review` · `done` · `failed`), `from`, `to` (an agent, `owner` or `anyone`), `review`, the request (`body`, `files`) and `entries` (its thread). Body ≤ 1 MiB; files as `{ name, text \| base64 }`; `receive` → the oldest task for you + its `receipt`, or `null`. | | `journal.skills()` / `entries(skill, { status?, limit? })` / `add({ skill, kind?, text, source? })` / `resolve(skill, ids?)` | The skill journal: learnings / errors per skill, `open` until an agent folds them into the SKILL.md and resolves them. `entries` lists oldest first, the open ones unless `status` says otherwise, up to `limit` (1–200, default 200). | | `agent.setContext(v)` / `agent.select(text)` / `clearContext()` / `clearSelection()` / `setAutoSelection(bool)` | Feed the IDE's AI agent. Text the user selects in the app already reaches it automatically. | | `ui.dialog({ message, detail?, type?, fields?, buttons?, alert?, scope? })` | Cowork's dialog → `{ label, values }` — see [Dialogs](#dialogs). (`alert`/`confirm`/`prompt` don't work in webviews.) | | `ui.confirm({ title?, body?, confirmLabel?, cancelLabel?, destructive? })` | `ui.dialog`'s yes/no shorthand → `true` on confirm, `false` on Cancel or Esc. `destructive: true` for a delete. | | `chat.ask(text)` | Open the workbench chat and submit `text`. | | `consent.request({ value, title, body, detail })` | Ask the user to approve a command → `{granted, ticket?}`. | | `stream.open({ argv, consent, onData, onEnd })` | One long-lived process with raw stdio (terminals, `tail -f`). Needs a consent grant/ticket; max 2 open per view. | | `identity.currentUser()` | The signed-in user `{uid, email}` (or null). | | `viewSource()` | Open this app's source in the text editor. | | `setTitleActions([...])` / `onTitleAction(cb)` | Title-bar buttons on an app's pane in the standalone runner (`cowork app serve`) only; the IDE ignores them, so put an app's buttons in its UI. | | `closeTab()` | Close this app's editor tab. | | `statusBar.get(app)` / `statusBar.onChange(cb)` | An app's footer item (`statusBar` in its manifest) as last drawn, with its `data` — or null. `onChange((app, item) => …)` is called with each app's current item right away, then on every change — subscribe instead of reading `get` at mount. | | `statusBar.refresh(app?)` / `statusBar.click(app)` | Re-run an app's footer routine now (after your widget changed what it reports) / act as if the user clicked it — a widget's "Connect" button can simply be the footer item's click. | ## Dialogs [Section titled “Dialogs”](#dialogs) `cowork.ui.dialog(opts)` resolves `{ label, values }`: the button's label and the fields' values (by field `id`) plus the button's own `values`. Cancel or Esc → `label: null`, with `values` as the fields were left — test `label`, never `values`. * Fields: `text` · `textarea` · `number` · `checkbox` · `select` · `color` · `section` (read-only: `label` + `placeholder` text, `monospace`). `values[id]` is a string (a `number`: a number, `""` while empty; a `checkbox`: a boolean). `select` and `color` take `options: [{ label, value }]` — a swatch's `value` is a hex color (`#rgb` … `#rrggbbaa`) or `""` for no color. * Buttons: `{ label, role: "submit" | "cancel", primary?, destructive?, card?, sublabel?, values? }`; a `card` is an option row in the body. With no buttons, one OK. * To refuse an answer, ask again with `alert: { variant: "destructive", text }` and the typed `value`s. * **Scope:** leave the default `window` (dims the whole IDE) for anything that creates, changes or deletes data; `scope: "pane"` (dims only this pane) is ONLY for a choice that changes how things look, like a theme. ```js const { label, values } = await cowork.ui.dialog({ type: "none", message: "Rename the report?", fields: [{ id: "name", type: "text", label: "Name", value: report.name }], buttons: [{ label: "Cancel", role: "cancel" }, { label: "Rename", primary: true, role: "submit" }], }); if (label === "Rename") await cowork.routines.run("rename-report", { name: values.name }); if (await cowork.ui.confirm({ title: "Delete the report?", confirmLabel: "Delete", destructive: true })) { /* … */ } ``` ## A complete widget [Section titled “A complete widget”](#a-complete-widget) The CRUD template (`cowork template install cowork/app-sidebar-workbench --name <n>`) is the full reference; this is the same pattern for a connected app's sidebar: ```jsx import { useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' export default function Inbox() { const cowork = window.cowork const [data, setData] = useState(() => cowork.state.get() ?? null) // instant paint const [error, setError] = useState(null) const [connecting, setConnecting] = useState(false) const inbox = useRef(null) useEffect(() => { // One loop for the app, while a pane is on screen — never setInterval. inbox.current = cowork.routines.subscribe('mail-inbox', { limit: 25 }, { interval: 60, onData: (result) => { setData(result); setError(null); cowork.state.set(result) }, onError: setError, // NotConnected pauses the loop }) return () => inbox.current.close() }, []) async function connect() { await cowork.credentials.connect('google') // the sign-in of the capability this app "uses" setConnecting(true) // the sign-in finishes in the browser } // NotConnected paused the loop: it resumes on refresh() — or, in the IDE, by itself when a pane // of the app is shown again (the user coming back from the browser). if (error?.name === 'NotConnected') { return ( <div className="flex flex-col gap-2 p-4"> <Button onClick={connect}>Connect Google</Button> {connecting && <Button variant="outline" onClick={() => inbox.current.refresh()}>I've signed in</Button>} </div> ) } return ( <div className="flex h-full flex-col gap-2 bg-background p-3 text-foreground"> {error && <p className="text-sm text-destructive">{error.message} <span className="font-mono text-xs opacity-70">{error.runId?.slice(0, 8)}</span></p>} {!data && !error && [0, 1, 2].map((i) => <Skeleton key={i} className="h-10 w-full" />)} {(data?.messages ?? []).map((m) => ( <button key={m.id} type="button" className="rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent" onClick={() => cowork.agent.select(m.subject)}> {m.subject} </button> ))} </div> ) } ``` ## Not `.html` [Section titled “Not .html”](#not-html) `entry` must be `.jsx` / `.tsx`. A `.html` file renders in a sandboxed iframe that gets only `cowork.agent` — no routines, no theme tokens — so build app UIs as `.jsx`. # CLI reference > Every cowork command, generated from the CLI's usage table — every command and group verb that `cowork --help` and `cowork <command> --help` print. ## cowork (no command) [Section titled “cowork (no command)”](#cowork-no-command) ```sh cowork ``` No args: install if needed, start, open the browser at the workbench URL, then block in the foreground until you close the terminal or hit Ctrl-C. Closing the terminal stops the engine. ## cowork setup [Section titled “cowork setup”](#cowork-setup) ```sh cowork setup [--yes] [--force] ``` macOS/Windows: turn this downloaded exe into a real install — copy to \<root>/bin, add to your user PATH, install the engine, enable autostart, leave the engine running detached, open the workbench. One action. ## cowork install [Section titled “cowork install”](#cowork-install) ```sh cowork install [--force] ``` Install Cowork from the release endpoint (<https://cowork-landing-s9uesd.pages.dev/api/release/latest>). Default for npm + Bun-compiled CLI. ## cowork start [Section titled “cowork start”](#cowork-start) ```sh cowork start --headless ``` The routine runner without the IDE: the daemon in the foreground (systemd, Docker, a terminal) — routines, schedules, webhooks, app auto-updates. Nothing of the IDE is downloaded. A later `cowork start` on the same box adds the IDE. ```sh cowork start [folder] [--port n] [--force] [--verbose|-v] ``` Launch Cowork. If OUR cowork is already running, reports it and exits (single instance, identity-verified). If the default port is taken by a FOREIGN program, moves to a free port and saves it to config (sticky); an explicit --port fails instead. --force replaces a running instance. --verbose passes -vvv to code-server; logs land at \<root>/data/logs/\<TIMESTAMP>/. ## cowork login [Section titled “cowork login”](#cowork-login) ```sh cowork login ``` Sign this machine in from a terminal (works on a server: paste the address the browser ends on). The session renews itself until `cowork logout`. ```sh cowork login --email <address> ``` An account with a password, no browser: the password from a hidden prompt or stdin. ## cowork restart [Section titled “cowork restart”](#cowork-restart) ```sh cowork restart ``` Stop the running Cowork engine and relaunch it. ## cowork open [Section titled “cowork open”](#cowork-open) ```sh cowork open [--browser] ``` Open the Cowork workbench. Reuses the running engine; opens the installed Cowork Studio desktop app if present (--browser skips it), else the installed PWA, else a browser tab. ## cowork desktop [Section titled “cowork desktop”](#cowork-desktop) ```sh cowork desktop install|uninstall|status [--from <zip>|--version <V>|--latest] ``` The Cowork Studio desktop app (Electron shell): By default this CLI's own release (it matches the engine). Install extracts to /Applications (or the per-user equivalent) and the app self-registers its login item + shortcuts (--register). No cert needed on this path (CLI downloads carry no quarantine). ## cowork stop [Section titled “cowork stop”](#cowork-stop) ```sh cowork stop ``` Stop the running Cowork engine + bridge daemon. ## cowork onboarding [Section titled “cowork onboarding”](#cowork-onboarding) ```sh cowork onboarding [status|reset [step…]] ``` The first-run steps, in the terminal: answer what is pending (the same steps the app asks — sign-in itself happens in the app). status lists them; reset asks again (all steps, or the named ones). ## cowork logout [Section titled “cowork logout”](#cowork-logout) ```sh cowork logout [--forget] ``` Sign this machine out of Cowork (every open window follows). --forget also removes that account's local data: its workspaces list, onboarding progress, team-server state, organization access, secrets, the personal server's target here, and the desktop app's browser data. Workspace files are never touched. Never kills a pid it can't verify as cowork's. No-op if nothing is running. ## cowork update [Section titled “cowork update”](#cowork-update) ```sh cowork update [...flags] ``` Alias for install. With --force it stops the running engine first and restarts it after the update (open tabs overlay + reload themselves). ## cowork self-update [Section titled “cowork self-update”](#cowork-self-update) ```sh cowork self-update [--version X.Y.Z] [--port n] ``` One-click update path (spawned by the in-IDE "Update & Restart" button): stop, install through the signed chokepoint, restart, health-verify; rolls back to the previous install on failure. ## cowork status [Section titled “cowork status”](#cowork-status) ```sh cowork status [--json] ``` Print versions, state, and the signed-in account. ## cowork doctor [Section titled “cowork doctor”](#cowork-doctor) ```sh cowork doctor [--fix] [--repatch] [--disable-autostart] ``` Diagnose the install (patch state, pidfiles, daemon health/drift, watcher binary, logs). --fix applies the repairs it reports. --repatch force re-applies the workbench patches (dev loop — for iterating on theme/CSS assets). --disable-autostart removes the start-at-login setup (re-enable by re-running "cowork install"). ## cowork --install-extension|--uninstall-extension|--list-extensions [Section titled “cowork --install-extension|--uninstall-extension|--list-extensions”](#cowork---install-extension--uninstall-extension--list-extensions) ```sh cowork --install-extension <id|vsix> cowork --uninstall-extension <id> cowork --list-extensions [--show-versions] ``` Extension management, passed straight through to the engine (code-server) with cowork's dirs — so whatever the engine's CLI supports works verbatim (or just use the Extensions panel in the workbench). ## cowork command [Section titled “cowork command”](#cowork-command) ```sh cowork command <id> [--args '<json-array>'] [--timeout <sec>] ``` Invoke a registered VS Code command inside the RUNNING workbench (via the bridge daemon + the cowork-command-bridge extension). Allowlisted ids only (cowork.\*, workbench.profiles.actions.\*) — widen via COWORK_COMMAND_ALLOW=\<prefix,…>. ## cowork consent [Section titled “cowork consent”](#cowork-consent) ```sh cowork consent allow|revoke <value> | list [--workspace <dir>] ``` Approve a routine's command ahead of time in a workspace (where no window can ask — headless). ## cowork routine [Section titled “cowork routine”](#cowork-routine) ```sh cowork routine run <id> [--args '<json>'] [--workspace <dir>] [--args -] [--json] ``` Run a routine — declared in an app's cowork.json — in its own process; result printed, the run audited. --args - reads the JSON from stdin; --json prints the outcome as one JSON line (remote callers). ```sh cowork routine list [--workspace <dir>] [--json] ``` The workspace's routines and what each does: active (on its own), on demand, or off. ```sh cowork routine active <id> on|off [--workspace <dir>] [--json] ``` The Active switch: on arms the routine's declared schedule, webhook or notifications (and the workspace); off stops it. ```sh cowork routine runs [--workspace <dir> | --all] [--routine <id>] [--app <name>] [--agent <id>] [--task <id>] [--status ok|error|denied] [--unchanged] [--limit <n>] [--before <cursor>] [--json] ``` The routines' runs in this machine's audit log, newest first, a page at a time (--limit, default 50; each page ends with the command for the next). --all = every workspace; --app = one package's runs; --agent / --task = one agent login's runs / one task's; --unchanged adds the runs that changed nothing. On a server: docker exec / ssh. ```sh cowork routine runs --team-server <ts> [--app <name>] [--device <host>] [--status ok|error|denied] [--limit <n>] [--before <cursor>] [--json] ``` The runs every runner reported to that team server (its owners and admins). ```sh cowork routine runs show <run-id> [--workspace <dir> | --all] [--json] ``` One run: its error, result and log tail (the audit log keeps 7 days). ## cowork machine [Section titled “cowork machine”](#cowork-machine) ```sh cowork machine add <id> <[user@]host> [--port <n>] [--identity <key file>] [--known-hosts <file>] [--workspace <folder there>] [--cli <cowork path there>] ``` A machine running Cowork headless, opened as a remote window: its files, a terminal there, its routines and executions — over ssh only. Adding connects once to check it; --workspace defaults to the home folder there. ```sh cowork machine list ``` The machines added on this computer. ```sh cowork machine remove <id> ``` Forget a machine and close its connection. ```sh cowork machine open <id> [--browser] ``` Open its remote window, as cowork open does (--browser skips the desktop app). ## cowork table [Section titled “cowork table”](#cowork-table) ```sh cowork table create <name> | list [--workspace <dir>] ``` Create a workspace SQLite table at .cowork/data/\<name>.db, listed in Manage Workspace. list shows every .cowork/data/\*.db. ## cowork queue [Section titled “cowork queue”](#cowork-queue) ```sh cowork queue create <name> [--max-attempts <n>] [--lease <s>] [--review | --no-review] [--json] ``` Create a task queue, or change one's settings: the attempts a task gets (default 3), how long a receiver holds a task (lease, default 300 s), and whether finishing a task needs your review. ```sh cowork queue list [--json] ``` The workspace's task queues and their counts: needs you, to do, in progress, needs review, done, failed. ```sh cowork queue send <name> [<text> | --body-file <f> | --stdin] [--subject <line>] [--to <agent|owner|anyone>] [--as <name>] [--review | --no-review] [--ref <id>] [--meta '<json>'] [--file <path>]… [--delay <s>] [--json] ``` Add a task to a workspace task queue — work for agents and scripts (.cowork/local/queues.db, the Tasks view); the first send creates the queue. The subject defaults to the body's first line; --to defaults to anyone, --as (the sender) to owner. --file attaches a copy; --ref is a dedupe key (a second send with it answers the first task); --delay holds the task back \<s> seconds. ```sh cowork queue receive <name> [--as <name> [--mine]] [--lease <s>] [--wait <s>] [--json] ``` Take the oldest task to \<name> or anyone (--mine: only \<name>'s; no --as: unaddressed ones only), with its id and receipt; you hold it for the lease. --wait polls up to \<s> seconds (600 at most). Exit 3 = empty. ```sh cowork queue ack <name> <id> --receipt <r> [--result <text> | --result-file <f>] [--file <path>]… [--review] [--to <agent|owner|anyone>] [--json] ``` Finish a received task: Done, or Needs review with --review (or when the task asks for review); --to hands it on as To do. --result is your entry; --file attaches copies of workspace files to it. ```sh cowork queue nack <name> <id> --receipt <r> [--delay <s>] [--error <text>] [--json] ``` Give it back: To do again after --delay, Failed once the queue's attempts run out; --error is your entry. ```sh cowork queue approve <name> <id> [--json] ``` Needs review → Done. ```sh cowork queue reject <name> <id> --comment <text> [--file <path>]… [--json] ``` Send back a task in Needs review: To do again for the agent who finished it, with --comment as what to change. ```sh cowork queue reply <name> <id> [<text> | --body-file <f> | --stdin] [--to <agent|owner|anyone>] [--as <name>] [--file <path>]… [--json] ``` An entry on the task's thread. To an agent or anyone it sends the task to it (reopening a done or failed one); to owner, to nobody, or while the task is in progress, it is a note. ```sh cowork queue retry <name> <id> [--json] ``` Failed → To do. ```sh cowork queue peek <name> <id> [--json] ``` One task and its thread, without taking it. ```sh cowork queue items <name> [--agent <name>] [--q <words>] [--limit <n>] [--json] [--view needs_you|todo|in_progress|needs_review|done|failed|all] ``` A queue's tasks, one line each: --agent = to, from or held by that name; --q = every word; --limit (default 50, at most 200). ```sh cowork queue receivers [<name>] [--json] ``` The agents seen taking tasks (of one queue, or every queue), and whether each is listening now. ```sh cowork queue summary [--json] ``` What needs you across every queue, and the open journal entries. ```sh cowork queue delete <name> [<id>] [--yes] ``` Delete one task and its files; without an id, the whole task queue (needs --yes). ```sh cowork queue purge <name> [--status done|failed|all] ``` Delete the queue's done tasks (default), its failed ones, or all of them. Every verb takes --workspace \<dir> (default: the current folder). ## cowork journal [Section titled “cowork journal”](#cowork-journal) ```sh cowork journal add <skill> [<text> | --text-file <f> | --stdin] [--kind learning|error|note] [--source <s>] [--json] ``` Write an entry for a skill — a learning (default), an error or a note — open until an agent folds it into the skill and resolves it. ```sh cowork journal list [--json] ``` Every skill with entries: how many are open and applied, and where its SKILL.md is. ```sh cowork journal entries <skill> [--open | --applied | --all] [--json] ``` A skill's entries, oldest first — the open ones unless --applied or --all. Exit 3 = none open. ```sh cowork journal resolve <skill> [<id>…] ``` Mark entries applied: the given ids, or every open entry of the skill. ```sh cowork journal delete <id> ``` Delete one entry. Every verb takes --workspace \<dir> (default: the current folder). ## cowork personal-server [Section titled “cowork personal-server”](#cowork-personal-server) ```sh cowork personal-server [status] [--json] ``` This account's personal server: the web inbox mirrors a folder's `inbox` task queue both ways (/loop /task-inbox \[name] takes its tasks). Status: who serves it, when it was last seen. Every verb needs Cowork running and signed in. ```sh cowork personal-server create [<dir>] [--json] ``` Create it, served by this computer from \<dir> (default: the current folder). ```sh cowork personal-server target <dir> [--json] ``` Change the folder: this computer serves it from \<dir>. ```sh cowork personal-server delete --yes [--json] ``` Delete it: its tasks and files on the server (this computer's inbox stays). ```sh cowork personal-server pull [--json] ``` Sync now: apply the web inbox's actions here and push the queue's tasks. ## cowork mcp-browser [Section titled “cowork mcp-browser”](#cowork-mcp-browser) ```sh cowork mcp-browser ``` Stdio MCP server exposing the desktop shell's embedded browser (snapshot→ref→click/fill/read). Point Claude Code or any MCP client at it; the shell must be running. Long-lived — the client owns the lifetime. ## cowork app [Section titled “cowork app”](#cowork-app) ```sh cowork app list [--json] ``` Every app in the workspace (a .cowork/apps/\<name>/ folder holding cowork.json): version, where it came from (local | \<team-server>@\<version>, pinned or auto), on/off, whether a newer version is out. ```sh cowork app install <team-server>/<app>[@X.Y.Z] ``` Install an app from a team server's store into .cowork/apps/\<app>/. @X.Y.Z takes a version the store still keeps and PINS it (auto-update leaves it). ```sh cowork app install <https url> [--title <name>] [--display tab|sidebar|right] ``` Install a web site as a site app in .cowork/apps/\<name>/: its name, icon and color from the site's web app manifest (the page's title and icon when it has none). It opens in the desktop app, signed in with your browser's accounts — in a tab, or a side bar pane (right: beside Chat). ```sh cowork app update [<app>] [--replace|--keep-both|--ai-merge |--skip] ``` No name: every clean, unpinned update. A name: take the latest (unpins); an app you edited needs one choice (replace / keep both / Claude merges / skip this version). ```sh cowork app remove <app> ``` Uninstall a store or site install (files you edited are kept). A local app is yours — delete its folder yourself. ```sh cowork app enable|disable <app> ``` Switch the whole app folder on/off. ```sh cowork app versions <team-server>/<app> ``` The versions the store keeps (yours marked). ```sh cowork app unpublish <team-server>/<app>[@X.Y.Z] --yes ``` Delete it from the store (admin); with @X.Y.Z only that version (not the latest — push a newer one first). Installed copies keep working. ```sh cowork app open <file.jsx|cowork.json> [--as <mode>] ``` Open an app in the running workbench — a site app by its .cowork/apps/\<name>/cowork.json. ```sh cowork app serve [folder] [--port n] [--no-open] ``` Run the project's apps as a local webapp (no IDE) at <http://127.0.0.1:51190> (--port); its routines run through this computer's daemon. ```sh cowork app build [folder] [--out <dir>] ``` Write them as a static website: routines run at their deployed URLs (`cowork deploy run`). ```sh cowork app deploy [folder] [--project <name>] [--tables <d1-name>] (every app verb: [--workspace <dir>]) ``` Build + publish to Cloudflare Pages. --tables \<d1> also deploys the site's tables endpoint. ## cowork push [Section titled “cowork push”](#cowork-push) ```sh cowork push [dir] [--to <team-server> [--remember]] [--dry-run] [--json] ``` Publish each app's cowork.json version to a team server's store: dir is an app folder (it holds cowork.json) or a workspace (every .cowork/apps/\<name>/cowork.json in it); default the current folder. Where it goes: --to, else the store the app was installed from, else "publishTo" in .cowork/workspace.json; --remember stores the --to team server as that "publishTo". Unchanged content is "up to date" (nothing uploaded). Changed content at an already-published version bumps the patch and writes it back to cowork.json. A version older than the store's is refused — run `cowork app update <app>` first. After a push the app follows its store like any install, so a collaborator's push reaches you as an update. --dry-run says what it would do. ## cowork org [Section titled “cowork org”](#cowork-org) ```sh cowork org [me] [--json] ``` Your organization, the team servers it lets you install from, and the orgs you manage. ```sh cowork org groups [--org <slug>] [--json] ``` The groups of an org you manage and their members (emails). ```sh cowork org group create <name> [--members e1,e2] [--org <slug>] ``` Create a group. Members are emails of people already in the organization. ```sh cowork org group set <name> [--add-members e1,e2] [--remove-members e1,e2] [--org <slug>] ``` Change a group's members. Applied to the latest saved state (a concurrent save is re-read and re-applied once). ```sh cowork org group delete <name> --yes [--org <slug>] ``` Delete a group for the whole organization — the team servers opened to it close to its members. \--org \<slug> picks the organization when you manage several. ## cowork agent [Section titled “cowork agent”](#cowork-agent) ```sh cowork agent list [--json] ``` This account's agent logins: name, id, when each last connected, the revoked ones. ```sh cowork agent create <name> [--workspace <dir>] cowork agent token <name> [--workspace <dir>] ``` A login for an agent (Claude Code, Codex …), or a new token for one (the old one stops working). Prints the token ONCE, and the lines that connect a client to this workspace's capabilities. ```sh cowork agent revoke <name> --yes ``` Sign an agent out for good: its token stops working everywhere. ```sh cowork agent grants [--agent <name>] [--workspace <dir>] [--json] ``` What each agent may run here: every routine of the workspace's capabilities, the default and each agent's answer (\* = set here; otherwise reads are on, the rest off). ```sh cowork agent grant <cap>/<routine>|<cap>/* allow|deny|default [--agent <name>] [--workspace <dir>] ``` Allow or deny one routine (\<cap>/\* = every routine of the capability) for one agent, or by default for all; default drops the setting. ```sh cowork agent door [--listen <host:port>] [--url <url>] cowork agent door off ``` The agents' door on its own listener, for agents on other machines: it serves /api/mcp alone. No flags shows it; --url is the address agents connect to (required for 0.0.0.0) — put HTTPS in front of it; off closes it. ## cowork credential [Section titled “cowork credential”](#cowork-credential) ```sh cowork credential list [--json] [--workspace <dir>] ``` The connections this workspace's capabilities declare: sign-in or key, connected or not, and the capabilities that use each. ```sh cowork credential connect <name> [--field <k>[=<v>] …] [--workspace <dir>] ``` Connect one: a key from its fields (--field token=…, or --field token alone to read it from stdin, out of shell history); a sign-in prints the address to open in a browser. ```sh cowork credential disconnect <name> ``` Forget it (revoked at the service when it can be) — for every capability that uses it. ## cowork deploy [Section titled “cowork deploy”](#cowork-deploy) ```sh cowork deploy add <id> [--target <t>] [flags] [--mode local|deployed] [--webhook] [--schedule "<cron>"] [--secret N]... [--notifications <app>[,<app>|*]] [--var K=V]... [--name x] [--region r] [--role-arn a] [--hook-token] [--db <d1-name>] [--cors-origin o] [--workspace <dir>] ``` A routine's trigger (and deployment), in .cowork/workspace.json#deployments — the only place a schedule, webhook or notifications trigger lives. --schedule = a 5-field cron, --webhook = an HTTP trigger, --notifications = the notifications these apps raise (names as in `cowork app list`; \* = every app's), each one the run's args. --mode local (default): this computer fires the schedule, in local time, once armed (cowork routine active \<id> on); deployed: only the copy `deploy run` put on the target does, in UTC (a webhook answers and notifications fire locally either way). No --target: auto-picks worker|container from the routine's imports (aws-lambda | gcloud-function: explicit only). --db: a Worker's ctx.db D1 database, created at `deploy run` when missing. Only NAMES land in git — never secret values. ```sh cowork deploy remove <id> ``` Unregister a deployment (drops its local hooks). ```sh cowork deploy list | status ``` List deployments (hook URLs, schedule, deployed URL, triggers armed?); status adds per-target tool/auth doctor output. ```sh cowork deploy build <id> ``` Build the deploy artifact into .cowork/local/deploy-build/ (prints files + warnings). ```sh cowork deploy dev <id> [--port N] ``` Run the platform's LOCAL emulator in the foreground (workerd / RIE / functions-framework / docker). Ctrl-C to stop. ```sh cowork deploy run <id> [--consumer-tokens-file <path>] ``` Deploy for real via the platform CLI; prints the webhook URL. --consumer-tokens-file bakes accepted bearer-token hashes into the deployment. ```sh cowork deploy hooks ``` Print every local webhook URL for the workspace. ```sh cowork deploy triggers on|off ``` Arm / disarm local webhooks, schedules and notification triggers for this workspace — the consent gate (off by default; an imported workspace.json never self-activates). ```sh cowork deploy doctor ``` Per-target tool + auth preflight. ## cowork webapp [Section titled “cowork webapp”](#cowork-webapp) ```sh cowork webapp deploy <dirOrName> [--project p] [--branch main|staging] [--routines id,id] [--workspace <dir>] ``` Stage a static webapp (injecting cowork-routines.js so it can call your deployed routines) and deploy it to Cloudflare Pages. ```sh cowork webapp dev <dirOrName> [--port N] [--routines id,id] ``` Serve the staged webapp locally via wrangler pages dev (routines wired to local webhooks). ## cowork secrets [Section titled “cowork secrets”](#cowork-secrets) ```sh cowork secrets set|get|list|delete <name> ``` The signed-in account's secret store (encrypted, key in the OS keychain). Values are readable only by routines (ctx.secrets.get) and 'secrets get' — never by webviews or daemon routes. ## cowork publish [Section titled “cowork publish”](#cowork-publish) ```sh cowork publish [folder] [--items p1,p2] [--team-server <slug>]... [--out <file>] [--type <type>] [--subtype <s>] [--requires <r>]... [--version X.Y.Z] [--name <n>] [--title <t>] [--description <d>] [--tags a,b] [--screenshot] [--timeout <s>] [--dry-run] ``` Package this workspace and publish it to a team server's store. No --team-server: packages only and prints the file path (a temp file unless --out). With --team-server: uploads into that team server's store (you must be signed in as one of its admins); name several and each answers on its own. --items packages just those paths (a folder whole, plus any \_shared/ helpers above them); exactly one item makes it a workspace-component. Missing paths are reported, not packaged. --type / --subtype default to assets.json's item block, then to what the selection implies; a template needs a subtype. --requires desktop|web says where the item runs. The server assigns the version unless --version pins one. --title / --description / --tags are the store card; a republish keeps any card field it does not state. --screenshot (a workspace or workspace template) makes the folder's last capture — the one Cowork Studio's Home shows — its first screenshot while it has none; look at it first: it is the window as it was, which can show more than the files you publish. --timeout raises the upload's wait (default 120 s). --dry-run uploads nothing. Apps: cowork push. \<type>: workspace|workspace-component|template|course ## cowork team-server [Section titled “cowork team-server”](#cowork-team-server) ```sh cowork team-server list ``` Your team servers (slug, role, counts). A team server is members + a store of things they can install; it is the one place you share through. ```sh cowork team-server create <name> [--desc <t>] [--visibility public|private] [--requires-approval] ``` Create one. Display names aren't unique — the slug is minted for you. Default visibility is private (invite-only); --requires-approval turns a join into a request one of its admins approves. ```sh cowork team-server info|join|leave <team-server> ``` Show one / join it (--note \<text> rides a join request) / leave it. ```sh cowork team-server members [list] <team-server> ``` Who is in it. ```sh cowork team-server members add <ts> <email> [--admin] ``` Add someone by email (--admin makes them an admin). They get the team server's updates. ```sh cowork team-server members remove <ts> <email> ``` Remove someone. Their Cowork drops the team server (and its courses) at the next updates poll. ```sh cowork team-server requests <team-server> cowork team-server members approve|deny <ts> <e> ``` Who is waiting to join; settle each with approve or deny. ```sh cowork team-server store <team-server> ``` What it publishes (latest version of each item). ```sh cowork team-server install <team-server>/<item>[@X.Y.Z] [--dir <path>] ``` Install an item: a workspace (or a workspace template, untracked) into --dir (default the current folder), a workspace-component into .cowork/apps/\<name>/ of the workspace at --dir. @X.Y.Z takes a retained version and pins it. App / routine / skill templates are scaffolded (cowork template install), and courses are subscribed. Cowork does not review what team servers publish. ```sh cowork team-server delete <team-server> --yes ``` Delete it for everyone (owner only). ## cowork template [Section titled “cowork template”](#cowork-template) ```sh cowork template install <team-server>/<name> --name <component> [--dir <path>] [--host web|desktop] ``` Scaffold an app / routine / skill template into the workspace at --dir (default the current folder), as the Create menu does. \<component> names its folder and files; nothing is written if any target path exists. ## cowork component [Section titled “cowork component”](#cowork-component) ```sh cowork component install <git url>[#ref] [--path sub] [--commit sha] [--allow-unverified] ``` Install a component straight from a git repo into .cowork/apps/\<name>/. Never attested, so it always shows the unverified screen; the override must pin --commit \<sha>. ## cowork --import-profile [Section titled “cowork --import-profile”](#cowork---import-profile) ```sh cowork --import-profile <file.code-profile> ``` Import a profile into the running workbench and switch to it (native .code-profile format). Idempotent: an existing same-name profile is reused (switched to), never duplicated. --timeout \<sec> raises the wait budget (default 300s; the workbench keeps applying even if the CLI stops waiting). ## cowork --activate-profile [Section titled “cowork --activate-profile”](#cowork---activate-profile) ```sh cowork --activate-profile <name> ``` Switch the running workbench to the named profile (created empty if it doesn't exist). ## cowork --export-profile [Section titled “cowork --export-profile”](#cowork---export-profile) ```sh cowork --export-profile <name> <file> ``` Export the named profile ("current" = the active one) to a .code-profile file, no save dialog. ## cowork --list-profiles [Section titled “cowork --list-profiles”](#cowork---list-profiles) ```sh cowork --list-profiles ``` List the workbench's profiles (name, id, current). ## cowork --delete-profile [Section titled “cowork --delete-profile”](#cowork---delete-profile) ```sh cowork --delete-profile <name> ``` Delete the named profile (and its server-side mirror). The Default profile can't be deleted. ## cowork --import-workspace [Section titled “cowork --import-workspace”](#cowork---import-workspace) ```sh cowork --import-workspace <file.cowork> ``` Import a local workspace bundle (zip: profile + files). Copies its files into the open workspace folder per assets.json (always | if-newer | if-missing | never), then imports the bundled profile. --dry-run prints the plan, changes nothing; --timeout \<sec> raises the profile-apply budget (default 300s; big templates can need more). A bundle may declare a postinstall app (assets.json "events") that opens after import (--no-events suppresses it). To install what a team server publishes, use cowork team-server install. ## cowork --export-workspace [Section titled “cowork --export-workspace”](#cowork---export-workspace) ```sh cowork --export-workspace <name> <file> ``` Bundle the named profile ("current" = active) + the files listed in the workspace's assets.json into a .cowork zip. .gitignore is honored (node_modules & co. stay out). --dry-run lists the would-be bundle without exporting. ## cowork --help [Section titled “cowork --help”](#cowork---help) ```sh cowork --help ``` Show this message. ## cowork --version [Section titled “cowork --version”](#cowork---version) ```sh cowork --version ``` Print this CLI's version. ## Notes [Section titled “Notes”](#notes) Default install: downloads the prebuilt Cowork bundle for your OS/arch from the release endpoint and seeds \<root>. No patches run locally — the bundle is already cooked. \<root> is \~/.cowork-studio (Windows: %LOCALAPPDATA%\Cowork), or $COWORK_HOME. # VS Code commands reference > The VS Code commands Cowork's extensions contribute, and which of them cowork command may run. `cowork command <id>` runs a command in the open IDE. It runs ids that start with `cowork.` or `workbench.profiles.actions.`; `COWORK_COMMAND_ALLOW=<prefix,…>` widens that for one invocation. ## Commands [Section titled “Commands”](#commands) | Command | Title | When | Extension | | ---------------------------------------- | --------------------------------------------------------------- | ---------------------- | ------------------------ | | `cowork.academy.discover` | Cowork: Find Courses | | cowork-frontend-sidemenu | | `cowork.agentMap.open` | Cowork: Show in Agent Skills | | cowork-frontend-sidemenu | | `cowork.browser.toggle` | Cowork: Show Browser | | cowork-command-bridge | | `cowork.chooseApp` | Cowork: Choose the App for This File Type… | | cowork-frontend-sidemenu | | `cowork.create.new` | Cowork: Create New… | | cowork-command-bridge | | `cowork.editSource` | Cowork: Edit Source | | cowork-frontend-sidemenu | | `cowork.marketplace.createTeamServer` | Cowork: Create a Team Server | | cowork-marketplace | | `cowork.marketplace.joinTeamServer` | Cowork: Join a Team Server | | cowork-marketplace | | `cowork.marketplace.open` | Cowork: Open Team Servers | | cowork-marketplace | | `cowork.marketplace.refresh` | Cowork: Refresh Team Servers | | cowork-marketplace | | `cowork.monitor.open` | Cowork: Open Monitor | | cowork-frontend-sidemenu | | `cowork.openAsApp` | Cowork: Open as App | | cowork-frontend-sidemenu | | `cowork.publish.open` | Cowork: Open Publish Workspace | | cowork-frontend-sidemenu | | `cowork.remote.open` | Cowork: Open Machine… | | cowork-remote | | `cowork.remote.reconnect` | Cowork: Reconnect to This Machine | `cowork.remote.window` | cowork-remote | | `cowork.routines.open` | Cowork: Open Routines | | cowork-frontend-sidemenu | | `cowork.runRoutine` | Cowork: Run Routine… | | cowork-frontend-sidemenu | | `cowork.showLogs` | Cowork: Show Logs | | cowork-frontend-sidemenu | | `cowork.tasks.open` | Cowork: Open Tasks | | cowork-frontend-sidemenu | | `cowork.teams.workspace` | Cowork: Workspace Home | | cowork-teams | | `cowork.tools.open` | Cowork: Open Manage Workspace | | cowork-frontend-sidemenu | | `cowork.workspace.switch` | Cowork: Switch Workspace… | | cowork-command-bridge | | `coworkDocs.detectLibreOffice` | Cowork Docs: Diagnose LibreOffice | | cowork-docs | | `coworkDocs.openFolderView` | Cowork Docs: Open Folder View | | cowork-docs | | `coworkDocs.openFolderViewAtPath` | Cowork Docs: Open Folder View at Path | | cowork-docs | | `coworkNotebook.findReplace.close` | Cowork Notebook: Close Find Widget | | cowork-notebook | | `coworkNotebook.findReplace.next` | Cowork Notebook: Find Next | | cowork-notebook | | `coworkNotebook.findReplace.open` | Cowork Notebook: Find | | cowork-notebook | | `coworkNotebook.findReplace.openReplace` | Cowork Notebook: Replace | | cowork-notebook | | `coworkNotebook.findReplace.previous` | Cowork Notebook: Find Previous | | cowork-notebook | | `coworkNotebook.findReplace.toggleCase` | Cowork Notebook: Toggle Find: Match Case | | cowork-notebook | | `coworkNotebook.findReplace.toggleRegex` | Cowork Notebook: Toggle Find: Use Regular Expression | | cowork-notebook | | `coworkNotebook.findReplace.toggleWord` | Cowork Notebook: Toggle Find: Match Whole Word | | cowork-notebook | | `coworkNotebook.trackChanges.ai` | Cowork Notebook: Track changes — Show AI / external suggestions | | cowork-notebook | | `coworkNotebook.trackChanges.all` | Cowork Notebook: Track changes — Show all sources | | cowork-notebook | | `coworkNotebook.trackChanges.cycle` | Cowork Notebook: Track changes — Cycle display mode | | cowork-notebook | | `coworkNotebook.trackChanges.git` | Cowork Notebook: Track changes — Show git diff (cherry-pick) | | cowork-notebook | | `coworkNotebook.trackChanges.off` | Cowork Notebook: Track changes — Off | | cowork-notebook | | `coworkNotebook.trackChanges.user` | Cowork Notebook: Track changes — Show user drafts | | cowork-notebook | | `coworkProjects.addToProjects` | Add to Projects | `false` | cowork-projects | | `coworkProjects.newProject` | Cowork Projects: New | | cowork-projects | | `coworkProjects.openFolderView` | Open in Folder View | `false` | cowork-projects | | `coworkProjects.openHome` | Cowork Projects: Home | | cowork-projects | | `coworkProjects.openInProjects` | Open in Projects | `false` | cowork-projects | | `coworkProjects.search` | Cowork Projects: Search | | cowork-projects | | `coworkProjects.sortAlphabetical` | Alphabetical | | cowork-projects | | `coworkProjects.sortCreated` | Created | | cowork-projects | | `coworkProjects.sortRecent` | Recent | | cowork-projects | | `coworkUpdates.restartToUpdate` | Cowork: Update Cowork… | `cowork.update.staged` | cowork-updates | | `workbench.action.browser.open` | Browser: Open Integrated Browser | | cowork-command-bridge | # cowork.json reference > Every field of an app's manifest, generated from cowork.schema.json — the schema the editor validates cowork.json against. A folder .cowork/apps/\<name>/ with this file is a Cowork app: it shows on the activity bar, brings its routines and file routes, and is what `cowork push` publishes to a team server. ## `$schema` [Section titled “$schema”](#schema) `string` ## `name` [Section titled “name”](#name) `string` · **required** *(pattern `^[a-z0-9][a-z0-9-]{0,62}$`)* Store identity and activity-bar id: lowercase letters, digits and dashes. Unique within a workspace and within a team server's store. ## `version` [Section titled “version”](#version) `string` · **required** *(maxLength 40, pattern `^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`)* Semver X.Y.Z. `cowork push` publishes exactly this version (and bumps the patch when this version is already published and the files changed). ## `title` [Section titled “title”](#title) `string` *(maxLength 200)* Display name: the activity-bar tooltip, the pane title, the store card. Defaults to name. A title starting with a system icon's name (Explorer, Search, Manage Workspace, …) gets no activity-bar icon. ## `description` [Section titled “description”](#description) `string` *(maxLength 2000)* One or two sentences for the store card. ## `icon` [Section titled “icon”](#icon) `string` *(pattern `^(?:[a-z0-9-]+|\$\([a-z0-9-]+\)|(?![/\\])(?!.*(?:^|/)\.\.?(?:/|$))(?!.*//)[^\\:]+\.(?:[pP][nN][gG]|[sS][vV][gG]|[wW][eE][bB][pP]|[jJ][pP][eE]?[gG]))$`)* A codicon id, e.g. "mail", "calendar", "rocket" (<https://microsoft.github.io/vscode-codicons/>), or an image file inside this folder, e.g. "icon.png" (.png/.svg/.webp/.jpg, at most 256 KB) — shown full-color on the activity-bar tile. ## `color` [Section titled “color”](#color) `string` *(pattern `^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`)* Icon tile background on the activity bar, e.g. "#EA4335" (the glyph turns white). ## `entry` [Section titled “entry”](#entry) `string` *(pattern `\.(jsx|tsx)$`)* The app UI, relative to this folder. Present → the app gets an activity-bar icon. A web app has "web" instead — never both. ## `web` [Section titled “web”](#web) `object` Makes the folder a web app — index — or a site app — url. A web app: a prebuilt static site (an index.html with its scripts, styles, .wasm …) that opens in an editor tab on its own origin, unmodified. Always display "tab". Route files to it with fileRouting → its index, e.g. {"\*\*/\*.docx": "web/index.html"}. Opens in the Cowork Studio desktop app and in Cowork in a browser tab or the PWA, served by the Cowork daemon on its own \*.app.localhost origin. * `web.index` · `string` *(maxLength 200, pattern `^(?![/\\])(?!.*(?:^|/)\.\.?(?:/|$))(?!.*//)[^\\:]+\.[hH][tT][mM][lL]?$`)* — The site's page, e.g. "web/index.html" — its folder is what is served. Relative to this folder; with download, relative to the downloaded build instead (a zip whose files sit in one top folder has that folder stripped), so usually "index.html" — and a build copied into this folder as web/\<index> wins over the download. * `web.open` · `string` *(maxLength 200, pattern `^drop:\S`)* — Only for an app that opens a file just by a drop: "drop:\<css selector>", e.g. "drop:#canvas" — the opened file is dropped on that element. Apps that open files through a file picker or an \<input type=file> need nothing here. * `web.download` · `object` — Where the build comes from, so the app folder stays small: fetched on the first open, verified, unpacked and cached once per sha256. index is then relative to the build. * `web.download.url` · `string` **required** *(maxLength 2048, pattern `^https://`)* — The build's zip, e.g. a GitHub release asset. * `web.download.sha256` · `string` **required** *(pattern `^[0-9a-fA-F]{64}$`)* — The zip's SHA-256 (shasum -a 256 \<file>): checked before anything is unpacked. * `web.url` · `string` *(maxLength 2048, pattern `^(https://|http://(localhost|127\.0\.0\.1|\[::1\])([:/]|$))`)* — Makes the folder a site app instead: the address of a web site (e.g. "<https://chat.example.com/>") that opens in an app tab of the Cowork Studio desktop app — or in a side bar pane, with display "right" or "sidebar" — signed in with your browser's accounts — its notifications and its unread count become the app's. https (http only on localhost), no user or password. Never together with index; open, download and fileRouting do not apply. * `web.scope` · `string` *(maxLength 2048, pattern `^(https://|http://(localhost|127\.0\.0\.1|\[::1\])([:/]|$))`)* — A site app's pages: a start of url on its origin, e.g. "<https://example.com/app/>". Default: url up to its last "/". ## `display` [Section titled “display”](#display) `"sidebar" | "tab" | "modal" | "right"` *(default `"sidebar"`)* Where the app shows. sidebar = in a pane of the left side bar, opened by its activity-bar icon; tab = in an editor tab (no side panel); modal = in a dialog; right = in a pane of the right side bar, a tab of its own there beside Chat (no activity-bar icon). A site app (web.url) is tab by default and may be sidebar or right, the site drawn in the pane; a web app (web.index) is always tab. ## `open` [Section titled “open”](#open) `string | object` Sidebar apps only: what to open in the editor when the icon is first clicked — an app file in this folder, or a routine that returns {path}. * `open.routine` · `string` **required** *(pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`)* * `open.args` · `object` ## `routines` [Section titled “routines”](#routines) `{ [key: string]: string | object }` Routine id → .ts file in this folder, or {"file": "x.ts", "description": "…", "readOnly": true, "input": {…}, "timeout": 120} to say what it does or give it longer than 30 s. Apps call them with cowork.routines.run(id, args). An agent sees each routine of a capability as a tool, by its description and input: a readOnly one it may run unless the person turns it off, any other only once the person allows it. * `routines.file` · `string` **required** *(pattern `\.(ts|js|mjs)$`)* * `routines.description` · `string` *(maxLength 400)* — One line on what it does — what an agent reads to pick it. * `routines.readOnly` · `boolean` *(default `false`)* — It changes nothing (lists, opens, looks up): no write at the service, no money spent, no user code run, nothing written in the workspace but its own cache. Every agent may run it unless the person turns it off, and an agent's app may run it without asking — when in doubt it is not a read. Never together with destructive or reachesOthers. * `routines.destructive` · `boolean` *(default `false`)* — What it changes cannot be undone (deletes, refunds). * `routines.reachesOthers` · `boolean` *(default `false`)* — It reaches people beyond the account (sends, posts, pays). * `routines.input` · `object` — Its arguments as a JSON Schema, at most 8 KB: {"type": "object", "properties": {"query": {"type": "string", "description": "…"}}, "required": \["query"]}. Leave it out for a routine that takes none. * `routines.input.type` · `any` **required** * `routines.input.properties` · `object` * `routines.timeout` · `integer` *(minimum 1, maximum 300)* — How long one run may take, in seconds (default 30). Runs from apps, agents (`cowork routine run`), schedules and webhooks all honor it. ## `fileRouting` [Section titled “fileRouting”](#filerouting) `{ [key: string]: string }` Workspace-relative glob → app file in this folder that renders matching files (e.g. "\*\*/\*.maillist.json": "mail-list.jsx"). A glob ending in / routes folders instead ("Notebook/": "notebook.jsx" opens when the folder is clicked). An .html value opens the file in this app's web app and must be its web.index. ## `requires` [Section titled “requires”](#requires) `("web" | "desktop")[]` *(maxItems 1)* Where the app can run. \["desktop"] = Cowork Studio only (e.g. it drives the embedded browser). Empty = anywhere. ## `tags` [Section titled “tags”](#tags) `string[]` *(maxItems 20)* ## `banner` [Section titled “banner”](#banner) `string` *(pattern `^(?![/\\])(?!.*(?:^|/)\.\.?(?:/|$))(?!.*//)[^\\:]+\.(?:[pP][nN][gG]|[sS][vV][gG]|[wW][eE][bB][pP]|[jJ][pP][eE]?[gG])$`)* The store listing's wide picture: an image file inside this folder, e.g. "art/banner.png" (.png/.svg/.webp/.jpg, at most 1 MB). cowork push uploads it. ## `screenshots` [Section titled “screenshots”](#screenshots) `string[]` *(maxItems 5)* The store listing's screenshots, in order: image files inside this folder (.png/.svg/.webp/.jpg, at most 1 MB each). cowork push uploads them and clears any the store holds beyond them. ## `statusBar` [Section titled “statusBar”](#statusbar) `object` One item in the footer (status bar), drawn from what a routine of this app returns. Widgets read it with cowork.statusBar.get(name) and can refresh or click it. * `statusBar.routine` · `string` **required** *(pattern `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`)* — One of this app's routines. It returns the item — {text, icon?, tooltip?, tone?: "warning"|"error", click?: {args, confirm?}, menu?: \[{label, args, confirm?}], data?} — or null to hide it. A click runs the same routine with the click's args. * `statusBar.args` · `object` — What the routine gets when the item refreshes. Default {}. * `statusBar.interval` · `integer` *(default `60`, minimum 15, maximum 3600)* — Seconds between refreshes while the window has focus (it also refreshes when focus returns and after a click). ## `credential` [Section titled “credential”](#credential) `object` How this capability connects to its service: a sign-in (oauth), a pasted key (key), or either. The person connects once in Manage Workspace › Components › Credentials, and its routines call the service with ctx.credential. A capability's only — an app's is ignored, with a problem line: it reaches the connections of the capabilities it uses. * `credential.oauth` · `object` * `credential.oauth.provider` · `"google" | "microsoft"` **required** — The service it signs in to: "google" or "microsoft" (a provider the OAuth engine has a preset for). * `credential.oauth.scopes` · `string[]` **required** *(maxItems 32)* — The access its routines need. Every capability signing in to the same provider shares ONE sign-in; connecting asks for what they all need together. * `credential.oauth.ownClient` · `boolean` — It signs in through its own OAuth client (the person's own Cloud project), under its own name, instead of the shared sign-in. * `credential.key` · `object` * `credential.key.fields` · `string[]` **required** *(minItems 1, maxItems 8)* — What the person pastes, e.g. \["token"]. * `credential.key.url` · `string` *(maxLength 2048, pattern `^https://`)* — Where to get it, e.g. the service's API keys page. * `credential.key.hint` · `string` *(maxLength 200)* — One line on how, shown beside the form. ## `capture` [Section titled “capture”](#capture) `("screen" | "system-audio" | "mic" | "screenshot")[]` What the app's panes may capture (each still asked for in Cowork's dialog): a recording with video needs "screen", its audio "system-audio" and/or "mic"; a still needs "screenshot". Not on a capability. ## `kind` [Section titled “kind”](#kind) `"app" | "capability"` *(default `"app"`)* app = something people open; capability = a package of routines that apps use and agents call as tools — no entry, no fileRouting, no uses of its own. Manage Workspace lists capabilities under Components › Capabilities; it never changes what the package registers. ## `uses` [Section titled “uses”](#uses) `{ [key: string]: string }` *(maxProperties 8)* The capabilities this app needs: capability name → the lowest version it works with, e.g. {"gmail": "0.2.0"} (met by the same major at that version or above). Manage Workspace › Components › Capabilities shows any that are missing, switched off or too old. # workspace.json reference > Every key of a workspace's .cowork/workspace.json, generated from workspace.schema.json — the schema the editor validates it against. What a workspace remembers: how the window starts, what is switched off, where pushes go, how routines are deployed. It never says what the workspace has (apps, routines and file routes come from each app's cowork.json), and it never travels with a pushed or exported app. Every key is optional and a missing file is normal; a value of the wrong type is ignored, never an error. ## `$schema` [Section titled “$schema”](#schema) `string` ## `name` [Section titled “name”](#name) `string` The workspace's display name in the projects list. Defaults to the folder's name. ## `startup` [Section titled “startup”](#startup) `object` How the window looks when this workspace opens. * `startup.home` · `string` *(pattern `^[a-z0-9][a-z0-9-]{0,62}$`)* — The app the window opens on, by app name. Defaults to the app named "home", if the workspace has one. * `startup.left` · `boolean` — false starts with the side bar closed, true with it open. Absent keeps whatever the window last had. * `startup.right` · `boolean` — false starts with the secondary side bar closed, true with it open. Absent keeps whatever the window last had. ## `disabled` [Section titled “disabled”](#disabled) `object` Things switched off without deleting them. A checked-in list can only ever turn things off. * `disabled.routines` · `string[]` — Routine ids that refuse to run and are skipped by schedules. They still show in lists. * `disabled.apps` · `string[]` — App folders (e.g. ".cowork/apps/gmail") or single app files, relative to the workspace. A switched-off app stays on disk but has no activity-bar icon, is never the home app, and its file routes fall through to the default editor. ## `rail` [Section titled “rail”](#rail) `object` The activity bar's layout: folders that group app icons. Cowork writes it when the user drags icons into a folder; an agent may write it to group apps (a member is an app's view id, `workbench.view.cowork.app.<name>`). * `rail.folders` · `object[]` — Folders that group activity-bar icons (at most 12), made by dragging one icon onto another. * `rail.folders[].id` · `string` **required** *(pattern `^[a-z0-9][a-z0-9-]{0,31}$`)* — The folder's id. * `rail.folders[].title` · `string` **required** *(maxLength 40)* — The folder's name. * `rail.folders[].color` · `string` *(pattern `^#[0-9a-fA-F]{6}$`)* — Tile color, e.g. "#F0386B". * `rail.folders[].members` · `string[]` **required** *(minItems 1, maxItems 24)* — The activity-bar ids of the icons inside (at most 24). ## `wallpaper` [Section titled “wallpaper”](#wallpaper) `string` An image shown behind the workspace, as a path relative to the workspace root (png, jpg, webp, gif, avif or svg, up to 20 MB). The Manage Workspace picker copies the image to .cowork/data/wallpaper.\<ext>, which travels with a published workspace. ## `publishTo` [Section titled “publishTo”](#publishto) `string` *(pattern `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$`)* The team server slug that `cowork push` publishes to when the app has no home store and no --to is given. `cowork push --to <slug> --remember` sets it. ## `openWith` [Section titled “openWith”](#openwith) `{ [key: string]: string }` Which app opens a file type when two apps claim the same glob: glob -> app name. Without an entry the first switched-on app (in folder order) wins. ## `notifications` [Section titled “notifications”](#notifications) `object` Notification preferences for this workspace. * `notifications.muted` · `string[]` — App folders (e.g. ".cowork/apps/gmail") whose desktop notifications are dropped and whose badge leaves the activity-bar count. ## `deployments` [Section titled “deployments”](#deployments) `{ [key: string]: object }` Where each routine runs outside the IDE: routine id -> deployment. It records intent only (target, schedule, secret names), never secret values; `cowork deploy add` writes it. A webhook, schedule or notifications trigger never starts by itself from a checked-in or imported file: arming it needs the owner's consent on their machine. * `deployments.target` · `"cloudflare-worker" | "aws-lambda" | "gcloud-function" | "container"` **required** — Where the routine is built to run. * `deployments.mode` · `"local" | "deployed"` — "local" runs invocations on this machine, "deployed" on the target. Default "local". * `deployments.name` · `string` *(pattern `^[a-z][a-z0-9-]{0,53}$`)* — The cloud resource's name. Default "cowork-\<routine id>". * `deployments.webhook` · `boolean` — Expose an HTTP webhook that runs the routine. * `deployments.schedule` · `string` — A 5-field cron expression (minute granularity) that runs the routine. * `deployments.notifications` · `string[]` — Run the routine on this machine with each notification these apps raise, by app name (as in its cowork.json), or "\*" for every app's. Its args are the notification ({app, appTitle, title, body, tag, urgent, at}). A notification raised by a run that one started starts nothing. * `deployments.secrets` · `string[]` — Names of the secrets pushed to the target's secret store at deploy time. Names only; values stay in the secrets store. * `deployments.vars` · `{ [key: string]: string }` — Plain (non-secret) environment variables baked into the deployed routine: name -> value. * `deployments.hookToken` · `boolean` — Require a token header on webhook calls. The token is generated and kept as a secret. * `deployments.corsOrigin` · `string` — The CORS origin webhook responses allow: "*" or an origin like "<https://example.com>". Default "*". * `deployments.region` · `string` — Cloud region (aws-lambda and gcloud-function). * `deployments.roleArn` · `string` — An existing execution role (aws-lambda). Cowork never creates IAM roles. * `deployments.port` · `integer` — The port the container listens on (container). * `deployments.registry` · `string` — The image registry to push to (container). * `deployments.db` · `string` *(pattern `^[a-z][a-z0-9-]{0,53}$`)* — The D1 database name the routine's ctx.db uses (cloudflare-worker). Created at deploy time when missing; several routines may share one. ## `agents` [Section titled “agents”](#agents) `object` How the workspace's agent map finds instruction files, skills, subagents and commands. Each key replaces the built-in list; a key that is not well-formed is ignored. * `agents.instructions` · `string[]` — Instruction file names, e.g. \["CLAUDE.md", "AGENTS.md"]. * `agents.skill` · `string` — The file name that makes a folder a skill, e.g. "SKILL.md". * `agents.homes` · `string[]` — Agent home folders, e.g. \[".claude"]. * `agents.agents` · `string[]` — Folders holding subagent docs. * `agents.commands` · `string[]` — Folders holding slash-command docs. * `agents.skip` · `string[]` — Folder names never read. * `agents.stepWords` · `string[]` — Words that make a heading a step ("step 3") or a process. * `agents.notStepWords` · `string[]` — Words that make a heading never hold steps. * `agents.columns` · `object` — Header names recognized in stage tables. * `agents.columns.label` · `string[]` — Header names of the column holding a stage's label. * `agents.columns.id` · `string[]` — Header names of the column holding a stage's id. * `agents.columns.skills` · `string[]` — Header names of the column listing a stage's skills. * `agents.columns.detail` · `string[]` — Header names of the column holding a stage's description. * `agents.groupBy` · `string[]` — Skill metadata keys, in order, whose value says who a skill is for; the map groups skills by it. * `agents.routines` · `string[]` — Commands that run a routine followed by its id, e.g. \["cowork routine run"]; each one found in a doc adds a routine node. ## `projects` [Section titled “projects”](#projects) `object[]` Folders of this workspace listed as projects, pinned by hand or from "Add to Projects". * `projects[].name` · `string` — The project's display name. * `projects[].path` · `string` — Folder relative to this workspace's root. Defaults to the name. A path outside the workspace is dropped. ## `scan` [Section titled “scan”](#scan) `object` How the projects list discovers sub-folders that are Cowork workspaces themselves. * `scan.enabled` · `boolean` — false turns discovery off. Default true. * `scan.depth` · `integer` *(minimum 1, maximum 2)* — How many folder levels to look into. Default 2, at most 2. * `scan.include` · `string[]` — Globs; when set, only matching folders are kept. * `scan.exclude` · `string[]` — Globs of folders to drop. Wins over include. ## `notebook` [Section titled “notebook”](#notebook) `object` Settings for the Notebook editor. * `notebook.trackChanges` · `object` — Marks what changed in a document since a git revision. * `notebook.trackChanges.enabled` · `boolean` — Turn change tracking on. Default false. * `notebook.trackChanges.defaultDisplay` · `"off" | "git" | "ai" | "user" | "all"` — Whose changes are marked when a document opens. Default "off". * `notebook.trackChanges.git` · `object` * `notebook.trackChanges.git.baseRef` · `string` — The git revision changes are measured against. Default "HEAD". * `notebook.trackChanges.reviewExternalWrites` · `"never" | "when-dirty" | "always"` — When edits made outside the editor show up as marks. Default "when-dirty".