This is the full 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. Generated From the CLI's usage table, Cowork 0.2.90. `cowork --help` prints the top-level entries and `cowork <command> --help` a command's own, group verbs included. ## 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. Generated From the bundled extensions' `contributes.commands`, Cowork 0.2.90. `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. Generated From `cowork.schema.json`, Cowork 0.2.90. How the fields work together: [Apps and cowork.json](/docs/guides/manifest/). 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. Generated From `workspace.schema.json`, Cowork 0.2.90. 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". # CoworkCaptureResult ## Properties [Section titled “Properties”](#properties) ### bytes [Section titled “bytes”](#bytes) ```ts bytes: number; ``` *** ### durationMs [Section titled “durationMs”](#durationms) ```ts durationMs: number; ``` *** ### path [Section titled “path”](#path) ```ts path: string; ``` *** ### status [Section titled “status”](#status) ```ts status: "ok" | "aborted"; ``` # CoworkCaptureSession A recording in progress (`cowork.capture.record`). ## Properties [Section titled “Properties”](#properties) ### file [Section titled “file”](#file) ```ts file: string; ``` Workspace-relative path the recording lands at. *** ### id [Section titled “id”](#id) ```ts id: string; ``` *** ### onAudio [Section titled “onAudio”](#onaudio) ```ts onAudio: (cb) => () => void; ``` Live audio (`live: true`): 16 kHz mono Int16 frames of 20 ms. Returns the unsubscribe. #### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | | --------- | ------------------- | | `cb` | (`frame`) => `void` | #### Returns [Section titled “Returns”](#returns) () => `void` *** ### onStop [Section titled “onStop”](#onstop) ```ts onStop: (cb) => () => void; ``` Called once when the recording ends — Stop on the pill, the system, or `stop()`. Returns the unsubscribe. #### Parameters [Section titled “Parameters”](#parameters-1) | Parameter | Type | | --------- | -------------------- | | `cb` | (`result`) => `void` | #### Returns [Section titled “Returns”](#returns-1) () => `void` *** ### stop [Section titled “stop”](#stop) ```ts stop: () => Promise<CoworkCaptureResult>; ``` #### Returns [Section titled “Returns”](#returns-2) `Promise`<[`CoworkCaptureResult`](/docs/reference/sdk/interfaces/coworkcaptureresult/)> # CoworkConnection How a capability reaches its service — a sign-in, a pasted key, or either — named after the service it signs in to (`google`, shared by every Google capability) or after the capability. Never a token or a key. ## Properties [Section titled “Properties”](#properties) ### account? [Section titled “account?”](#account) ```ts account?: object; ``` Who signed in, when the service says. #### email [Section titled “email”](#email) ```ts email: string; ``` *** ### fields? [Section titled “fields?”](#fields) ```ts fields?: string[]; ``` A key's fields, where to get them, and one line on how. *** ### hint? [Section titled “hint?”](#hint) ```ts hint?: string; ``` *** ### kind [Section titled “kind”](#kind) ```ts kind: "oauth" | "key" | "both"; ``` `oauth`: a sign-in; `key`: fields the person pastes; `both`: either one. *** ### missingScopes [Section titled “missingScopes”](#missingscopes) ```ts missingScopes: string[]; ``` The access a sign-in still has to grant (all of it while not connected). *** ### name [Section titled “name”](#name) ```ts name: string; ``` *** ### state [Section titled “state”](#state) ```ts state: "connected" | "needs-scopes" | "not-connected"; ``` `needs-scopes`: signed in, without some access a capability that uses it needs. *** ### url? [Section titled “url?”](#url) ```ts url?: string; ``` *** ### usedBy [Section titled “usedBy”](#usedby) ```ts usedBy: object[]; ``` The capabilities that use it. #### name [Section titled “name”](#name-1) ```ts name: string; ``` #### title [Section titled “title”](#title) ```ts title: string; ``` # CoworkDbExecResult What `ctx.db(path).exec()` returns: the rows the statement changed and the last inserted rowid. ## Properties [Section titled “Properties”](#properties) ### changes [Section titled “changes”](#changes) ```ts changes: number; ``` *** ### lastInsertRowid [Section titled “lastInsertRowid”](#lastinsertrowid) ```ts lastInsertRowid: number; ``` # CoworkDbQueryResult What `ctx.db(path).query()` returns: the column names and each row's values in that order. ## Properties [Section titled “Properties”](#properties) ### columns [Section titled “columns”](#columns) ```ts columns: string[]; ``` *** ### rowCount [Section titled “rowCount”](#rowcount) ```ts rowCount: number; ``` *** ### rows [Section titled “rows”](#rows) ```ts rows: unknown[][]; ``` # CoworkDeployedRoutineContext The context a DEPLOYED routine receives (`cowork deploy` — it runs on a server, not on this computer): a subset of [CoworkRoutineContext](/docs/reference/sdk/interfaces/coworkroutinecontext/). There is no Cowork around it, so it has no sign-in, no account secrets, no data folder and no consent prompts — calling a member that is not here fails. ## Properties [Section titled “Properties”](#properties) ### auth [Section titled “auth”](#auth) ```ts auth: object; ``` Always signed out: `getState()` → `{ authenticated: false, user: null }`, `getIdToken()` → null. #### getIdToken [Section titled “getIdToken”](#getidtoken) ```ts getIdToken: () => Promise<string | null>; ``` The ID token to call an authenticated API with — null when signed out, or when the token is too close to expiring to be trusted. ##### Returns [Section titled “Returns”](#returns) `Promise`<`string` | `null`> #### getState [Section titled “getState”](#getstate) ```ts getState: () => Promise<{ authenticated: boolean; user: | { displayName: string | null; email: string | null; uid: string; } | null; }>; ``` `authenticated` and the account (`user`), or `user: null` when nobody is signed in. ##### Returns [Section titled “Returns”](#returns-1) `Promise`<{ `authenticated`: `boolean`; `user`: | { `displayName`: `string` | `null`; `email`: `string` | `null`; `uid`: `string`; } | `null`; }> *** ### db? [Section titled “db?”](#db) ```ts db?: (path) => object; ``` A SQLite database where the target has one: in a container, files under its data folder (`COWORK_DATA_DIR`, default /data) — synchronous; on a Cloudflare Worker deployed with a D1 database, that database whatever the path — `query`/`exec` return Promises (`await` them and the routine runs on both) and `transaction` throws. Absent elsewhere. #### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | | --------- | -------- | | `path` | `string` | #### Returns [Section titled “Returns”](#returns-2) `object` ##### exec [Section titled “exec”](#exec) ```ts exec: (sql, params?) => | CoworkDbExecResult | Promise<CoworkDbExecResult>; ``` ###### Parameters [Section titled “Parameters”](#parameters-1) | Parameter | Type | | --------- | ------------ | | `sql` | `string` | | `params?` | `unknown`\[] | ###### Returns [Section titled “Returns”](#returns-3) \| [`CoworkDbExecResult`](/docs/reference/sdk/interfaces/coworkdbexecresult/) | `Promise`<[`CoworkDbExecResult`](/docs/reference/sdk/interfaces/coworkdbexecresult/)> ##### query [Section titled “query”](#query) ```ts query: (sql, params?) => | CoworkDbQueryResult | Promise<CoworkDbQueryResult>; ``` ###### Parameters [Section titled “Parameters”](#parameters-2) | Parameter | Type | | --------- | ------------ | | `sql` | `string` | | `params?` | `unknown`\[] | ###### Returns [Section titled “Returns”](#returns-4) \| [`CoworkDbQueryResult`](/docs/reference/sdk/interfaces/coworkdbqueryresult/) | `Promise`<[`CoworkDbQueryResult`](/docs/reference/sdk/interfaces/coworkdbqueryresult/)> ##### transaction [Section titled “transaction”](#transaction) ```ts transaction: <T>(fn) => T; ``` ###### Type Parameters [Section titled “Type Parameters”](#type-parameters) | Type Parameter | | -------------- | | `T` | ###### Parameters [Section titled “Parameters”](#parameters-3) | Parameter | Type | | --------- | --------- | | `fn` | () => `T` | ###### Returns [Section titled “Returns”](#returns-5) `T` *** ### identity [Section titled “identity”](#identity) ```ts identity: CoworkIdentity; ``` An anonymous caller: `user` and `organization` null. *** ### log [Section titled “log”](#log) ```ts log: (...parts) => void; ``` A line in the platform's log (stderr). #### Parameters [Section titled “Parameters”](#parameters-4) | Parameter | Type | | ---------- | ------------ | | ...`parts` | `unknown`\[] | #### Returns [Section titled “Returns”](#returns-6) `void` *** ### secrets [Section titled “secrets”](#secrets) ```ts secrets: Pick<CoworkRoutineContext["secrets"], "get" | "list">; ``` The deployment's environment: `get(name)` is the variable of that name (the secrets you deployed with are set there); `list()` the names of those secrets. *** ### unchanged [Section titled “unchanged”](#unchanged) ```ts unchanged: () => void; ``` Does nothing: a deployed run keeps no run list to leave it out of. #### Returns [Section titled “Returns”](#returns-7) `void` *** ### workspaceRoot [Section titled “workspaceRoot”](#workspaceroot) ```ts workspaceRoot: string; ``` The deployment's `COWORK_WORKSPACE_ROOT`, else "/" — a deployed routine has no workspace. # CoworkDialog A dialog (`cowork.ui.dialog`): a question, a choice or a short form. ## Properties [Section titled “Properties”](#properties) ### alert? [Section titled “alert?”](#alert) ```ts alert?: object; ``` Why the last answer was refused — ask again with it. #### text? [Section titled “text?”](#text) ```ts text?: string; ``` #### title? [Section titled “title?”](#title) ```ts title?: string; ``` #### variant? [Section titled “variant?”](#variant) ```ts variant?: "default" | "destructive"; ``` *** ### buttons? [Section titled “buttons?”](#buttons) ```ts buttons?: CoworkDialogButton[]; ``` Default: one OK. *** ### detail? [Section titled “detail?”](#detail) ```ts detail?: string; ``` *** ### fields? [Section titled “fields?”](#fields) ```ts fields?: CoworkDialogField[]; ``` *** ### message [Section titled “message”](#message) ```ts message: string; ``` *** ### scope? [Section titled “scope?”](#scope) ```ts scope?: "window" | "pane"; ``` "window" (default): over the whole window. "pane": over this pane only — ONLY for a choice that changes how things look (a theme), never one that creates, changes or deletes data. *** ### type? [Section titled “type?”](#type) ```ts type?: "error" | "warning" | "info" | "none"; ``` The header icon — "none" for a plain question. Default "info". # CoworkDialogButton A [CoworkDialog](/docs/reference/sdk/interfaces/coworkdialog/) button. ## Properties [Section titled “Properties”](#properties) ### card? [Section titled “card?”](#card) ```ts card?: boolean; ``` An option row in the body (label over `sublabel`) instead of a footer button. *** ### destructive? [Section titled “destructive?”](#destructive) ```ts destructive?: boolean; ``` *** ### label [Section titled “label”](#label) ```ts label: string; ``` *** ### primary? [Section titled “primary?”](#primary) ```ts primary?: boolean; ``` *** ### role [Section titled “role”](#role) ```ts role: "submit" | "cancel"; ``` *** ### sublabel? [Section titled “sublabel?”](#sublabel) ```ts sublabel?: string; ``` *** ### values? [Section titled “values?”](#values) ```ts values?: Record<string, string | number | boolean>; ``` Merged into the answer's values when this button submits. # CoworkDialogField One field of a [CoworkDialog](/docs/reference/sdk/interfaces/coworkdialog/). Its answer is `values[id]`: a string for `text`, `textarea`, `select` and `color`; a number for `number` ("" while empty); a boolean for `checkbox`; a `section` has none. ## Properties [Section titled “Properties”](#properties) ### id [Section titled “id”](#id) ```ts id: string; ``` *** ### label? [Section titled “label?”](#label) ```ts label?: string; ``` *** ### monospace? [Section titled “monospace?”](#monospace) ```ts monospace?: boolean; ``` *** ### options? [Section titled “options?”](#options) ```ts options?: CoworkDialogOption[]; ``` `select`: the choices. `color`: the swatches, each `value` a hex color (`#rgb` … `#rrggbbaa`) or "" for no color — any other value is not drawn. *** ### placeholder? [Section titled “placeholder?”](#placeholder) ```ts placeholder?: string; ``` For a `section`: its body text. *** ### type [Section titled “type”](#type) ```ts type: | "number" | "text" | "textarea" | "checkbox" | "select" | "section" | "color"; ``` *** ### value? [Section titled “value?”](#value) ```ts value?: string | number | boolean; ``` The starting answer. Default: "" — `false` for a checkbox, the first option for a select. # CoworkDialogOption A `select` choice or a `color` swatch: `value` is what `values[id]` gets, `label` what the person sees (a swatch's accessible name). ## Properties [Section titled “Properties”](#properties) ### label [Section titled “label”](#label) ```ts label: string; ``` *** ### value [Section titled “value”](#value) ```ts value: string; ``` # CoworkIdentity Who is signed in, and the organization they belong to. ## Properties [Section titled “Properties”](#properties) ### organization [Section titled “organization”](#organization) ```ts organization: | { id: string; name: string; slug: string; } | null; ``` `name` is the slug until Cowork has heard back from the organization, early in a session. *** ### user [Section titled “user”](#user) ```ts user: | { email: string | null; uid: string; } | null; ``` # CoworkJournalEntry ## Properties [Section titled “Properties”](#properties) ### appliedAt? [Section titled “appliedAt?”](#appliedat) ```ts appliedAt?: number; ``` *** ### createdAt [Section titled “createdAt”](#createdat) ```ts createdAt: number; ``` *** ### id [Section titled “id”](#id) ```ts id: number; ``` *** ### kind [Section titled “kind”](#kind) ```ts kind: "learning" | "error" | "note"; ``` *** ### skill [Section titled “skill”](#skill) ```ts skill: string; ``` *** ### source? [Section titled “source?”](#source) ```ts source?: string; ``` *** ### status [Section titled “status”](#status) ```ts status: "open" | "applied"; ``` *** ### text [Section titled “text”](#text) ```ts text: string; ``` # CoworkNotification A notification an app raised (`cowork.notify` in a widget, `ctx.notify` in a routine): what `cowork.onNotification` hands a widget, and the args of a routine started by its `notifications` trigger (`deployments` in .cowork/workspace.json). `app` is the name (as in its cowork.json) of the app it shows as — a capability's shows as the app that uses it — and `appTitle` that app's title; `at` is when Cowork accepted it (epoch ms). ## Properties [Section titled “Properties”](#properties) ### app [Section titled “app”](#app) ```ts app: string; ``` *** ### appTitle [Section titled “appTitle”](#apptitle) ```ts appTitle: string; ``` *** ### at [Section titled “at”](#at) ```ts at: number; ``` *** ### body? [Section titled “body?”](#body) ```ts body?: string; ``` *** ### tag? [Section titled “tag?”](#tag) ```ts tag?: string; ``` *** ### title [Section titled “title”](#title) ```ts title: string; ``` *** ### urgent? [Section titled “urgent?”](#urgent) ```ts urgent?: boolean; ``` # CoworkOpenedFile ## Properties [Section titled “Properties”](#properties) ### basename [Section titled “basename”](#basename) ```ts basename: string; ``` *** ### ext [Section titled “ext”](#ext) ```ts ext: string; ``` *** ### matchedGlob [Section titled “matchedGlob”](#matchedglob) ```ts matchedGlob: string; ``` *** ### path [Section titled “path”](#path) ```ts path: string; ``` *** ### schemaVersion [Section titled “schemaVersion”](#schemaversion) ```ts schemaVersion: number; ``` *** ### workspaceRelativePath [Section titled “workspaceRelativePath”](#workspacerelativepath) ```ts workspaceRelativePath: string; ``` # CoworkQueueEntry One card of a task's thread after its request; `to` null is a note to nobody. ## Properties [Section titled “Properties”](#properties) ### body [Section titled “body”](#body) ```ts body: string; ``` *** ### createdAt [Section titled “createdAt”](#createdat) ```ts createdAt: number; ``` *** ### files [Section titled “files”](#files) ```ts files: CoworkQueueFile[]; ``` *** ### from [Section titled “from”](#from) ```ts from: string; ``` *** ### id [Section titled “id”](#id) ```ts id: number; ``` *** ### ref [Section titled “ref”](#ref) ```ts ref: string | null; ``` *** ### to [Section titled “to”](#to) ```ts to: string | null; ``` # CoworkQueueFile `path` is workspace-relative, `absPath` absolute. ## Properties [Section titled “Properties”](#properties) ### absPath [Section titled “absPath”](#abspath) ```ts absPath: string; ``` *** ### bytes [Section titled “bytes”](#bytes) ```ts bytes: number; ``` *** ### name [Section titled “name”](#name) ```ts name: string; ``` *** ### path [Section titled “path”](#path) ```ts path: string; ``` # CoworkQueueInfo ## Properties [Section titled “Properties”](#properties) ### counts [Section titled “counts”](#counts) ```ts counts: Record<CoworkTaskView, number>; ``` *** ### createdAt [Section titled “createdAt”](#createdat) ```ts createdAt: number; ``` *** ### leaseS [Section titled “leaseS”](#leases) ```ts leaseS: number; ``` *** ### maxAttempts [Section titled “maxAttempts”](#maxattempts) ```ts maxAttempts: number; ``` *** ### name [Section titled “name”](#name) ```ts name: string; ``` *** ### review [Section titled “review”](#review) ```ts review: boolean; ``` # CoworkQueueReceiver An agent seen taking tasks (`listening`: within the last 10 minutes). ## Properties [Section titled “Properties”](#properties) ### listening [Section titled “listening”](#listening) ```ts listening: boolean; ``` *** ### name [Section titled “name”](#name) ```ts name: string; ``` *** ### queue [Section titled “queue”](#queue) ```ts queue: string; ``` *** ### seenAt [Section titled “seenAt”](#seenat) ```ts seenAt: number; ``` # CoworkQueueTask A task. A list row carries the request as a preview and no `entries`; `receipt` comes only with `receive`. ## Properties [Section titled “Properties”](#properties) ### attempts [Section titled “attempts”](#attempts) ```ts attempts: number; ``` *** ### body [Section titled “body”](#body) ```ts body: string; ``` *** ### bodyBytes [Section titled “bodyBytes”](#bodybytes) ```ts bodyBytes: number; ``` *** ### bodyTruncated? [Section titled “bodyTruncated?”](#bodytruncated) ```ts bodyTruncated?: true; ``` *** ### createdAt [Section titled “createdAt”](#createdat) ```ts createdAt: number; ``` *** ### entries? [Section titled “entries?”](#entries) ```ts entries?: CoworkQueueEntry[]; ``` *** ### entryCount [Section titled “entryCount”](#entrycount) ```ts entryCount: number; ``` *** ### files [Section titled “files”](#files) ```ts files: CoworkQueueFile[]; ``` *** ### from [Section titled “from”](#from) ```ts from: string; ``` *** ### heldBy [Section titled “heldBy”](#heldby) ```ts heldBy: string | null; ``` *** ### id [Section titled “id”](#id) ```ts id: number; ``` *** ### last [Section titled “last”](#last) ```ts last: | { at: number; from: string; preview: string; } | null; ``` *** ### leasedAt? [Section titled “leasedAt?”](#leasedat) ```ts leasedAt?: number; ``` *** ### leaseUntil? [Section titled “leaseUntil?”](#leaseuntil) ```ts leaseUntil?: number; ``` *** ### maxAttempts [Section titled “maxAttempts”](#maxattempts) ```ts maxAttempts: number; ``` *** ### meta [Section titled “meta”](#meta) ```ts meta: Record<string, unknown>; ``` *** ### queue [Section titled “queue”](#queue) ```ts queue: string; ``` *** ### receipt? [Section titled “receipt?”](#receipt) ```ts receipt?: string; ``` *** ### ref [Section titled “ref”](#ref) ```ts ref: string | null; ``` *** ### review [Section titled “review”](#review) ```ts review: boolean; ``` *** ### status [Section titled “status”](#status) ```ts status: CoworkTaskStatus; ``` *** ### subject [Section titled “subject”](#subject) ```ts subject: string; ``` *** ### to [Section titled “to”](#to) ```ts to: string; ``` *** ### updatedAt [Section titled “updatedAt”](#updatedat) ```ts updatedAt: number; ``` *** ### visibleAt [Section titled “visibleAt”](#visibleat) ```ts visibleAt: number; ``` # CoworkRoutineContext The context a routine's default export receives: `export default async (args, ctx: CoworkRoutineContext) => result`. A deployed routine (`cowork deploy`) gets [CoworkDeployedRoutineContext](/docs/reference/sdk/interfaces/coworkdeployedroutinecontext/) instead. ## Properties [Section titled “Properties”](#properties) ### auth [Section titled “auth”](#auth) ```ts auth: object; ``` Who is signed into Cowork, and their short-lived Firebase ID token. Read on every call (the token rotates hourly — never keep one from the start of a long run). #### getIdToken [Section titled “getIdToken”](#getidtoken) ```ts getIdToken: () => Promise<string | null>; ``` The ID token to call an authenticated API with — null when signed out, or when the token is too close to expiring to be trusted. ##### Returns [Section titled “Returns”](#returns) `Promise`<`string` | `null`> #### getState [Section titled “getState”](#getstate) ```ts getState: () => Promise<{ authenticated: boolean; user: | { displayName: string | null; email: string | null; uid: string; } | null; }>; ``` `authenticated` and the account (`user`), or `user: null` when nobody is signed in. ##### Returns [Section titled “Returns”](#returns-1) `Promise`<{ `authenticated`: `boolean`; `user`: | { `displayName`: `string` | `null`; `email`: `string` | `null`; `uid`: `string`; } | `null`; }> *** ### badge [Section titled “badge”](#badge) ```ts badge: (count) => Promise<void>; ``` This routine's app's count on the OS badge — as `cowork.badge`: latest wins, 0 clears. Report it on every poll: `await ctx.badge(unread)`. #### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | | --------- | -------- | | `count` | `number` | #### Returns [Section titled “Returns”](#returns-2) `Promise`<`void`> *** ### browser [Section titled “browser”](#browser) ```ts browser: object; ``` The desktop app's built-in browser, step by step: `snapshot()` lists the page's actionable elements with refs (`ref_3 button "Sign in"`), then `click` / `fill` / `type` / `waitFor` act on a ref or a CSS selector. Pages keep the user's own sign-ins, so `evaluate` can call a site's API with its cookies. Only in the desktop app: without it every call throws BrowserUnavailableError (`status()` is the cheap check); a failed action throws with the browser's message. Calls without a `tabIndex` act on the active tab. #### back [Section titled “back”](#back) ```ts back: () => Promise<{ ok: boolean; url: string; }>; ``` ##### Returns [Section titled “Returns”](#returns-3) `Promise`<{ `ok`: `boolean`; `url`: `string`; }> #### click [Section titled “click”](#click) ```ts click: (refOrSelector) => Promise<{ found: boolean; label?: string; role?: string; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-1) | Parameter | Type | | --------------- | -------- | | `refOrSelector` | `string` | ##### Returns [Section titled “Returns”](#returns-4) `Promise`<{ `found`: `boolean`; `label?`: `string`; `role?`: `string`; }> #### evaluate [Section titled “evaluate”](#evaluate) ```ts evaluate: <T>(js, opts?) => Promise<T>; ``` Run JS in a tab and get its FULL value back (any size; JSON-serializable). `js` is the body of an async function — use `return`. `fetch` inside it sends the page's cookies, so open the tab on the origin you mean to call first. `tabIndex` (from `tabs.list()`) runs in that tab WITHOUT activating it — resolve it each run, indices shift when tabs close. `world`: "isolated" (default — same DOM and cookies, separate globals, the page cannot see the script) or "main" (the site's own globals). ##### Type Parameters [Section titled “Type Parameters”](#type-parameters) | Type Parameter | Default type | | -------------- | ------------ | | `T` | `unknown` | ##### Parameters [Section titled “Parameters”](#parameters-2) | Parameter | Type | | ---------------- | -------------------------------------------------------------- | | `js` | `string` | | `opts?` | { `tabIndex?`: `number`; `world?`: `"isolated"` \| `"main"`; } | | `opts.tabIndex?` | `number` | | `opts.world?` | `"isolated"` \| `"main"` | ##### Returns [Section titled “Returns”](#returns-5) `Promise`<`T`> #### fill [Section titled “fill”](#fill) ```ts fill: (refOrSelector, value, submit?) => Promise<{ found: boolean; ok?: boolean; value?: string; }>; ``` Set a form field (text / select / checkbox / radio) the way a user would, so framework-controlled inputs keep the value; `submit` presses Enter after. ##### Parameters [Section titled “Parameters”](#parameters-3) | Parameter | Type | | --------------- | --------- | | `refOrSelector` | `string` | | `value` | `string` | | `submit?` | `boolean` | ##### Returns [Section titled “Returns”](#returns-6) `Promise`<{ `found`: `boolean`; `ok?`: `boolean`; `value?`: `string`; }> #### find [Section titled “find”](#find) ```ts find: (query, limit?) => Promise<{ matches: object[]; query: string; }>; ``` Elements whose label matches `query`, with their refs (`limit`: default 10, at most 50). ##### Parameters [Section titled “Parameters”](#parameters-4) | Parameter | Type | | --------- | -------- | | `query` | `string` | | `limit?` | `number` | ##### Returns [Section titled “Returns”](#returns-7) `Promise`<{ `matches`: `object`\[]; `query`: `string`; }> #### forward [Section titled “forward”](#forward) ```ts forward: () => Promise<{ ok: boolean; url: string; }>; ``` ##### Returns [Section titled “Returns”](#returns-8) `Promise`<{ `ok`: `boolean`; `url`: `string`; }> #### hover [Section titled “hover”](#hover) ```ts hover: (refOrSelector) => Promise<{ found: boolean; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-5) | Parameter | Type | | --------------- | -------- | | `refOrSelector` | `string` | ##### Returns [Section titled “Returns”](#returns-9) `Promise`<{ `found`: `boolean`; }> #### inject [Section titled “inject”](#inject) ```ts inject: (spec) => Promise<{ id: string; ok: boolean; persist: boolean; url: string; value: unknown; world: string; }>; ``` Apply CSS and/or JS to the page. `world` as in `evaluate`; `persist` re-applies after every navigation (an app that re-renders on each click); the same `id` replaces an earlier injection. `value` is the script's completion value, cut at 4096 characters — use `evaluate` for anything long. `tabIndex` targets a tab without activating it. ##### Parameters [Section titled “Parameters”](#parameters-6) | Parameter | Type | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `spec` | { `css?`: `string`; `id?`: `string`; `js?`: `string`; `persist?`: `boolean`; `tabIndex?`: `number`; `world?`: `"isolated"` \| `"main"`; } | | `spec.css?` | `string` | | `spec.id?` | `string` | | `spec.js?` | `string` | | `spec.persist?` | `boolean` | | `spec.tabIndex?` | `number` | | `spec.world?` | `"isolated"` \| `"main"` | ##### Returns [Section titled “Returns”](#returns-10) `Promise`<{ `id`: `string`; `ok`: `boolean`; `persist`: `boolean`; `url`: `string`; `value`: `unknown`; `world`: `string`; }> #### injections [Section titled “injections”](#injections) ```ts injections: () => Promise<{ injections: object[]; }>; ``` ##### Returns [Section titled “Returns”](#returns-11) `Promise`<{ `injections`: `object`\[]; }> #### navigate [Section titled “navigate”](#navigate) ```ts navigate: (url) => Promise<{ ok: boolean; url: string; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-7) | Parameter | Type | | --------- | -------- | | `url` | `string` | ##### Returns [Section titled “Returns”](#returns-12) `Promise`<{ `ok`: `boolean`; `url`: `string`; }> #### open [Section titled “open”](#open) ```ts open: (url?) => Promise<{ ok: boolean; url: string; }>; ``` A URL opens in a new tab; no URL shows the browser (its last-active tab, or a new one). ##### Parameters [Section titled “Parameters”](#parameters-8) | Parameter | Type | | --------- | -------- | | `url?` | `string` | ##### Returns [Section titled “Returns”](#returns-13) `Promise`<{ `ok`: `boolean`; `url`: `string`; }> #### press [Section titled “press”](#press) ```ts press: (key) => Promise<{ key: string; ok: boolean; }>; ``` "Return", "Tab", "cmd+a", … ##### Parameters [Section titled “Parameters”](#parameters-9) | Parameter | Type | | --------- | -------- | | `key` | `string` | ##### Returns [Section titled “Returns”](#returns-14) `Promise`<{ `key`: `string`; `ok`: `boolean`; }> #### read [Section titled “read”](#read) ```ts read: (maxChars?) => Promise<{ text: string; title: string; url: string; }>; ``` The page's visible text (`maxChars`: default 20000, at most 100000). ##### Parameters [Section titled “Parameters”](#parameters-10) | Parameter | Type | | ----------- | -------- | | `maxChars?` | `number` | ##### Returns [Section titled “Returns”](#returns-15) `Promise`<{ `text`: `string`; `title`: `string`; `url`: `string`; }> #### reload [Section titled “reload”](#reload) ```ts reload: () => Promise<{ ok: boolean; url: string; }>; ``` ##### Returns [Section titled “Returns”](#returns-16) `Promise`<{ `ok`: `boolean`; `url`: `string`; }> #### screenshot [Section titled “screenshot”](#screenshot) ```ts screenshot: () => Promise<{ height: number; path: string; width: number; }>; ``` A JPEG of the page, written to a temporary file — its path. ##### Returns [Section titled “Returns”](#returns-17) `Promise`<{ `height`: `number`; `path`: `string`; `width`: `number`; }> #### scroll [Section titled “scroll”](#scroll) ```ts scroll: (opts?) => Promise<{ scrolledTo?: string; scrollY?: number; }>; ``` Scroll an element into view (`ref`), or the page by `dy` pixels (default 600). ##### Parameters [Section titled “Parameters”](#parameters-11) | Parameter | Type | | ----------- | -------------------------------------- | | `opts?` | { `dy?`: `number`; `ref?`: `string`; } | | `opts.dy?` | `number` | | `opts.ref?` | `string` | ##### Returns [Section titled “Returns”](#returns-18) `Promise`<{ `scrolledTo?`: `string`; `scrollY?`: `number`; }> #### snapshot [Section titled “snapshot”](#snapshot) ```ts snapshot: (maxNodes?) => Promise<{ elements: string[]; title: string; truncated: boolean; url: string; }>; ``` One line per actionable element (`ref_3 button "Sign in"`) — the refs every other call targets. `truncated` when the page had more than `maxNodes`. ##### Parameters [Section titled “Parameters”](#parameters-12) | Parameter | Type | | ----------- | -------- | | `maxNodes?` | `number` | ##### Returns [Section titled “Returns”](#returns-19) `Promise`<{ `elements`: `string`\[]; `title`: `string`; `truncated`: `boolean`; `url`: `string`; }> #### status [Section titled “status”](#status) ```ts status: () => Promise<{ host?: "pane" | "tabs"; open: boolean; shell?: string; tabs: object[]; }>; ``` Whether the browser has tabs open, and which. ##### Returns [Section titled “Returns”](#returns-20) `Promise`<{ `host?`: `"pane"` | `"tabs"`; `open`: `boolean`; `shell?`: `string`; `tabs`: `object`\[]; }> #### tabs [Section titled “tabs”](#tabs) ```ts tabs: object; ``` ##### tabs.activate [Section titled “tabs.activate”](#tabsactivate) ```ts activate: (index) => Promise<{ active: number; }>; ``` ###### Parameters [Section titled “Parameters”](#parameters-13) | Parameter | Type | | --------- | -------- | | `index` | `number` | ###### Returns [Section titled “Returns”](#returns-21) `Promise`<{ `active`: `number`; }> ##### tabs.close [Section titled “tabs.close”](#tabsclose) ```ts close: (index) => Promise<{ count: number; }>; ``` ###### Parameters [Section titled “Parameters”](#parameters-14) | Parameter | Type | | --------- | -------- | | `index` | `number` | ###### Returns [Section titled “Returns”](#returns-22) `Promise`<{ `count`: `number`; }> ##### tabs.list [Section titled “tabs.list”](#tabslist) ```ts list: () => Promise<{ tabs: object[]; }>; ``` ###### Returns [Section titled “Returns”](#returns-23) `Promise`<{ `tabs`: `object`\[]; }> ##### tabs.new [Section titled “tabs.new”](#tabsnew) ```ts new: (url?, opts?) => Promise<{ count: number; existing?: boolean; id?: string; index: number; }>; ``` A new tab (on `url`, if given). `opts.id` asks for that tab: when it is still open it is answered (`existing: true`) instead of opening a second one. ###### Parameters [Section titled “Parameters”](#parameters-15) | Parameter | Type | | ---------- | -------------------- | | `url?` | `string` | | `opts?` | { `id?`: `string`; } | | `opts.id?` | `string` | ###### Returns [Section titled “Returns”](#returns-24) `Promise`<{ `count`: `number`; `existing?`: `boolean`; `id?`: `string`; `index`: `number`; }> #### ~~toggle~~ [Section titled “toggle”](#toggle) ```ts toggle: (show?) => Promise<{ open: boolean; }>; ``` Deprecated Use `open()`: the browser is never hidden. `toggle(true)` shows it like `open()`; `toggle(false)` does nothing. ##### Parameters [Section titled “Parameters”](#parameters-16) | Parameter | Type | | --------- | --------- | | `show?` | `boolean` | ##### Returns [Section titled “Returns”](#returns-25) `Promise`<{ `open`: `boolean`; }> #### type [Section titled “type”](#type) ```ts type: (text, refOrSelector?, submit?) => Promise<{ found: boolean; ok: boolean; }>; ``` Type text into the element (or the focused one); `submit` presses Enter after. ##### Parameters [Section titled “Parameters”](#parameters-17) | Parameter | Type | | ---------------- | --------- | | `text` | `string` | | `refOrSelector?` | `string` | | `submit?` | `boolean` | ##### Returns [Section titled “Returns”](#returns-26) `Promise`<{ `found`: `boolean`; `ok`: `boolean`; }> #### uninject [Section titled “uninject”](#uninject) ```ts uninject: (id?) => Promise<{ ok: boolean; removed: string[]; }>; ``` Remove one injection by id, or all of them. CSS goes; a script that already ran cannot be un-run — its effects last until the page reloads. ##### Parameters [Section titled “Parameters”](#parameters-18) | Parameter | Type | | --------- | -------- | | `id?` | `string` | ##### Returns [Section titled “Returns”](#returns-27) `Promise`<{ `ok`: `boolean`; `removed`: `string`\[]; }> #### waitFor [Section titled “waitFor”](#waitfor) ```ts waitFor: (opts) => Promise<{ error?: string; ok: boolean; waitedMs?: number; }>; ``` Wait until `text` appears or an element (`ref`) exists — the deterministic gate after a click that loads something (`timeoutMs`: default 10000, at most 20000). ##### Parameters [Section titled “Parameters”](#parameters-19) | Parameter | Type | | ----------------- | ---------------------------------------------------------------- | | `opts` | { `ref?`: `string`; `text?`: `string`; `timeoutMs?`: `number`; } | | `opts.ref?` | `string` | | `opts.text?` | `string` | | `opts.timeoutMs?` | `number` | ##### Returns [Section titled “Returns”](#returns-28) `Promise`<{ `error?`: `string`; `ok`: `boolean`; `waitedMs?`: `number`; }> *** ### command [Section titled “command”](#command) ```ts command: object; ``` Run a terminal command, gated by a consent MODAL. Cowork asks the user to approve (a native modal: Allow once / Always allow in this workspace / Cancel), then runs `command` in the workspace folder through a LOGIN shell (PATH like a terminal, so `gcloud`/`brew` bins resolve and `gcloud auth login` opens the default browser itself) and resolves `{ code, stdout, stderr }`. The modal text is yours to set: `title` (heading) + `description` (one line framing the ask); the command is always shown under "Command:". Consent is keyed on `value` — a STABLE label, so a variable arg (e.g. a VM IP) won't defeat a prior "Always allow"; defaults to the command. Grants live in Cowork's per-workspace consent file, on this computer, NEVER in the workspace — every machine approves the first time. A Deny (or an unanswered prompt) rejects (ConsentRequired). For a long-lived interactive process use a stream instead. #### run [Section titled “run”](#run) ```ts run: (command, opts?) => Promise<{ code: number; stderr: string; stdout: string; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-20) | Parameter | Type | | ------------------- | --------------------------------------------------------------------------------------------- | | `command` | `string` | | `opts?` | { `description?`: `string`; `timeoutMs?`: `number`; `title?`: `string`; `value?`: `string`; } | | `opts.description?` | `string` | | `opts.timeoutMs?` | `number` | | `opts.title?` | `string` | | `opts.value?` | `string` | ##### Returns [Section titled “Returns”](#returns-29) `Promise`<{ `code`: `number`; `stderr`: `string`; `stdout`: `string`; }> *** ### consent [Section titled “consent”](#consent) ```ts consent: object; ``` Consent-ONLY request — the same native modal as `command.run`, but nothing executes: the routine gets `{ granted, ticket? }` to attach to a consent-gated call (e.g. hand it to the widget for `stream.open`). `value` is the stable consent label ("Always allow" persists it to USER DATA); `command` is what the modal displays under "Command:". Deny or an unanswered prompt → `{ granted: false }` — never a throw. With no Cowork window open to ask, `reason` says so and names the `cowork consent allow` command that approves it instead. #### request [Section titled “request”](#request) ```ts request: (opts) => Promise<{ granted: boolean; reason?: string; ticket?: string; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-21) | Parameter | Type | | ------------------- | ------------------------------------------------------------------------------------------ | | `opts` | { `command?`: `string`; `description?`: `string`; `title?`: `string`; `value`: `string`; } | | `opts.command?` | `string` | | `opts.description?` | `string` | | `opts.title?` | `string` | | `opts.value` | `string` | ##### Returns [Section titled “Returns”](#returns-30) `Promise`<{ `granted`: `boolean`; `reason?`: `string`; `ticket?`: `string`; }> *** ### credential [Section titled “credential”](#credential) ```ts credential: object; ``` This capability's connection — the one its cowork.json `"credential"` declares: a sign-in, a pasted key, or either. A capability declaring none gets NotConnected saying so; an app's routine, NotGranted (it calls its capability's routines with ctx.routines.run). A call that needs it connected throws NotConnected naming where the person connects it. #### disconnect [Section titled “disconnect”](#disconnect) ```ts disconnect: () => Promise<{ removed: string[]; revoked: boolean; }>; ``` Forget it, revoked at the service when it can be — for every capability that shares it. ##### Returns [Section titled “Returns”](#returns-31) `Promise`<{ `removed`: `string`\[]; `revoked`: `boolean`; }> #### fetch [Section titled “fetch”](#fetch) ```ts fetch: (url, init?) => Promise<Response>; ``` An authenticated fetch to the service. A sign-in sends its bearer — a 401 refreshes the token once and retries; only to the service's API hosts, https. A key sends nothing by itself: put its header in `init.headers`. A 429 or 5xx waits and retries `retries` times (0–4, default 2; 0 for a write that must never repeat); `json` is sent as the JSON body. Resolves with the Response whatever its status; throws NotConnected while it is not connected (or the service refused the saved sign-in) and BadRequest for a URL a sign-in may not be sent to. ##### Parameters [Section titled “Parameters”](#parameters-22) | Parameter | Type | | --------- | ------------------------ | | `url` | `string` \| `URL` | | `init?` | `RequestInit` & `object` | ##### Returns [Section titled “Returns”](#returns-32) `Promise`<`Response`> #### fields [Section titled “fields”](#fields) ```ts fields: () => Record<string, string>; ``` A key's fields by name, e.g. `{ token }` — NotConnected naming the ones not pasted yet. ##### Returns [Section titled “Returns”](#returns-33) `Record`<`string`, `string`> #### start [Section titled “start”](#start) ```ts start: () => Promise<{ authUrl: string; }>; ``` Begin a sign-in: the consent URL to open in a browser. A key is pasted in Manage Workspace, never through a routine. ##### Returns [Section titled “Returns”](#returns-34) `Promise`<{ `authUrl`: `string`; }> #### status [Section titled “status”](#status-1) ```ts status: () => object; ``` Whether it is connected with everything this capability asks for, and as whom — never a token. ##### Returns [Section titled “Returns”](#returns-35) `object` ###### account? [Section titled “account?”](#account) ```ts account?: object; ``` ###### account.email [Section titled “account.email”](#accountemail) ```ts email: string; ``` ###### connected [Section titled “connected”](#connected) ```ts connected: boolean; ``` #### token [Section titled “token”](#token) ```ts token: (opts?) => Promise<string>; ``` A sign-in's access token, refreshed when needed (`{ force: true }` after the service answered 401); a key's first field. Prefer `fetch`. ##### Parameters [Section titled “Parameters”](#parameters-23) | Parameter | Type | | ------------- | ------------------------ | | `opts?` | { `force?`: `boolean`; } | | `opts.force?` | `boolean` | ##### Returns [Section titled “Returns”](#returns-36) `Promise`<`string`> *** ### data [Section titled “data”](#data) ```ts data: object; ``` The app's own data folder, `.cowork/data/<app name>/` (it travels with the workspace): names are paths inside it ("Gmail.json", "records/acme.json"). Keep user data here, not in the app folder, so the app stays clean and updates itself. A routine outside an app (`.cowork/apps/<name>/`) has no data folder: `dir` is "" and every call that touches a file throws (`fileName`, which only builds a name, still works). #### dir [Section titled “dir”](#dir) ```ts readonly dir: string; ``` Workspace-relative: `.cowork/data/<app>`. #### fileName [Section titled “fileName”](#filename) ```ts fileName: (title, fallback) => string; ``` A title as a safe file name (≤ 60 characters), or `fallback`. ##### Parameters [Section titled “Parameters”](#parameters-24) | Parameter | Type | | ---------- | --------- | | `title` | `unknown` | | `fallback` | `string` | ##### Returns [Section titled “Returns”](#returns-37) `string` #### list [Section titled “list”](#list) ```ts list: (subdir?) => string[]; ``` The files in the folder or a subfolder, sorted (dot files left out). ##### Parameters [Section titled “Parameters”](#parameters-25) | Parameter | Type | | --------- | -------- | | `subdir?` | `string` | ##### Returns [Section titled “Returns”](#returns-38) `string`\[] #### path [Section titled “path”](#path) ```ts path: (name) => string; ``` A name's workspace-relative path — what `cowork.openFile` and fileRouting take. ##### Parameters [Section titled “Parameters”](#parameters-26) | Parameter | Type | | --------- | -------- | | `name` | `string` | ##### Returns [Section titled “Returns”](#returns-39) `string` #### read [Section titled “read”](#read-1) ```ts read: <T>(name, fallback?) => T; ``` A JSON file's value; `fallback` when it is missing or not JSON. ##### Type Parameters [Section titled “Type Parameters”](#type-parameters-1) | Type Parameter | Default type | | -------------- | ------------ | | `T` | `unknown` | ##### Parameters [Section titled “Parameters”](#parameters-27) | Parameter | Type | | ----------- | -------- | | `name` | `string` | | `fallback?` | `T` | ##### Returns [Section titled “Returns”](#returns-40) `T` #### remove [Section titled “remove”](#remove) ```ts remove: (name) => boolean; ``` Delete a file; false when there was none. ##### Parameters [Section titled “Parameters”](#parameters-28) | Parameter | Type | | --------- | -------- | | `name` | `string` | ##### Returns [Section titled “Returns”](#returns-41) `boolean` #### write [Section titled “write”](#write) ```ts write: (name, value) => string; ``` Write a value as JSON (atomically) → its workspace-relative path. ##### Parameters [Section titled “Parameters”](#parameters-29) | Parameter | Type | | --------- | --------- | | `name` | `string` | | `value` | `unknown` | ##### Returns [Section titled “Returns”](#returns-42) `string` *** ### db [Section titled “db”](#db) ```ts db: (path) => object; ``` Open a SQLite database file in the workspace (`path` workspace-relative; the file and its folder are created when missing) — directly in this routine's process, in WAL mode with a busy timeout, so it is safe beside Cowork's own readers. Handles close when the run ends. #### Parameters [Section titled “Parameters”](#parameters-30) | Parameter | Type | | --------- | -------- | | `path` | `string` | #### Returns [Section titled “Returns”](#returns-43) ##### exec [Section titled “exec”](#exec) ```ts exec: (sql, params?) => CoworkDbExecResult; ``` One write or DDL statement — a string holding a second statement throws. ###### Parameters [Section titled “Parameters”](#parameters-31) | Parameter | Type | | --------- | ------------ | | `sql` | `string` | | `params?` | `unknown`\[] | ###### Returns [Section titled “Returns”](#returns-44) [`CoworkDbExecResult`](/docs/reference/sdk/interfaces/coworkdbexecresult/) ##### query [Section titled “query”](#query) ```ts query: (sql, params?) => CoworkDbQueryResult; ``` One statement's rows. Meant for reads: it does not refuse a write, but `exec` is the call for writes. ###### Parameters [Section titled “Parameters”](#parameters-32) | Parameter | Type | | --------- | ------------ | | `sql` | `string` | | `params?` | `unknown`\[] | ###### Returns [Section titled “Returns”](#returns-45) [`CoworkDbQueryResult`](/docs/reference/sdk/interfaces/coworkdbqueryresult/) ##### transaction [Section titled “transaction”](#transaction) ```ts transaction: <T>(fn) => T; ``` Runs `fn` atomically: a throw rolls everything back. SYNC-ONLY — an async callback (or one returning a promise) throws, as an awaited gap would hold the transaction open across unrelated work. ###### Type Parameters [Section titled “Type Parameters”](#type-parameters-2) | Type Parameter | | -------------- | | `T` | ###### Parameters [Section titled “Parameters”](#parameters-33) | Parameter | Type | | --------- | --------- | | `fn` | () => `T` | ###### Returns [Section titled “Returns”](#returns-46) `T` *** ### error [Section titled “error”](#error) ```ts error: (name, message) => Error; ``` An Error carrying `name`, to throw: `throw ctx.error("NotConnected", "Paste your token in the sidebar.")` — the widget's promise rejects with `e.name === "NotConnected"` instead of a message to match. `ctx.credential` throws `NotConnected` itself. #### Parameters [Section titled “Parameters”](#parameters-34) | Parameter | Type | | --------- | -------- | | `name` | `string` | | `message` | `string` | #### Returns [Section titled “Returns”](#returns-47) `Error` *** ### identity [Section titled “identity”](#identity) ```ts identity: CoworkIdentity; ``` Who this run is for — the signed-in user and their organization, as Cowork saw them when the run started (frozen). Use it to act on WHO (e.g. stamp an approver). Nobody signed in: `user` and `organization` null. *** ### journal [Section titled “journal”](#journal) ```ts journal: object; ``` Leave a learning / error note for a skill in the skill journal (`source` defaults to "routine:"). #### add [Section titled “add”](#add) ```ts add: (entry) => CoworkJournalEntry; ``` ##### Parameters [Section titled “Parameters”](#parameters-35) | Parameter | Type | | --------------- | ------------------------------------------------------------------------------------------------------------- | | `entry` | { `kind?`: `"learning"` \| `"error"` \| `"note"`; `skill`: `string`; `source?`: `string`; `text`: `string`; } | | `entry.kind?` | `"learning"` \| `"error"` \| `"note"` | | `entry.skill` | `string` | | `entry.source?` | `string` | | `entry.text` | `string` | ##### Returns [Section titled “Returns”](#returns-48) [`CoworkJournalEntry`](/docs/reference/sdk/interfaces/coworkjournalentry/) *** ### log [Section titled “log”](#log) ```ts log: (...parts) => void; ``` A line in the run's log — strings as written, anything else as JSON; `console.log` lands there too. It streams while the run is live, and a run that failed or changed something keeps its last lines in its record. #### Parameters [Section titled “Parameters”](#parameters-36) | Parameter | Type | | ---------- | ------------ | | ...`parts` | `unknown`\[] | #### Returns [Section titled “Returns”](#returns-49) `void` *** ### notify [Section titled “notify”](#notify) ```ts notify: (n) => Promise<void>; ``` An OS notification from this routine's app — what `cowork.notify` does in a widget, and the way to notify while no pane is open (a poll that found new mail): `await ctx.notify({ title: "New email", body: from, tag: "inbox" })`. The desktop app shows it even with its window closed; a browser tab on the workspace is best effort; only the ACTIVE workspace's show, and nobody showing it within a minute drops it (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; either one's switch mutes them). A routine outside an app (`.cowork/apps/<name>/`) gets BadRequest. One raised by a run that a notification started (or a routine it called) shows, but starts no routine. #### Parameters [Section titled “Parameters”](#parameters-37) | Parameter | Type | | ----------- | --------------------------------------------------------------------------------- | | `n` | { `body?`: `string`; `tag?`: `string`; `title`: `string`; `urgent?`: `boolean`; } | | `n.body?` | `string` | | `n.tag?` | `string` | | `n.title` | `string` | | `n.urgent?` | `boolean` | #### Returns [Section titled “Returns”](#returns-50) `Promise`<`void`> *** ### queue [Section titled “queue”](#queue) ```ts queue: (name) => object; ``` A task queue in this workspace (.cowork/local/queues.db), direct; the same tasks as `cowork queue …` and the Tasks view. `send` creates it; `receive` takes the oldest task for `as` or for anyone (null = none); `ack`/`nack` need its `receipt` — `ack`'s result and files are the holder's entry (a `{path}` only from inside the workspace), `review` asks the owner, `to` hands it on. Files: `{path}` (workspace-relative or absolute, copied), `{name, text}` or `{name, base64}`. `list`'s `q`: every word, in what the Tasks view shows of a task. #### Parameters [Section titled “Parameters”](#parameters-38) | Parameter | Type | | --------- | -------- | | `name` | `string` | #### Returns [Section titled “Returns”](#returns-51) `object` ##### ack [Section titled “ack”](#ack) ```ts ack: (id, receipt, opts?) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-39) | Parameter | Type | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `number` | | `receipt` | `string` | | `opts?` | { `files?`: [`CoworkQueueFileInput`](/docs/reference/sdk/type-aliases/coworkqueuefileinput/)\[]; `result?`: `string`; `review?`: `boolean`; `to?`: `string`; } | | `opts.files?` | [`CoworkQueueFileInput`](/docs/reference/sdk/type-aliases/coworkqueuefileinput/)\[] | | `opts.result?` | `string` | | `opts.review?` | `boolean` | | `opts.to?` | `string` | ###### Returns [Section titled “Returns”](#returns-52) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) ##### approve [Section titled “approve”](#approve) ```ts approve: (id) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-40) | Parameter | Type | | --------- | -------- | | `id` | `number` | ###### Returns [Section titled “Returns”](#returns-53) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) ##### counts [Section titled “counts”](#counts) ```ts counts: () => Record<CoworkTaskView, number>; ``` ###### Returns [Section titled “Returns”](#returns-54) `Record`<[`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/), `number`> ##### list [Section titled “list”](#list-1) ```ts list: (opts?) => CoworkQueueTask[]; ``` ###### Parameters [Section titled “Parameters”](#parameters-41) | Parameter | Type | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `opts?` | { `agent?`: `string`; `before?`: `number`; `limit?`: `number`; `q?`: `string`; `view?`: [`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/); } | | `opts.agent?` | `string` | | `opts.before?` | `number` | | `opts.limit?` | `number` | | `opts.q?` | `string` | | `opts.view?` | [`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/) | ###### Returns [Section titled “Returns”](#returns-55) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)\[] ##### nack [Section titled “nack”](#nack) ```ts nack: (id, receipt, opts?) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-42) | Parameter | Type | | -------------- | -------------------------------------------- | | `id` | `number` | | `receipt` | `string` | | `opts?` | { `delayS?`: `number`; `error?`: `string`; } | | `opts.delayS?` | `number` | | `opts.error?` | `string` | ###### Returns [Section titled “Returns”](#returns-56) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) ##### peek [Section titled “peek”](#peek) ```ts peek: (id) => | CoworkQueueTask | null; ``` ###### Parameters [Section titled “Parameters”](#parameters-43) | Parameter | Type | | --------- | -------- | | `id` | `number` | ###### Returns [Section titled “Returns”](#returns-57) \| [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) | `null` ##### receive [Section titled “receive”](#receive) ```ts receive: (opts?) => | CoworkQueueTask & object | null; ``` ###### Parameters [Section titled “Parameters”](#parameters-44) | Parameter | Type | | -------------- | ------------------------------------------------------------- | | `opts?` | { `as?`: `string`; `leaseS?`: `number`; `mine?`: `boolean`; } | | `opts.as?` | `string` | | `opts.leaseS?` | `number` | | `opts.mine?` | `boolean` | ###### Returns [Section titled “Returns”](#returns-58) \| [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) & `object` | `null` ##### reject [Section titled “reject”](#reject) ```ts reject: (id, comment) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-45) | Parameter | Type | | --------- | -------- | | `id` | `number` | | `comment` | `string` | ###### Returns [Section titled “Returns”](#returns-59) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) ##### reply [Section titled “reply”](#reply) ```ts reply: (id, body, opts?) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-46) | Parameter | Type | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `number` | | `body` | `string` | | `opts?` | { `as?`: `string`; `files?`: [`CoworkQueueFileInput`](/docs/reference/sdk/type-aliases/coworkqueuefileinput/)\[]; `ref?`: `string`; `to?`: `string` \| `null`; } | | `opts.as?` | `string` | | `opts.files?` | [`CoworkQueueFileInput`](/docs/reference/sdk/type-aliases/coworkqueuefileinput/)\[] | | `opts.ref?` | `string` | | `opts.to?` | `string` \| `null` | ###### Returns [Section titled “Returns”](#returns-60) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) ##### retry [Section titled “retry”](#retry) ```ts retry: (id) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-47) | Parameter | Type | | --------- | -------- | | `id` | `number` | ###### Returns [Section titled “Returns”](#returns-61) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) ##### send [Section titled “send”](#send) ```ts send: (body, opts?) => CoworkQueueTask; ``` ###### Parameters [Section titled “Parameters”](#parameters-48) | Parameter | Type | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `body` | `string` | | `opts?` | { `as?`: `string`; `delayS?`: `number`; `files?`: [`CoworkQueueFileInput`](/docs/reference/sdk/type-aliases/coworkqueuefileinput/)\[]; `meta?`: `Record`<`string`, `unknown`>; `ref?`: `string`; `review?`: `boolean`; `subject?`: `string`; `to?`: `string`; } | | `opts.as?` | `string` | | `opts.delayS?` | `number` | | `opts.files?` | [`CoworkQueueFileInput`](/docs/reference/sdk/type-aliases/coworkqueuefileinput/)\[] | | `opts.meta?` | `Record`<`string`, `unknown`> | | `opts.ref?` | `string` | | `opts.review?` | `boolean` | | `opts.subject?` | `string` | | `opts.to?` | `string` | ###### Returns [Section titled “Returns”](#returns-62) [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) *** ### routines [Section titled “routines”](#routines) ```ts routines: object; ``` Run another routine of this workspace and await its result — an ordinary run (off switch, a concurrency slot, its own audit record naming this run in `caller.chain`). An id your app declares is your app's own routine. · calls nest two deep: a → b → c runs, c calling another throws RoutineDepth; a routine already in the chain throws RoutineCycle; · the nested run never outlives this one (`timeoutMs` is capped by what is left); · resolves with its result; a routine that threw rejects with ITS `e.name` (and `e.runId`); a refusal rejects as NotFound, RoutineDisabled or TooManyRoutines — thrown at once, never waited out. #### run [Section titled “run”](#run-1) ```ts run: (id, args?, opts?) => Promise<unknown>; ``` ##### Parameters [Section titled “Parameters”](#parameters-49) | Parameter | Type | | ----------------- | --------------------------- | | `id` | `string` | | `args?` | `unknown` | | `opts?` | { `timeoutMs?`: `number`; } | | `opts.timeoutMs?` | `number` | ##### Returns [Section titled “Returns”](#returns-63) `Promise`<`unknown`> *** ### secrets [Section titled “secrets”](#secrets) ```ts secrets: object; ``` The signed-in Cowork account's secrets, kept encrypted on this computer (the key in the OS keychain) — one store for every workspace, never synced: get() returns undefined for an unset name; list() returns names only (never values). Reserved namespace: names under "cowork." are SYSTEM-managed — provisioned by signing in, not writable by apps/routines/the CLI. "cowork.openrouter" is a preprovisioned OpenRouter API key, present ONLY when signed in — base URL <https://openrouter.ai/api/v1> (OpenAI-compatible), enough to call OpenRouter's free ":free" models out of the box. Read it like any secret: ctx.secrets.get("cowork.openrouter"). A capability's routines touch only its own family (`<name>.*` — what they cache, like a user id) plus reading "cowork.openrouter"; the service it connects to is `ctx.credential`. Anything else — and every secret from an APP's routine — throws NotGranted (an app calls its capability's routines with ctx.routines.run). A package that declares neither `kind` nor `uses` predates capabilities and keeps unrestricted access until it declares one. #### get [Section titled “get”](#get) ```ts get: (name) => string | undefined; ``` ##### Parameters [Section titled “Parameters”](#parameters-50) | Parameter | Type | | --------- | -------- | | `name` | `string` | ##### Returns [Section titled “Returns”](#returns-64) `string` | `undefined` #### list [Section titled “list”](#list-2) ```ts list: () => string[]; ``` The names this routine may read (never values). ##### Returns [Section titled “Returns”](#returns-65) `string`\[] #### set [Section titled “set”](#set) ```ts set: (name, value) => void; ``` Store a secret from inside a routine — a value it caches, a token it minted. THROWS on a reserved "cowork." name or an invalid name (grammar: /^\[A-Za-z\_]\[A-Za-z0-9\_.-]\*$/ — no colons). To forget the connection, ctx.credential.disconnect(). ##### Parameters [Section titled “Parameters”](#parameters-51) | Parameter | Type | | --------- | -------- | | `name` | `string` | | `value` | `string` | ##### Returns [Section titled “Returns”](#returns-66) `void` *** ### unchanged [Section titled “unchanged”](#unchanged) ```ts unchanged: () => void; ``` This run changed nothing — a read, a poll that found nothing. It stays in the local audit but is not an execution: never reported to a team server, left out of the run lists (`cowork routine runs --unchanged` shows it). A run that then throws counts anyway. #### Returns [Section titled “Returns”](#returns-67) `void` *** ### workspaceRoot [Section titled “workspaceRoot”](#workspaceroot) ```ts workspaceRoot: string; ``` The workspace folder's path. A routine does not run with it as its working directory: resolve workspace files against it. # CoworkRoutineRun One run in `cowork.routines.runs` — metadata only, never the routine's output. ## Properties [Section titled “Properties”](#properties) ### app? [Section titled “app?”](#app) ```ts app?: string; ``` The package that owns the routine's code (its manifest name); absent for a loose routine. *** ### chain? [Section titled “chain?”](#chain) ```ts chain?: string[]; ``` The routines that called this run, outermost first; absent for a run nobody called. *** ### durationMs [Section titled “durationMs”](#durationms) ```ts durationMs: number; ``` *** ### error? [Section titled “error?”](#error) ```ts error?: string; ``` *** ### kind? [Section titled “kind?”](#kind) ```ts kind?: "app" | "capability"; ``` That package's kind, when the record says. *** ### ok [Section titled “ok”](#ok) ```ts ok: boolean; ``` *** ### principal? [Section titled “principal?”](#principal) ```ts principal?: | { kind: "owner"; } | { id: string; kind: "agent"; name: string; }; ``` Who ran it: the owner (a person, a pane, a schedule) or an agent signed in with its own login. *** ### resultPreview? [Section titled “resultPreview?”](#resultpreview) ```ts resultPreview?: string; ``` *** ### routineId [Section titled “routineId”](#routineid) ```ts routineId: string; ``` *** ### runId? [Section titled “runId?”](#runid) ```ts runId?: string; ``` *** ### source? [Section titled “source?”](#source) ```ts source?: string; ``` What started it — the coding agent or the source (`claude-code`, `cli`, `app`, …). *** ### task? [Section titled “task?”](#task) ```ts task?: object; ``` A task's own record (an agent starting or finishing one): its title and goal, or its outcome. #### goal? [Section titled “goal?”](#goal) ```ts goal?: string; ``` #### outcome? [Section titled “outcome?”](#outcome) ```ts outcome?: string; ``` #### parent? [Section titled “parent?”](#parent) ```ts parent?: string; ``` #### title? [Section titled “title?”](#title) ```ts title?: string; ``` *** ### taskId? [Section titled “taskId?”](#taskid) ```ts taskId?: string; ``` The agent's task the run was made for. *** ### ts [Section titled “ts”](#ts) ```ts ts: number; ``` # CoworkSdk `window.cowork` (also the global `cowork`): everything an app's widget can call — its routines, dialogs, connections, notifications, capture, its own state and panes. A widget never sees a secret or the filesystem: that work belongs in a routine. ## Properties [Section titled “Properties”](#properties) ### agent [Section titled “agent”](#agent) ```ts agent: object; ``` Expose content to the IDE's AI agent. Text the user selects in the app is captured automatically; these calls add programmatic control. Everything mirrors into `.cowork/local/agent-context.md`, which the agent receives as live selection context. NOTE: `agent` is also the one part of this SDK available inside `.html` apps. #### clearContext [Section titled “clearContext”](#clearcontext) ```ts clearContext: () => void; ``` ##### Returns [Section titled “Returns”](#returns) `void` #### clearSelection [Section titled “clearSelection”](#clearselection) ```ts clearSelection: () => void; ``` Clears only the select() slot; highlighted-text capture is unaffected. ##### Returns [Section titled “Returns”](#returns-1) `void` #### select [Section titled “select”](#select) ```ts select: (text) => void; ``` Publish a synthetic "selection" (list/canvas UIs — one item per line). COMPOSES with any highlighted text — the agent sees both. ##### Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | | --------- | -------- | | `text` | `string` | ##### Returns [Section titled “Returns”](#returns-2) `void` #### setAutoSelection [Section titled “setAutoSelection”](#setautoselection) ```ts setAutoSelection: (enabled) => void; ``` Default-ON automatic capture of highlighted text. `false` stops the automatic publishes and clears the captured text; `true` re-publishes the current selection. select()/setContext() unaffected. ##### Parameters [Section titled “Parameters”](#parameters-1) | Parameter | Type | | --------- | --------- | | `enabled` | `boolean` | ##### Returns [Section titled “Returns”](#returns-3) `void` #### setContext [Section titled “setContext”](#setcontext) ```ts setContext: (value) => void; ``` Extra app context for the agent (objects are JSON-stringified). ##### Parameters [Section titled “Parameters”](#parameters-2) | Parameter | Type | | --------- | --------- | | `value` | `unknown` | ##### Returns [Section titled “Returns”](#returns-4) `void` *** ### app [Section titled “app”](#app) ```ts app: object; ``` This app's own panes — its sidebar, its tabs, its dialog — talking to each other: `post` reaches every OTHER open pane of the same app in this workspace (plain data); `onMessage` returns the unsubscribe. E.g. the sidebar posts `{ type: "open", id }` and the tab shows that record. #### onMessage [Section titled “onMessage”](#onmessage) ```ts onMessage: (cb) => () => void; ``` ##### Parameters [Section titled “Parameters”](#parameters-3) | Parameter | Type | | --------- | --------------------- | | `cb` | (`message`) => `void` | ##### Returns [Section titled “Returns”](#returns-5) () => `void` #### post [Section titled “post”](#post) ```ts post: (message) => void; ``` ##### Parameters [Section titled “Parameters”](#parameters-4) | Parameter | Type | | --------- | --------- | | `message` | `unknown` | ##### Returns [Section titled “Returns”](#returns-6) `void` *** ### badge [Section titled “badge”](#badge) ```ts badge: (count) => Promise<void>; ``` 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). `count` is a non-negative integer. A muted app's count leaves the sum. #### Parameters [Section titled “Parameters”](#parameters-5) | Parameter | Type | | --------- | -------- | | `count` | `number` | #### Returns [Section titled “Returns”](#returns-7) `Promise`<`void`> *** ### browser [Section titled “browser”](#browser) ```ts browser: object; ``` The desktop app's in-IDE browser — the page beside the IDE, for a setup guide's "Open …" buttons. `open(url)` shows an http(s) page there (a tab already on that site is reused, else a new tab) and resolves `{ embedded: true }`; without an in-IDE browser (web, code-server) it falls back to `openExternal` and resolves `{ embedded: false }` — so call it unconditionally. `status()` lists its tabs (`{ open, tabs: [{ url, title, active }] }`), or `null` when there is none — poll it (only while `visible`) to follow where the user is, e.g. to pick an id out of the URL they reached. #### open [Section titled “open”](#open) ```ts open: (url) => Promise<{ embedded: boolean; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-6) | Parameter | Type | | --------- | -------- | | `url` | `string` | ##### Returns [Section titled “Returns”](#returns-8) `Promise`<{ `embedded`: `boolean`; }> #### status [Section titled “status”](#status) ```ts status: () => Promise< | { open: boolean; tabs: object[]; } | null>; ``` ##### Returns [Section titled “Returns”](#returns-9) `Promise`< | { `open`: `boolean`; `tabs`: `object`\[]; } | `null`> *** ### capture [Section titled “capture”](#capture) ```ts capture: object; ``` Screen and audio capture. Declare what the panes may capture in cowork.json (`"capture": ["screen", "system-audio", "mic", "screenshot"]`, any subset); every capture still asks the person in Cowork's dialog, which names this app, what it asks for and what it gets — nothing starts without that Allow, and a recording shows a pill with Stop. `record` resolves once recording (the file lands at `session.file` after Stop; `live: true` also streams 16 kHz mono frames to `onAudio` — the app hears what the person hears); `shot` resolves with one PNG (`path` when saved, `image` when asked). Files default to `Recordings/<date-time>-<app>.webm` / `.png`. Rejects by name: Denied, NotDeclared, Busy (one per workspace), DiskFull, Unsupported (a page that cannot capture). Panes only — no routine captures. #### current [Section titled “current”](#current) ```ts current: () => | CoworkCaptureSession | null; ``` This app's recording in progress, if any — to re-attach after the pane remounts. ##### Returns [Section titled “Returns”](#returns-10) \| [`CoworkCaptureSession`](/docs/reference/sdk/interfaces/coworkcapturesession/) | `null` #### record [Section titled “record”](#record) ```ts record: (opts?) => Promise<CoworkCaptureSession>; ``` ##### Parameters [Section titled “Parameters”](#parameters-7) | Parameter | Type | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `opts?` | { `audio?`: { `mic?`: `boolean`; `system?`: `boolean`; }; `file?`: `string`; `live?`: `boolean`; `video?`: `false` \| `"window"` \| `"screen"`; } | | `opts.audio?` | { `mic?`: `boolean`; `system?`: `boolean`; } | | `opts.audio.mic?` | `boolean` | | `opts.audio.system?` | `boolean` | | `opts.file?` | `string` | | `opts.live?` | `boolean` | | `opts.video?` | `false` \| `"window"` \| `"screen"` | ##### Returns [Section titled “Returns”](#returns-11) `Promise`<[`CoworkCaptureSession`](/docs/reference/sdk/interfaces/coworkcapturesession/)> #### shot [Section titled “shot”](#shot) ```ts shot: (opts?) => Promise<{ image?: Blob; path?: string; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-8) | Parameter | Type | | -------------- | -------------------------------------------------------------------------------- | | `opts?` | { `file?`: `string`; `image?`: `boolean`; `source?`: `"window"` \| `"screen"`; } | | `opts.file?` | `string` | | `opts.image?` | `boolean` | | `opts.source?` | `"window"` \| `"screen"` | ##### Returns [Section titled “Returns”](#returns-12) `Promise`<{ `image?`: `Blob`; `path?`: `string`; }> *** ### chat [Section titled “chat”](#chat) ```ts chat: object; ``` Send a message to the workbench chat panel: opens the chat and SUBMITS the text as a query. Fire-and-forget. #### ask [Section titled “ask”](#ask) ```ts ask: (text) => void; ``` ##### Parameters [Section titled “Parameters”](#parameters-9) | Parameter | Type | | --------- | -------- | | `text` | `string` | ##### Returns [Section titled “Returns”](#returns-13) `void` *** ### closeTab [Section titled “closeTab”](#closetab) ```ts closeTab: () => void; ``` Close this app's editor tab (as if the user hit × on it). No-op in a sidebar viewpane. #### Returns [Section titled “Returns”](#returns-14) `void` *** ### consent [Section titled “consent”](#consent) ```ts consent: object; ``` Command consent — ask the user (via the shared Allow/Deny + "Always allow in this workspace" modal) before running something. `value` is a stable human label; `detail` is shown verbatim (the exact command) for transparency. A persisted grant lives in Cowork's per-workspace consent file, on this computer, NEVER in the workspace — nothing is shipped, so every machine approves the first time. Returns `granted` + a one-shot `ticket` when allowed just once; pass it to `stream.open({ consent: { value, ticket } })`. #### request [Section titled “request”](#request) ```ts request: (opts) => Promise<{ granted: boolean; ticket?: string; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-10) | Parameter | Type | | -------------- | ----------------------------------------------------------------------------------------------------- | | `opts` | { `body?`: `string`; `detail?`: `string`; `kind?`: `string`; `title?`: `string`; `value`: `string`; } | | `opts.body?` | `string` | | `opts.detail?` | `string` | | `opts.kind?` | `string` | | `opts.title?` | `string` | | `opts.value` | `string` | ##### Returns [Section titled “Returns”](#returns-15) `Promise`<{ `granted`: `boolean`; `ticket?`: `string`; }> *** ### credentials [Section titled “credentials”](#credentials) ```ts credentials: object; ``` The connections of the capabilities this app uses. A capability declares how it connects (`"credential"` in its cowork.json); the person connects once — here or in Manage Workspace › Components › Credentials — and every capability sharing the connection is connected. Never a token or a key: the capability's routines call the service. #### connect [Section titled “connect”](#connect) ```ts connect: (name, fields?) => Promise<void>; ``` Connect it. A sign-in opens the service's consent for what every capability using it needs and resolves once the page is open — the sign-in finishes in the browser, then refresh; a key saves `fields` (e.g. `{ token }`). One that takes either signs in unless `fields` are given. ##### Parameters [Section titled “Parameters”](#parameters-11) | Parameter | Type | | --------- | ---------------------------- | | `name` | `string` | | `fields?` | `Record`<`string`, `string`> | ##### Returns [Section titled “Returns”](#returns-16) `Promise`<`void`> #### get [Section titled “get”](#get) ```ts get: (name) => Promise<CoworkConnection>; ``` The connection — ready when `state` is "connected". ##### Parameters [Section titled “Parameters”](#parameters-12) | Parameter | Type | | --------- | -------- | | `name` | `string` | ##### Returns [Section titled “Returns”](#returns-17) `Promise`<[`CoworkConnection`](/docs/reference/sdk/interfaces/coworkconnection/)> *** ### identity [Section titled “identity”](#identity) ```ts identity: object; ``` Who is signed in: currentUser() → {uid, email}, or null when signed out. #### currentUser [Section titled “currentUser”](#currentuser) ```ts currentUser: () => Promise< | { email: string | null; uid: string; } | null>; ``` ##### Returns [Section titled “Returns”](#returns-18) `Promise`< | { `email`: `string` | `null`; `uid`: `string`; } | `null`> *** ### journal [Section titled “journal”](#journal) ```ts journal: object; ``` The skill journal — learnings and error notes per skill, "open" until an agent folds them into the skill and resolves them. `entries` lists one skill's, oldest first: its open ones unless `status` says otherwise, at most `limit` (1–200, default 200). `resolve` marks the given ids applied, or every open one of the skill. #### add [Section titled “add”](#add) ```ts add: (entry) => Promise<CoworkJournalEntry>; ``` ##### Parameters [Section titled “Parameters”](#parameters-13) | Parameter | Type | | --------------- | ------------------------------------------------------------------------------------------------------------- | | `entry` | { `kind?`: `"learning"` \| `"error"` \| `"note"`; `skill`: `string`; `source?`: `string`; `text`: `string`; } | | `entry.kind?` | `"learning"` \| `"error"` \| `"note"` | | `entry.skill` | `string` | | `entry.source?` | `string` | | `entry.text` | `string` | ##### Returns [Section titled “Returns”](#returns-19) `Promise`<[`CoworkJournalEntry`](/docs/reference/sdk/interfaces/coworkjournalentry/)> #### entries [Section titled “entries”](#entries) ```ts entries: (skill, opts?) => Promise<CoworkJournalEntry[]>; ``` ##### Parameters [Section titled “Parameters”](#parameters-14) | Parameter | Type | | -------------- | ---------------------------------------------------------------------- | | `skill` | `string` | | `opts?` | { `limit?`: `number`; `status?`: `"all"` \| `"open"` \| `"applied"`; } | | `opts.limit?` | `number` | | `opts.status?` | `"all"` \| `"open"` \| `"applied"` | ##### Returns [Section titled “Returns”](#returns-20) `Promise`<[`CoworkJournalEntry`](/docs/reference/sdk/interfaces/coworkjournalentry/)\[]> #### resolve [Section titled “resolve”](#resolve) ```ts resolve: (skill, ids?) => Promise<{ resolved: number; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-15) | Parameter | Type | | --------- | ----------- | | `skill` | `string` | | `ids?` | `number`\[] | ##### Returns [Section titled “Returns”](#returns-21) `Promise`<{ `resolved`: `number`; }> #### skills [Section titled “skills”](#skills) ```ts skills: () => Promise<object[]>; ``` ##### Returns [Section titled “Returns”](#returns-22) `Promise`<`object`\[]> *** ### log [Section titled “log”](#log) ```ts log: (...parts) => void; ``` Emit a log line to the "Cowork" Output panel. #### Parameters [Section titled “Parameters”](#parameters-16) | Parameter | Type | | ---------- | ------------ | | ...`parts` | `unknown`\[] | #### Returns [Section titled “Returns”](#returns-23) `void` *** ### notify [Section titled “notify”](#notify) ```ts notify: (n) => Promise<void>; ``` 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: a backgrounded tab is throttled); only the ACTIVE workspace's notifications show. Rejects in a machine's (remote) window. `title` is required and shows exactly as sent (≤ 100 characters; `body` ≤ 300, `tag` ≤ 64 — longer is cut). A `tag` replaces this app's not-yet-shown notification with the same tag; `urgent` keeps the banner up and asks for attention (dock bounce, flashing taskbar). A click opens this app. Resolves too when the user muted the app (Manage Workspace › Notifications) or it sent more than 6 in a minute — those are dropped; one nobody shows within a minute is dropped as well. Rejects BadRequest without a title. To notify with no pane open, call `ctx.notify` from a routine. #### Parameters [Section titled “Parameters”](#parameters-17) | Parameter | Type | | ----------- | --------------------------------------------------------------------------------- | | `n` | { `body?`: `string`; `tag?`: `string`; `title`: `string`; `urgent?`: `boolean`; } | | `n.body?` | `string` | | `n.tag?` | `string` | | `n.title` | `string` | | `n.urgent?` | `boolean` | #### Returns [Section titled “Returns”](#returns-24) `Promise`<`void`> *** ### onNotification [Section titled “onNotification”](#onnotification) ```ts onNotification: (cb) => () => void; ``` Called with each of THIS app's notifications as Cowork accepts it — its panes' `cowork.notify`, its routines' `ctx.notify`, and a capability's that shows as this app — in every open pane of the app: `useEffect(() => cowork.onNotification((n) => setLast(n)), [])`. Returns the unsubscribe. Live only: a pane that opens later never gets earlier ones. A muted app's notifications and the ones Cowork drops (more than 6 a minute, the app switched off) never arrive. Another app's notifications reach a routine instead, through its `notifications` trigger. #### Parameters [Section titled “Parameters”](#parameters-18) | Parameter | Type | | --------- | --------------- | | `cb` | (`n`) => `void` | #### Returns [Section titled “Returns”](#returns-25) () => `void` *** ### onTitleAction [Section titled “onTitleAction”](#ontitleaction) ```ts onTitleAction: (handler) => () => void; ``` Subscribe to pane title-action button clicks (the standalone runner's panes). #### Parameters [Section titled “Parameters”](#parameters-19) | Parameter | Type | | --------- | ---------------- | | `handler` | (`id`) => `void` | #### Returns [Section titled “Returns”](#returns-26) () => `void` *** ### onVisibility [Section titled “onVisibility”](#onvisibility) ```ts onVisibility: (cb) => () => void; ``` Subscribe to visibility flips. Returns the unsubscribe function. #### Parameters [Section titled “Parameters”](#parameters-20) | Parameter | Type | | --------- | --------------------- | | `cb` | (`visible`) => `void` | #### Returns [Section titled “Returns”](#returns-27) () => `void` *** ### openedFile? [Section titled “openedFile?”](#openedfile) ```ts openedFile?: CoworkOpenedFile; ``` Present only when opened via fileRouting (editor tab). *** ### openExternal [Section titled “openExternal”](#openexternal) ```ts openExternal: (url) => void; ``` Open an external URL (http/https/mailto) via VS Code — shows the workbench trust prompt (Open / Copy / configure trusted domains). You usually DON'T need to call this: the app host intercepts clicks on any `<a href="https://…">` and routes them here automatically, so ordinary markup is already safe (relative and `#anchor` links are untouched). Call it directly for PROGRAMMATIC opens — no click involved. #### Parameters [Section titled “Parameters”](#parameters-21) | Parameter | Type | | --------- | -------- | | `url` | `string` | #### Returns [Section titled “Returns”](#returns-28) `void` *** ### openFile [Section titled “openFile”](#openfile) ```ts openFile: (path) => void; ``` Open a workspace file (routes through fileRouting when it matches). #### Parameters [Section titled “Parameters”](#parameters-22) | Parameter | Type | | --------- | -------- | | `path` | `string` | #### Returns [Section titled “Returns”](#returns-29) `void` *** ### queue [Section titled “queue”](#queue) ```ts queue: object; ``` Task queues (the workspace's .cowork/local/queues.db; the Tasks view) — work for agents and scripts. A task has a `subject`, a `status` (todo → in_progress → needs_review or done; failed past its attempts), who it is `from` and `to` (an agent's name, "owner" or "anyone"), `review` (its finish waits for the owner), the request (`body`, `files`) and a thread of `entries`. `send` creates the queue (≤ 1 MiB body, files as `{name, text}` or `{name, base64}` — data, never a path; a repeated `ref` answers the first task); `receive` takes the oldest task for `as` or for anyone (`mine`: its own only) with a `receipt` (null = none); `ack` finishes it — the result and files are the holder's entry, `review` asks the owner, `to` hands it on — `nack` gives it back. `approve`, `reject` (Send back), `reply`, `retry` act as the owner; `tasks` is the Tasks view's one read. Reads on a workspace with no queue yet answer empty. Times are epoch ms. #### ack [Section titled “ack”](#ack) ```ts ack: (queue, id, receipt, opts?) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-23) | Parameter | Type | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `queue` | `string` | | `id` | `number` | | `receipt` | `string` | | `opts?` | { `files?`: ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[]; `result?`: `string`; `review?`: `boolean`; `to?`: `string`; } | | `opts.files?` | ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[] | | `opts.result?` | `string` | | `opts.review?` | `boolean` | | `opts.to?` | `string` | ##### Returns [Section titled “Returns”](#returns-30) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### approve [Section titled “approve”](#approve) ```ts approve: (queue, id) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-24) | Parameter | Type | | --------- | -------- | | `queue` | `string` | | `id` | `number` | ##### Returns [Section titled “Returns”](#returns-31) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### create [Section titled “create”](#create) ```ts create: (queue, opts?) => Promise<CoworkQueueInfo>; ``` ##### Parameters [Section titled “Parameters”](#parameters-25) | Parameter | Type | | ------------------- | ------------------------------------------------------------------------ | | `queue` | `string` | | `opts?` | { `leaseS?`: `number`; `maxAttempts?`: `number`; `review?`: `boolean`; } | | `opts.leaseS?` | `number` | | `opts.maxAttempts?` | `number` | | `opts.review?` | `boolean` | ##### Returns [Section titled “Returns”](#returns-32) `Promise`<[`CoworkQueueInfo`](/docs/reference/sdk/interfaces/coworkqueueinfo/)> #### items [Section titled “items”](#items) ```ts items: (queue, opts?) => Promise<CoworkQueueTask[]>; ``` ##### Parameters [Section titled “Parameters”](#parameters-26) | Parameter | Type | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `queue` | `string` | | `opts?` | { `agent?`: `string`; `before?`: `number`; `limit?`: `number`; `view?`: [`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/); } | | `opts.agent?` | `string` | | `opts.before?` | `number` | | `opts.limit?` | `number` | | `opts.view?` | [`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/) | ##### Returns [Section titled “Returns”](#returns-33) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)\[]> #### list [Section titled “list”](#list) ```ts list: () => Promise<CoworkQueueInfo[]>; ``` ##### Returns [Section titled “Returns”](#returns-34) `Promise`<[`CoworkQueueInfo`](/docs/reference/sdk/interfaces/coworkqueueinfo/)\[]> #### nack [Section titled “nack”](#nack) ```ts nack: (queue, id, receipt, opts?) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-27) | Parameter | Type | | -------------- | -------------------------------------------- | | `queue` | `string` | | `id` | `number` | | `receipt` | `string` | | `opts?` | { `delayS?`: `number`; `error?`: `string`; } | | `opts.delayS?` | `number` | | `opts.error?` | `string` | ##### Returns [Section titled “Returns”](#returns-35) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### peek [Section titled “peek”](#peek) ```ts peek: (queue, id) => Promise< | CoworkQueueTask | null>; ``` ##### Parameters [Section titled “Parameters”](#parameters-28) | Parameter | Type | | --------- | -------- | | `queue` | `string` | | `id` | `number` | ##### Returns [Section titled “Returns”](#returns-36) `Promise`< | [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) | `null`> #### purge [Section titled “purge”](#purge) ```ts purge: (queue, status?) => Promise<{ purged: number; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-29) | Parameter | Type | | --------- | --------------------------------- | | `queue` | `string` | | `status?` | `"done"` \| `"failed"` \| `"all"` | ##### Returns [Section titled “Returns”](#returns-37) `Promise`<{ `purged`: `number`; }> #### receive [Section titled “receive”](#receive) ```ts receive: (queue, opts?) => Promise< | CoworkQueueTask & object | null>; ``` ##### Parameters [Section titled “Parameters”](#parameters-30) | Parameter | Type | | -------------- | ------------------------------------------------------------- | | `queue` | `string` | | `opts?` | { `as?`: `string`; `leaseS?`: `number`; `mine?`: `boolean`; } | | `opts.as?` | `string` | | `opts.leaseS?` | `number` | | `opts.mine?` | `boolean` | ##### Returns [Section titled “Returns”](#returns-38) `Promise`< | [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/) & `object` | `null`> #### receivers [Section titled “receivers”](#receivers) ```ts receivers: (queue?) => Promise<CoworkQueueReceiver[]>; ``` ##### Parameters [Section titled “Parameters”](#parameters-31) | Parameter | Type | | --------- | -------- | | `queue?` | `string` | ##### Returns [Section titled “Returns”](#returns-39) `Promise`<[`CoworkQueueReceiver`](/docs/reference/sdk/interfaces/coworkqueuereceiver/)\[]> #### reject [Section titled “reject”](#reject) ```ts reject: (queue, id, comment, opts?) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-32) | Parameter | Type | | ------------- | ------------------------------------------------------------------------------------------------------------ | | `queue` | `string` | | `id` | `number` | | `comment` | `string` | | `opts?` | { `files?`: ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[]; } | | `opts.files?` | ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[] | ##### Returns [Section titled “Returns”](#returns-40) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### remove [Section titled “remove”](#remove) ```ts remove: (queue, id?) => Promise<{ removed: boolean; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-33) | Parameter | Type | | --------- | -------- | | `queue` | `string` | | `id?` | `number` | ##### Returns [Section titled “Returns”](#returns-41) `Promise`<{ `removed`: `boolean`; }> #### reply [Section titled “reply”](#reply) ```ts reply: (queue, id, body, opts?) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-34) | Parameter | Type | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `queue` | `string` | | `id` | `number` | | `body` | `string` | | `opts?` | { `as?`: `string`; `files?`: ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[]; `to?`: `string` \| `null`; } | | `opts.as?` | `string` | | `opts.files?` | ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[] | | `opts.to?` | `string` \| `null` | ##### Returns [Section titled “Returns”](#returns-42) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### retry [Section titled “retry”](#retry) ```ts retry: (queue, id) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-35) | Parameter | Type | | --------- | -------- | | `queue` | `string` | | `id` | `number` | ##### Returns [Section titled “Returns”](#returns-43) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### send [Section titled “send”](#send) ```ts send: (queue, task) => Promise<CoworkQueueTask>; ``` ##### Parameters [Section titled “Parameters”](#parameters-36) | Parameter | Type | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `queue` | `string` | | `task` | { `as?`: `string`; `body?`: `string`; `delayS?`: `number`; `files?`: ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[]; `meta?`: `Record`<`string`, `unknown`>; `ref?`: `string`; `review?`: `boolean`; `subject?`: `string`; `to?`: `string`; } | | `task.as?` | `string` | | `task.body?` | `string` | | `task.delayS?` | `number` | | `task.files?` | ( \| { `name`: `string`; `text`: `string`; } \| { `base64`: `string`; `name`: `string`; })\[] | | `task.meta?` | `Record`<`string`, `unknown`> | | `task.ref?` | `string` | | `task.review?` | `boolean` | | `task.subject?` | `string` | | `task.to?` | `string` | ##### Returns [Section titled “Returns”](#returns-44) `Promise`<[`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)> #### summary [Section titled “summary”](#summary) ```ts summary: () => Promise<{ journal: number; needsYou: number; }>; ``` ##### Returns [Section titled “Returns”](#returns-45) `Promise`<{ `journal`: `number`; `needsYou`: `number`; }> #### tasks [Section titled “tasks”](#tasks) ```ts tasks: (opts?) => Promise<{ agents: CoworkQueueReceiver[]; counts: Record<CoworkTaskView, number>; queues: CoworkQueueInfo[]; tasks: CoworkQueueTask[]; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-37) | Parameter | Type | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `opts?` | { `agent?`: `string`; `limit?`: `number`; `q?`: `string`; `queue?`: `string`; `view?`: [`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/); } | | `opts.agent?` | `string` | | `opts.limit?` | `number` | | `opts.q?` | `string` | | `opts.queue?` | `string` | | `opts.view?` | [`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/) | ##### Returns [Section titled “Returns”](#returns-46) `Promise`<{ `agents`: [`CoworkQueueReceiver`](/docs/reference/sdk/interfaces/coworkqueuereceiver/)\[]; `counts`: `Record`<[`CoworkTaskView`](/docs/reference/sdk/type-aliases/coworktaskview/), `number`>; `queues`: [`CoworkQueueInfo`](/docs/reference/sdk/interfaces/coworkqueueinfo/)\[]; `tasks`: [`CoworkQueueTask`](/docs/reference/sdk/interfaces/coworkqueuetask/)\[]; }> *** ### routines [Section titled “routines”](#routines) ```ts routines: object; ``` Run a registered routine — declared in an app's .cowork/apps//cowork.json `routines` — and resolve its return value. Rejects with the routine's error: `e.name` is the kind it threw (`NotConnected` from ctx.error / 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, and a busy daemon (TooManyRoutines) is waited out within the call's timeout. #### run [Section titled “run”](#run) ```ts run: (id, args?, opts?) => Promise<unknown>; ``` ##### Parameters [Section titled “Parameters”](#parameters-38) | Parameter | Type | | ----------------- | --------------------------- | | `id` | `string` | | `args?` | `unknown` | | `opts?` | { `timeoutMs?`: `number`; } | | `opts.timeoutMs?` | `number` | ##### Returns [Section titled “Returns”](#returns-47) `Promise`<`unknown`> #### runs [Section titled “runs”](#runs) ```ts runs: (id?, opts?) => Promise<CoworkRoutineRun[]>; ``` Bounded run history (newest first) — metadata only (ts, ok, durationMs, error / resultPreview). Omit id for all routines in this workspace; `opts.app` narrows to one package's runs (its manifest name). Each run names its package (`app`, its `kind` when the record says) and, when another routine called it, `chain` — the callers, outermost first. ##### Parameters [Section titled “Parameters”](#parameters-39) | Parameter | Type | | ----------- | --------------------- | | `id?` | `string` | | `opts?` | { `app?`: `string`; } | | `opts.app?` | `string` | ##### Returns [Section titled “Returns”](#returns-48) `Promise`<[`CoworkRoutineRun`](/docs/reference/sdk/interfaces/coworkroutinerun/)\[]> #### subscribe [Section titled “subscribe”](#subscribe) ```ts subscribe: (id, args, opts) => object; ``` Keep a routine's answer fresh: runs now, then every `interval` seconds (min 5, default 60) while a pane of this app is on screen — ONE loop shared by every pane of the app asking the same routine with the same args (the sidebar and its tab poll once, not twice). `onData` gets each NEW answer (identical answers are not re-sent; a pane opening later gets the last one at once); `onError` gets the failure by name — `NotConnected` pauses the loop until `refresh()` (call it after the user connects); `refresh()` always answers this pane — from a run that starts after the call, even when nothing changed — await it for a spinner, or to see the result of a change you just made. Use this instead of setInterval: `useEffect(() => { const s = cowork.routines.subscribe(…); return () => s.close() }, [])`. ##### Parameters [Section titled “Parameters”](#parameters-40) | Parameter | Type | | ----------------- | ------------------------------------------------------------------------------------------------------------------ | | `id` | `string` | | `args` | `unknown` | | `opts` | { `interval?`: `number`; `onData?`: (`data`) => `void`; `onError?`: (`error`) => `void`; `timeoutMs?`: `number`; } | | `opts.interval?` | `number` | | `opts.onData?` | (`data`) => `void` | | `opts.onError?` | (`error`) => `void` | | `opts.timeoutMs?` | `number` | ##### Returns [Section titled “Returns”](#returns-49) `object` ###### close [Section titled “close”](#close) ```ts close: () => void; ``` ###### Returns [Section titled “Returns”](#returns-50) `void` ###### refresh [Section titled “refresh”](#refresh) ```ts refresh: () => Promise<void>; ``` ###### Returns [Section titled “Returns”](#returns-51) `Promise`<`void`> *** ### setTitleActions [Section titled “setTitleActions”](#settitleactions) ```ts setTitleActions: (actions) => void; ``` Replace the pane title-action buttons (the standalone runner's panes). #### Parameters [Section titled “Parameters”](#parameters-41) | Parameter | Type | | --------- | ----------- | | `actions` | `object`\[] | #### Returns [Section titled “Returns”](#returns-52) `void` *** ### state [Section titled “state”](#state) ```ts state: object; ``` Persisted per-view state (survives hide/show + reloads). #### get [Section titled “get”](#get-1) ```ts get: <T>() => T | undefined; ``` ##### Type Parameters [Section titled “Type Parameters”](#type-parameters) | Type Parameter | Default type | | -------------- | ------------ | | `T` | `unknown` | ##### Returns [Section titled “Returns”](#returns-53) `T` | `undefined` #### set [Section titled “set”](#set) ```ts set: (value) => void; ``` ##### Parameters [Section titled “Parameters”](#parameters-42) | Parameter | Type | | --------- | --------- | | `value` | `unknown` | ##### Returns [Section titled “Returns”](#returns-54) `void` *** ### statusBar [Section titled “statusBar”](#statusbar) ```ts statusBar: object; ``` The apps' footer items (see CoworkStatusBarItem), keyed by app name. #### click [Section titled “click”](#click) ```ts click: (app) => void; ``` Act as if the user clicked the app's item. ##### Parameters [Section titled “Parameters”](#parameters-43) | Parameter | Type | | --------- | -------- | | `app` | `string` | ##### Returns [Section titled “Returns”](#returns-55) `void` #### get [Section titled “get”](#get-2) ```ts get: (app) => | CoworkStatusBarItem | null; ``` An app's item as last drawn (`data` included), or null. ##### Parameters [Section titled “Parameters”](#parameters-44) | Parameter | Type | | --------- | -------- | | `app` | `string` | ##### Returns [Section titled “Returns”](#returns-56) \| [`CoworkStatusBarItem`](/docs/reference/sdk/interfaces/coworkstatusbaritem/) | `null` #### onChange [Section titled “onChange”](#onchange) ```ts onChange: (cb) => () => void; ``` Called with each app's current item right away, then on every change of any app's item — subscribe rather than reading get() at mount. ##### Parameters [Section titled “Parameters”](#parameters-45) | Parameter | Type | | --------- | ------------------------- | | `cb` | (`app`, `item`) => `void` | ##### Returns [Section titled “Returns”](#returns-57) () => `void` #### refresh [Section titled “refresh”](#refresh-1) ```ts refresh: (app?) => void; ``` Re-run an app's statusBar routine now (all apps without one) — after your widget changed what it reports. ##### Parameters [Section titled “Parameters”](#parameters-46) | Parameter | Type | | --------- | -------- | | `app?` | `string` | ##### Returns [Section titled “Returns”](#returns-58) `void` *** ### stream [Section titled “stream”](#stream) ```ts stream: object; ``` Persistent process streams — spawn ONE long-lived process on the daemon and move raw stdio bytes (PTY mirrors, tail -f, long CLIs), instead of paying a subprocess per routine call. Token-gated like routines.run. `onData` gets stdout bytes as they arrive; `onEnd` fires exactly once ("end" = the process exited, "error" = the stream broke, "closed" = you closed it). The daemon caps concurrent streams (8) and reaps orphans; ONE reader per stream — reconnect by opening a fresh stream. Per view at most 2 streams may be open at once (each pins one of the origin's \~6 browser connections) — open() throws past that cap, so close a stream before opening the next. #### open [Section titled “open”](#open-1) ```ts open: (opts) => Promise<{ close: () => Promise<void>; id: string; write: (data) => Promise<void>; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-47) | Parameter | Type | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `opts` | { `argv`: `string`\[]; `consent`: { `ticket?`: `string`; `value`: `string`; }; `onData`: (`bytes`) => `void`; `onEnd?`: (`reason`) => `void`; } | - | | `opts.argv` | `string`\[] | - | | `opts.consent` | { `ticket?`: `string`; `value`: `string`; } | Proof the user approved this command (from cowork.consent.request): the stable label they consented to + a one-shot `ticket` for an "allow once" (omit for a persisted "always allow"). Without a covering grant/ticket the daemon refuses (ConsentRequired). | | `opts.consent.ticket?` | `string` | - | | `opts.consent.value` | `string` | - | | `opts.onData` | (`bytes`) => `void` | - | | `opts.onEnd?` | (`reason`) => `void` | - | ##### Returns [Section titled “Returns”](#returns-59) `Promise`<{ `close`: () => `Promise`<`void`>; `id`: `string`; `write`: (`data`) => `Promise`<`void`>; }> *** ### tables [Section titled “tables”](#tables) ```ts tables: object; ``` Local SQLite tables. `db` is a WORKSPACE-RELATIVE path to a .db file; calls go through the daemon (WAL mode, so reads stay safe while routines write). query() is read-only (non-SELECT SQL is rejected) and every page is capped at 500 rows; exec() is for writes/DDL. #### exec [Section titled “exec”](#exec) ```ts exec: (opts) => Promise<{ changes: number; lastInsertRowid: number; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-48) | Parameter | Type | | -------------- | ------------------------------------------------------------- | | `opts` | { `db`: `string`; `params?`: `unknown`\[]; `sql`: `string`; } | | `opts.db` | `string` | | `opts.params?` | `unknown`\[] | | `opts.sql` | `string` | ##### Returns [Section titled “Returns”](#returns-60) `Promise`<{ `changes`: `number`; `lastInsertRowid`: `number`; }> #### query [Section titled “query”](#query) ```ts query: (opts) => Promise<{ columns: string[]; rowCount: number; rows: unknown[][]; truncated: boolean; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-49) | Parameter | Type | | -------------- | ------------------------------------------------------------------------------------------------------ | | `opts` | { `db`: `string`; `limit?`: `number`; `offset?`: `number`; `params?`: `unknown`\[]; `sql`: `string`; } | | `opts.db` | `string` | | `opts.limit?` | `number` | | `opts.offset?` | `number` | | `opts.params?` | `unknown`\[] | | `opts.sql` | `string` | ##### Returns [Section titled “Returns”](#returns-61) `Promise`<{ `columns`: `string`\[]; `rowCount`: `number`; `rows`: `unknown`\[]\[]; `truncated`: `boolean`; }> #### schema [Section titled “schema”](#schema) ```ts schema: (opts) => Promise<{ tables: object[]; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-50) | Parameter | Type | | --------- | ------------------- | | `opts` | { `db`: `string`; } | | `opts.db` | `string` | ##### Returns [Section titled “Returns”](#returns-62) `Promise`<{ `tables`: `object`\[]; }> *** ### ui [Section titled “ui”](#ui) ```ts ui: object; ``` Modal dialogs, drawn by Cowork. `dialog` resolves the button's `label` and the fields' `values` plus the button's own. On Cancel or Esc `label` is null and `values` still holds the fields as they were left — test `label`, never `values`. `confirm` is the yes/no shorthand: `true` only on the confirm action, `false` on Cancel or Esc; called bare it asks "Are you sure?" with Continue. #### confirm [Section titled “confirm”](#confirm) ```ts confirm: (opts?) => Promise<boolean>; ``` ##### Parameters [Section titled “Parameters”](#parameters-51) | Parameter | Type | | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `opts?` | { `body?`: `string`; `cancelLabel?`: `string`; `confirmLabel?`: `string`; `destructive?`: `boolean`; `title?`: `string`; } | | `opts.body?` | `string` | | `opts.cancelLabel?` | `string` | | `opts.confirmLabel?` | `string` | | `opts.destructive?` | `boolean` | | `opts.title?` | `string` | ##### Returns [Section titled “Returns”](#returns-63) `Promise`<`boolean`> #### dialog [Section titled “dialog”](#dialog) ```ts dialog: (opts) => Promise<{ label: string | null; values: Record<string, unknown>; }>; ``` ##### Parameters [Section titled “Parameters”](#parameters-52) | Parameter | Type | | --------- | -------------------------------------------------------------- | | `opts` | [`CoworkDialog`](/docs/reference/sdk/interfaces/coworkdialog/) | ##### Returns [Section titled “Returns”](#returns-64) `Promise`<{ `label`: `string` | `null`; `values`: `Record`<`string`, `unknown`>; }> *** ### viewSource [Section titled “viewSource”](#viewsource) ```ts viewSource: () => void; ``` Open this app's SOURCE file (editor tabs: the routed document) in the plain text editor — the "view code" affordance. #### Returns [Section titled “Returns”](#returns-65) `void` *** ### visible [Section titled “visible”](#visible) ```ts readonly visible: boolean; ``` Whether this pane/tab is currently on screen. Retained panes stay ALIVE while hidden, and the Page Visibility API / layout width both lie inside webview iframes — this host-pumped flag is the honest signal. Gate polls on it; refetch on the rising edge via `onVisibility`. *** ### workspaceRoot [Section titled “workspaceRoot”](#workspaceroot) ```ts workspaceRoot: string; ``` The workspace this pane belongs to: its folder's absolute path — in a machine's (remote) window `cowork-remote://<machine>/<path>`, on a published site `/published/<folder name>`. Its last segment is the workspace's name. # CoworkStatusBarItem An app's item in the footer (status bar). Declare it in the app's cowork.json — `"statusBar": {"routine": "my-status", "args"?: {…}, "interval"?: 60}` — and return this from that routine (null hides it). It refreshes every `interval` seconds (15–3600) while the window has focus, when focus returns and after a click. A click runs the same routine with `click.args` (`menu`: the user picks an entry first; `confirm`: a modal asks first). `data` is free-form — for the app's widgets. ## Properties [Section titled “Properties”](#properties) ### click? [Section titled “click?”](#click) ```ts click?: object; ``` #### args? [Section titled “args?”](#args) ```ts args?: Record<string, unknown>; ``` #### confirm? [Section titled “confirm?”](#confirm) ```ts confirm?: string; ``` *** ### data? [Section titled “data?”](#data) ```ts data?: unknown; ``` *** ### icon? [Section titled “icon?”](#icon) ```ts icon?: string; ``` A codicon id ("remote", "plug"; "sync\~spin" spins). *** ### menu? [Section titled “menu?”](#menu) ```ts menu?: object[]; ``` #### args? [Section titled “args?”](#args-1) ```ts args?: Record<string, unknown>; ``` #### confirm? [Section titled “confirm?”](#confirm-1) ```ts confirm?: string; ``` #### label [Section titled “label”](#label) ```ts label: string; ``` *** ### text [Section titled “text”](#text) ```ts text: string; ``` *** ### tone? [Section titled “tone?”](#tone) ```ts tone?: "error" | "warning"; ``` *** ### tooltip? [Section titled “tooltip?”](#tooltip) ```ts tooltip?: string; ``` # @cowork/shared The Cowork Studio SDK: `window.cowork` in an app's widgets, and the `ctx` a routine receives. Every Cowork workspace gets these types as `.cowork/local/cowork.d.ts` (ambient — nothing to import). ## Interfaces [Section titled “Interfaces”](#interfaces) * [CoworkCaptureResult](/docs/reference/sdk/interfaces/coworkcaptureresult/) * [CoworkCaptureSession](/docs/reference/sdk/interfaces/coworkcapturesession/) * [CoworkConnection](/docs/reference/sdk/interfaces/coworkconnection/) * [CoworkDbExecResult](/docs/reference/sdk/interfaces/coworkdbexecresult/) * [CoworkDbQueryResult](/docs/reference/sdk/interfaces/coworkdbqueryresult/) * [CoworkDeployedRoutineContext](/docs/reference/sdk/interfaces/coworkdeployedroutinecontext/) * [CoworkDialog](/docs/reference/sdk/interfaces/coworkdialog/) * [CoworkDialogButton](/docs/reference/sdk/interfaces/coworkdialogbutton/) * [CoworkDialogField](/docs/reference/sdk/interfaces/coworkdialogfield/) * [CoworkDialogOption](/docs/reference/sdk/interfaces/coworkdialogoption/) * [CoworkIdentity](/docs/reference/sdk/interfaces/coworkidentity/) * [CoworkJournalEntry](/docs/reference/sdk/interfaces/coworkjournalentry/) * [CoworkNotification](/docs/reference/sdk/interfaces/coworknotification/) * [CoworkOpenedFile](/docs/reference/sdk/interfaces/coworkopenedfile/) * [CoworkQueueEntry](/docs/reference/sdk/interfaces/coworkqueueentry/) * [CoworkQueueFile](/docs/reference/sdk/interfaces/coworkqueuefile/) * [CoworkQueueInfo](/docs/reference/sdk/interfaces/coworkqueueinfo/) * [CoworkQueueReceiver](/docs/reference/sdk/interfaces/coworkqueuereceiver/) * [CoworkQueueTask](/docs/reference/sdk/interfaces/coworkqueuetask/) * [CoworkRoutineContext](/docs/reference/sdk/interfaces/coworkroutinecontext/) * [CoworkRoutineRun](/docs/reference/sdk/interfaces/coworkroutinerun/) * [CoworkSdk](/docs/reference/sdk/interfaces/coworksdk/) * [CoworkStatusBarItem](/docs/reference/sdk/interfaces/coworkstatusbaritem/) ## Type Aliases [Section titled “Type Aliases”](#type-aliases) * [CoworkQueueFileInput](/docs/reference/sdk/type-aliases/coworkqueuefileinput/) * [CoworkTaskStatus](/docs/reference/sdk/type-aliases/coworktaskstatus/) * [CoworkTaskView](/docs/reference/sdk/type-aliases/coworktaskview/) # CoworkQueueFileInput ```ts type CoworkQueueFileInput = | { name?: string; path: string; } | { name: string; text: string; } | { base64: string; name: string; }; ``` # CoworkTaskStatus ```ts type CoworkTaskStatus = "todo" | "in_progress" | "needs_review" | "done" | "failed"; ``` # CoworkTaskView ```ts type CoworkTaskView = | "needs_you" | CoworkTaskStatus | "all"; ``` The Tasks view's views: Needs you (needs review, or to do for the owner), each status, all.