Skip to content

CoworkRoutineContext

The context a routine's default export receives: export default async (args, ctx: CoworkRoutineContext) => result. A deployed routine (cowork deploy) gets CoworkDeployedRoutineContext instead.

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

Promise<string | null>

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.

Promise<{ authenticated: boolean; user: | { displayName: string | null; email: string | null; uid: string; } | null; }>


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

Parameter Type
count number

Promise<void>


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

Promise<{ ok: boolean; url: string; }>

click: (refOrSelector) => Promise<{
found: boolean;
label?: string;
role?: string;
}>;
Parameter Type
refOrSelector string

Promise<{ found: boolean; label?: string; role?: string; }>

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 Parameter Default type
T unknown
Parameter Type
js string
opts? { tabIndex?: number; world?: "isolated" | "main"; }
opts.tabIndex? number
opts.world? "isolated" | "main"

Promise<T>

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.

Parameter Type
refOrSelector string
value string
submit? boolean

Promise<{ found: boolean; ok?: boolean; value?: string; }>

find: (query, limit?) => Promise<{
matches: object[];
query: string;
}>;

Elements whose label matches query, with their refs (limit: default 10, at most 50).

Parameter Type
query string
limit? number

Promise<{ matches: object[]; query: string; }>

forward: () => Promise<{
ok: boolean;
url: string;
}>;

Promise<{ ok: boolean; url: string; }>

hover: (refOrSelector) => Promise<{
found: boolean;
}>;
Parameter Type
refOrSelector string

Promise<{ found: boolean; }>

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.

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"

Promise<{ id: string; ok: boolean; persist: boolean; url: string; value: unknown; world: string; }>

injections: () => Promise<{
injections: object[];
}>;

Promise<{ injections: object[]; }>

navigate: (url) => Promise<{
ok: boolean;
url: string;
}>;
Parameter Type
url string

Promise<{ ok: boolean; url: string; }>

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

Parameter Type
url? string

Promise<{ ok: boolean; url: string; }>

press: (key) => Promise<{
key: string;
ok: boolean;
}>;

"Return", "Tab", "cmd+a", …

Parameter Type
key string

Promise<{ key: string; ok: boolean; }>

read: (maxChars?) => Promise<{
text: string;
title: string;
url: string;
}>;

The page's visible text (maxChars: default 20000, at most 100000).

Parameter Type
maxChars? number

Promise<{ text: string; title: string; url: string; }>

reload: () => Promise<{
ok: boolean;
url: string;
}>;

Promise<{ ok: boolean; url: string; }>

screenshot: () => Promise<{
height: number;
path: string;
width: number;
}>;

A JPEG of the page, written to a temporary file — its path.

Promise<{ height: number; path: string; width: number; }>

scroll: (opts?) => Promise<{
scrolledTo?: string;
scrollY?: number;
}>;

Scroll an element into view (ref), or the page by dy pixels (default 600).

Parameter Type
opts? { dy?: number; ref?: string; }
opts.dy? number
opts.ref? string

Promise<{ scrolledTo?: string; scrollY?: number; }>

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.

Parameter Type
maxNodes? number

Promise<{ elements: string[]; title: string; truncated: boolean; url: string; }>

status: () => Promise<{
host?: "pane" | "tabs";
open: boolean;
shell?: string;
tabs: object[];
}>;

Whether the browser has tabs open, and which.

Promise<{ host?: "pane" | "tabs"; open: boolean; shell?: string; tabs: object[]; }>

tabs: object;
activate: (index) => Promise<{
active: number;
}>;
Parameter Type
index number

Promise<{ active: number; }>

close: (index) => Promise<{
count: number;
}>;
Parameter Type
index number

Promise<{ count: number; }>

list: () => Promise<{
tabs: object[];
}>;

Promise<{ tabs: object[]; }>

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.

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

Promise<{ count: number; existing?: boolean; id?: string; index: number; }>

toggle: (show?) => Promise<{
open: boolean;
}>;
Parameter Type
show? boolean

Promise<{ open: boolean; }>

type: (text, refOrSelector?, submit?) => Promise<{
found: boolean;
ok: boolean;
}>;

Type text into the element (or the focused one); submit presses Enter after.

Parameter Type
text string
refOrSelector? string
submit? boolean

Promise<{ found: boolean; ok: boolean; }>

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.

Parameter Type
id? string

Promise<{ ok: boolean; removed: string[]; }>

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

Parameter Type
opts { ref?: string; text?: string; timeoutMs?: number; }
opts.ref? string
opts.text? string
opts.timeoutMs? number

Promise<{ error?: string; ok: boolean; waitedMs?: number; }>


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: (command, opts?) => Promise<{
code: number;
stderr: string;
stdout: string;
}>;
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

Promise<{ code: number; stderr: string; stdout: string; }>


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: (opts) => Promise<{
granted: boolean;
reason?: string;
ticket?: string;
}>;
Parameter Type
opts { command?: string; description?: string; title?: string; value: string; }
opts.command? string
opts.description? string
opts.title? string
opts.value string

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


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: () => Promise<{
removed: string[];
revoked: boolean;
}>;

Forget it, revoked at the service when it can be — for every capability that shares it.

Promise<{ removed: string[]; revoked: boolean; }>

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.

Parameter Type
url string | URL
init? RequestInit & object

Promise<Response>

fields: () => Record<string, string>;

A key's fields by name, e.g. { token } — NotConnected naming the ones not pasted yet.

Record<string, string>

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.

Promise<{ authUrl: string; }>

status: () => object;

Whether it is connected with everything this capability asks for, and as whom — never a token.

object

account?: object;
email: string;
connected: boolean;
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.

Parameter Type
opts? { force?: boolean; }
opts.force? boolean

Promise<string>


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

readonly dir: string;

Workspace-relative: .cowork/data/<app>.

fileName: (title, fallback) => string;

A title as a safe file name (≤ 60 characters), or fallback.

Parameter Type
title unknown
fallback string

string

list: (subdir?) => string[];

The files in the folder or a subfolder, sorted (dot files left out).

Parameter Type
subdir? string

string[]

path: (name) => string;

A name's workspace-relative path — what cowork.openFile and fileRouting take.

Parameter Type
name string

string

read: <T>(name, fallback?) => T;

A JSON file's value; fallback when it is missing or not JSON.

Type Parameter Default type
T unknown
Parameter Type
name string
fallback? T

T

remove: (name) => boolean;

Delete a file; false when there was none.

Parameter Type
name string

boolean

write: (name, value) => string;

Write a value as JSON (atomically) → its workspace-relative path.

Parameter Type
name string
value unknown

string


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.

Parameter Type
path string
exec: (sql, params?) => CoworkDbExecResult;

One write or DDL statement — a string holding a second statement throws.

Parameter Type
sql string
params? unknown[]

CoworkDbExecResult

query: (sql, params?) => CoworkDbQueryResult;

One statement's rows. Meant for reads: it does not refuse a write, but exec is the call for writes.

Parameter Type
sql string
params? unknown[]

CoworkDbQueryResult

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 Parameter
T
Parameter Type
fn () => T

T


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.

Parameter Type
name string
message string

Error


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: object;

Leave a learning / error note for a skill in the skill journal (source defaults to "routine:").

add: (entry) => 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

CoworkJournalEntry


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.

Parameter Type
...parts unknown[]

void


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.

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>


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.

Parameter Type
name string

object

ack: (id, receipt, opts?) => CoworkQueueTask;
Parameter Type
id number
receipt string
opts? { files?: CoworkQueueFileInput[]; result?: string; review?: boolean; to?: string; }
opts.files? CoworkQueueFileInput[]
opts.result? string
opts.review? boolean
opts.to? string

CoworkQueueTask

approve: (id) => CoworkQueueTask;
Parameter Type
id number

CoworkQueueTask

counts: () => Record<CoworkTaskView, number>;

Record<CoworkTaskView, number>

list: (opts?) => CoworkQueueTask[];
Parameter Type
opts? { agent?: string; before?: number; limit?: number; q?: string; view?: CoworkTaskView; }
opts.agent? string
opts.before? number
opts.limit? number
opts.q? string
opts.view? CoworkTaskView

CoworkQueueTask[]

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

CoworkQueueTask

peek: (id) =>
| CoworkQueueTask
| null;
Parameter Type
id number

| CoworkQueueTask | null

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

| CoworkQueueTask & object | null

reject: (id, comment) => CoworkQueueTask;
Parameter Type
id number
comment string

CoworkQueueTask

reply: (id, body, opts?) => CoworkQueueTask;
Parameter Type
id number
body string
opts? { as?: string; files?: CoworkQueueFileInput[]; ref?: string; to?: string | null; }
opts.as? string
opts.files? CoworkQueueFileInput[]
opts.ref? string
opts.to? string | null

CoworkQueueTask

retry: (id) => CoworkQueueTask;
Parameter Type
id number

CoworkQueueTask

send: (body, opts?) => CoworkQueueTask;
Parameter Type
body string
opts? { as?: string; delayS?: number; files?: CoworkQueueFileInput[]; meta?: Record<string, unknown>; ref?: string; review?: boolean; subject?: string; to?: string; }
opts.as? string
opts.delayS? number
opts.files? CoworkQueueFileInput[]
opts.meta? Record<string, unknown>
opts.ref? string
opts.review? boolean
opts.subject? string
opts.to? string

CoworkQueueTask


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

Promise<unknown>


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: (name) => string | undefined;
Parameter Type
name string

string | undefined

list: () => string[];

The names this routine may read (never values).

string[]

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

Parameter Type
name string
value string

void


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.

void


workspaceRoot: string;

The workspace folder's path. A routine does not run with it as its working directory: resolve workspace files against it.