Skip to content

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.

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: () => void;

void

clearSelection: () => void;

Clears only the select() slot; highlighted-text capture is unaffected.

void

select: (text) => void;

Publish a synthetic "selection" (list/canvas UIs — one item per line). COMPOSES with any highlighted text — the agent sees both.

Parameter Type
text string

void

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.

Parameter Type
enabled boolean

void

setContext: (value) => void;

Extra app context for the agent (objects are JSON-stringified).

Parameter Type
value unknown

void


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: (cb) => () => void;
Parameter Type
cb (message) => void

() => void

post: (message) => void;
Parameter Type
message unknown

void


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.

Parameter Type
count number

Promise<void>


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: (url) => Promise<{
embedded: boolean;
}>;
Parameter Type
url string

Promise<{ embedded: boolean; }>

status: () => Promise<
| {
open: boolean;
tabs: object[];
}
| null>;

Promise< | { open: boolean; tabs: object[]; } | null>


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: () =>
| CoworkCaptureSession
| null;

This app's recording in progress, if any — to re-attach after the pane remounts.

| CoworkCaptureSession | null

record: (opts?) => Promise<CoworkCaptureSession>;
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"

Promise<CoworkCaptureSession>

shot: (opts?) => Promise<{
image?: Blob;
path?: string;
}>;
Parameter Type
opts? { file?: string; image?: boolean; source?: "window" | "screen"; }
opts.file? string
opts.image? boolean
opts.source? "window" | "screen"

Promise<{ image?: Blob; path?: string; }>


chat: object;

Send a message to the workbench chat panel: opens the chat and SUBMITS the text as a query. Fire-and-forget.

ask: (text) => void;
Parameter Type
text string

void


closeTab: () => void;

Close this app's editor tab (as if the user hit × on it). No-op in a sidebar viewpane.

void


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: (opts) => Promise<{
granted: boolean;
ticket?: string;
}>;
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

Promise<{ granted: boolean; ticket?: string; }>


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: (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.

Parameter Type
name string
fields? Record<string, string>

Promise<void>

get: (name) => Promise<CoworkConnection>;

The connection — ready when state is "connected".

Parameter Type
name string

Promise<CoworkConnection>


identity: object;

Who is signed in: currentUser() → {uid, email}, or null when signed out.

currentUser: () => Promise<
| {
email: string | null;
uid: string;
}
| null>;

Promise< | { email: string | null; uid: string; } | null>


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: (entry) => Promise<CoworkJournalEntry>;
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

Promise<CoworkJournalEntry>

entries: (skill, opts?) => Promise<CoworkJournalEntry[]>;
Parameter Type
skill string
opts? { limit?: number; status?: "all" | "open" | "applied"; }
opts.limit? number
opts.status? "all" | "open" | "applied"

Promise<CoworkJournalEntry[]>

resolve: (skill, ids?) => Promise<{
resolved: number;
}>;
Parameter Type
skill string
ids? number[]

Promise<{ resolved: number; }>

skills: () => Promise<object[]>;

Promise<object[]>


log: (...parts) => void;

Emit a log line to the "Cowork" Output panel.

Parameter Type
...parts unknown[]

void


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.

Parameter Type
n { body?: string; tag?: string; title: string; urgent?: boolean; }
n.body? string
n.tag? string
n.title string
n.urgent? boolean

Promise<void>


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.

Parameter Type
cb (n) => void

() => void


onTitleAction: (handler) => () => void;

Subscribe to pane title-action button clicks (the standalone runner's panes).

Parameter Type
handler (id) => void

() => void


onVisibility: (cb) => () => void;

Subscribe to visibility flips. Returns the unsubscribe function.

Parameter Type
cb (visible) => void

() => void


openedFile?: CoworkOpenedFile;

Present only when opened via fileRouting (editor tab).


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.

Parameter Type
url string

void


openFile: (path) => void;

Open a workspace file (routes through fileRouting when it matches).

Parameter Type
path string

void


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: (queue, id, receipt, opts?) => Promise<CoworkQueueTask>;
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

Promise<CoworkQueueTask>

approve: (queue, id) => Promise<CoworkQueueTask>;
Parameter Type
queue string
id number

Promise<CoworkQueueTask>

create: (queue, opts?) => Promise<CoworkQueueInfo>;
Parameter Type
queue string
opts? { leaseS?: number; maxAttempts?: number; review?: boolean; }
opts.leaseS? number
opts.maxAttempts? number
opts.review? boolean

Promise<CoworkQueueInfo>

items: (queue, opts?) => Promise<CoworkQueueTask[]>;
Parameter Type
queue string
opts? { agent?: string; before?: number; limit?: number; view?: CoworkTaskView; }
opts.agent? string
opts.before? number
opts.limit? number
opts.view? CoworkTaskView

Promise<CoworkQueueTask[]>

list: () => Promise<CoworkQueueInfo[]>;

Promise<CoworkQueueInfo[]>

nack: (queue, id, receipt, opts?) => Promise<CoworkQueueTask>;
Parameter Type
queue string
id number
receipt string
opts? { delayS?: number; error?: string; }
opts.delayS? number
opts.error? string

Promise<CoworkQueueTask>

peek: (queue, id) => Promise<
| CoworkQueueTask
| null>;
Parameter Type
queue string
id number

Promise< | CoworkQueueTask | null>

purge: (queue, status?) => Promise<{
purged: number;
}>;
Parameter Type
queue string
status? "done" | "failed" | "all"

Promise<{ purged: number; }>

receive: (queue, opts?) => Promise<
| CoworkQueueTask & object
| null>;
Parameter Type
queue string
opts? { as?: string; leaseS?: number; mine?: boolean; }
opts.as? string
opts.leaseS? number
opts.mine? boolean

Promise< | CoworkQueueTask & object | null>

receivers: (queue?) => Promise<CoworkQueueReceiver[]>;
Parameter Type
queue? string

Promise<CoworkQueueReceiver[]>

reject: (queue, id, comment, opts?) => Promise<CoworkQueueTask>;
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; })[]

Promise<CoworkQueueTask>

remove: (queue, id?) => Promise<{
removed: boolean;
}>;
Parameter Type
queue string
id? number

Promise<{ removed: boolean; }>

reply: (queue, id, body, opts?) => Promise<CoworkQueueTask>;
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

Promise<CoworkQueueTask>

retry: (queue, id) => Promise<CoworkQueueTask>;
Parameter Type
queue string
id number

Promise<CoworkQueueTask>

send: (queue, task) => Promise<CoworkQueueTask>;
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

Promise<CoworkQueueTask>

summary: () => Promise<{
journal: number;
needsYou: number;
}>;

Promise<{ journal: number; needsYou: number; }>

tasks: (opts?) => Promise<{
agents: CoworkQueueReceiver[];
counts: Record<CoworkTaskView, number>;
queues: CoworkQueueInfo[];
tasks: CoworkQueueTask[];
}>;
Parameter Type
opts? { agent?: string; limit?: number; q?: string; queue?: string; view?: CoworkTaskView; }
opts.agent? string
opts.limit? number
opts.q? string
opts.queue? string
opts.view? CoworkTaskView

Promise<{ agents: CoworkQueueReceiver[]; counts: Record<CoworkTaskView, number>; queues: CoworkQueueInfo[]; tasks: CoworkQueueTask[]; }>


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: (id, args?, opts?) => Promise<unknown>;
Parameter Type
id string
args? unknown
opts? { timeoutMs?: number; }
opts.timeoutMs? number

Promise<unknown>

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.

Parameter Type
id? string
opts? { app?: string; }
opts.app? string

Promise<CoworkRoutineRun[]>

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() }, []).

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

object

close: () => void;

void

refresh: () => Promise<void>;

Promise<void>


setTitleActions: (actions) => void;

Replace the pane title-action buttons (the standalone runner's panes).

Parameter Type
actions object[]

void


state: object;

Persisted per-view state (survives hide/show + reloads).

get: <T>() => T | undefined;
Type Parameter Default type
T unknown

T | undefined

set: (value) => void;
Parameter Type
value unknown

void


statusBar: object;

The apps' footer items (see CoworkStatusBarItem), keyed by app name.

click: (app) => void;

Act as if the user clicked the app's item.

Parameter Type
app string

void

get: (app) =>
| CoworkStatusBarItem
| null;

An app's item as last drawn (data included), or null.

Parameter Type
app string

| CoworkStatusBarItem | null

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.

Parameter Type
cb (app, item) => void

() => void

refresh: (app?) => void;

Re-run an app's statusBar routine now (all apps without one) — after your widget changed what it reports.

Parameter Type
app? string

void


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: (opts) => Promise<{
close: () => Promise<void>;
id: string;
write: (data) => Promise<void>;
}>;
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 -

Promise<{ close: () => Promise<void>; id: string; write: (data) => Promise<void>; }>


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: (opts) => Promise<{
changes: number;
lastInsertRowid: number;
}>;
Parameter Type
opts { db: string; params?: unknown[]; sql: string; }
opts.db string
opts.params? unknown[]
opts.sql string

Promise<{ changes: number; lastInsertRowid: number; }>

query: (opts) => Promise<{
columns: string[];
rowCount: number;
rows: unknown[][];
truncated: boolean;
}>;
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

Promise<{ columns: string[]; rowCount: number; rows: unknown[][]; truncated: boolean; }>

schema: (opts) => Promise<{
tables: object[];
}>;
Parameter Type
opts { db: string; }
opts.db string

Promise<{ tables: object[]; }>


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: (opts?) => Promise<boolean>;
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

Promise<boolean>

dialog: (opts) => Promise<{
label: string | null;
values: Record<string, unknown>;
}>;
Parameter Type
opts CoworkDialog

Promise<{ label: string | null; values: Record<string, unknown>; }>


viewSource: () => void;

Open this app's SOURCE file (editor tabs: the routed document) in the plain text editor — the "view code" affordance.

void


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: 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.