# Operation reference

Look up API actions, their arguments, defaults and result types.

Find the API action you need by resource below. Each entry gives its name, arguments and result type. These declarations are generated from Cabane's API; [Requests and responses](https://cabane.ai/docs/requests-and-responses) explains the request format and error handling.

To call an entry marked `// query`, send a `POST` to `https://app.cabane.ai/api/workspaces/{workspaceId}/ops/query`. For `// mutate`, use the same URL ending in `/mutate`. Put the entry's name in `op` and its named arguments in `args`. For example, `files.read` becomes:

```json
{"op": "files.read", "args": {"path": "/reports/weekly.md"}}
```

Send a bearer token and `Content-Type: application/json`; [First API request](https://cabane.ai/docs/first-api-request) has a complete curl example.

In the declarations, `?` marks an optional argument, `=` gives its default, and `id|reference` means either named argument is accepted. The type after `→` describes the successful `result`. Brackets state limits and conditions, such as `[requires overwrite=true]`.

For the full definitions of an entry's input and result types, send its name to `/ops/describe`:

```json
{"op": "files.read"}
```

The response includes the declaration and the types it refers to. Sending `{}` lists the available operations. This is also useful when your code needs to discover the API on a particular Cabane server.

## agents

### `agents.activate`

Turn a deactivated agent back on — a reversible off/on that isn't deletion (a deactivated agent keeps their config + handle but is skipped by dispatch).

```
// mutate
agents.activate(id|handle:string) → AgentLifecycleReceipt
```

### `agents.create`

Create an in-app agent.

```
// mutate
agents.create(
  name:string,
  handle:string,
  charter?:string|null
) → AgentLifecycleReceipt
```

### `agents.deactivate`

Deactivate an agent — a reversible off (config + handle kept; skipped by dispatch).

```
// mutate
agents.deactivate(id|handle:string) → AgentLifecycleReceipt
```

### `agents.delete`

Delete (soft-archive/revoke) an agent — IRREVERSIBLE (there's no un-delete; create a fresh agent instead).

```
// mutate
agents.delete(id|handle:string) → AgentDeleteReceipt
```

### `agents.list`

The workspace's agents (your peers).

```
// query
agents.list(
  cursor?:string,
  maxBytes?:number=16 KiB,
  includeInactive?:boolean=false
) → {agents,more}
```

### `agents.read`

Read one agent by `id` or `handle`, whichever you hold, including public details and their look.

```
// query
agents.read(id|handle:string) → AgentReadResult
```

### `agents.update`

The one configure verb — set name, handle, charter, and run-config in one partial, atomic write.

```
// mutate
agents.update(
  id|handle:string,
  name?:string,
  newHandle?:string,
  charter?:string|null,
  runConfig?:Record<string,unknown>,
  color?:number,
  shape?:'round'|'square'|'drop'|'hex',
  topper?:'none'|'sprout'|'antenna',
  identityEditing?:'members'|'owner',
  roomPlacement?:'members'|'owner'
) → AgentLifecycleReceipt & { clearedConversationCount: number }
```

## asks

### `asks.list`

Asks across the workspace, newest first, filtered before paging.

```
// query
asks.list(
  roomId|roomSlug?:string,
  createdAfter?:string,
  createdBefore?:string,
  resolvedAfter?:string[requires state!=open],
  resolvedBefore?:string[requires state!=open],
  target?:string,
  asker?:string,
  state?:AskState|AskState[]=open,
  conversationId?:string,
  order?:'asc'|'desc'=desc,
  limit?:number=50,
  cursor?:string,
  maxBytes?:number=16 KiB
) → {asks,more}
```

## boards

### `boards.create`

Create a workroom's board, with a required workspace-unique prefix of 1–12 letters.

```
// mutate
boards.create(
  name:string,
  prefix:string,
  roomId:string,
  description?:string
) → { board: TaskBoardSummary }
```

### `boards.delete`

Delete the board and every task on it, each as `tasks.delete` does.

```
// mutate
boards.delete(
  id|slug|prefix:string,
  ifVersion:number
) → { deleted: true; board: TaskBoardSummary }
```

### `boards.list`

Workspace boards, each belonging to one workroom, at most one per room.

```
// query
boards.list(
  limit?:number=100,
  cursor?:string,
  maxBytes?:number=16 KiB,
  includeArchived?:boolean=false
) → {boards,more}
```

### `boards.read`

Board metadata by `id`, `slug` or `prefix`, whichever you hold — an object, never JSON text.

```
// query
boards.read(
  id|slug|prefix:string,
  maxBytes?:number=16 KiB,
  part?:string,
  startLine?:number,
  startOffset?:number,
  ifVersion?:number,
  ifSha?:string
) → TaskBoardReadResult{more,…}
```

### `boards.update`

Rename a board, edit its description, or set the template a task created on it takes by default — never renumbering its tasks.

```
// mutate
boards.update(
  id|slug|prefix:string,
  ifVersion:number,
  name?:string,
  description?:string,
  defaultTemplate?:string|null
) → { board: TaskBoardSummary }
```

## browser

### `browser.close`

Close this conversation's shared browser when the job is done and nobody is watching.

```
// mutate
browser.close() → BrowserStatus
```

### `browser.open`

Open this conversation's shared browser, or return the one already open (`created: false`).

```
// mutate
browser.open(fresh?:boolean=false) → BrowserOpenResult
```

### `browser.status`

Where this conversation's shared browser stands: available in the workspace, attached to your `browser` server this turn, and the live session if there is one.

```
// query
browser.status() → BrowserStatus
```

## context

### `context.around`

Orient around a thing you've found — it PLUS its immediate connected context (the docs where the thinking lives, the threads it spawned, who's involved).

```
// query
context.around(
  ref:string,
  kind?:GraphNodeKind,
  maxBytes?:number=16 KiB
) → ContextAroundResult
```

### `context.conversationsSince`

The CONVERSATIONS that moved in a time range (the `moved` list), plus the files each touched and who took part — each moved conversation's own `participants`, exactly as on `around`.

```
// query
context.conversationsSince(
  from:string,
  until?:string,
  roomId|roomSlug?:string,
  involving?:string,
  limit?:number=20,
  maxBytes?:number=16 KiB,
  cursor?:string
) → ContextSinceResult{moved,more,…}
```

### `context.fileChanges`

Recent FILE/FOLDER change events (the compact projection), newest-first — who touched what.

```
// query
context.fileChanges(
  from?:string,
  until?:string,
  under?:string,
  types?:string[],
  actor?:string,
  limit?:number=25,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → {changes,more}
```

### `context.references`

The BATCHED edge primitive: "what is each of these objects connected to, and when did each reference form?" One hop of graph neighbours of things you already know — the context-cluster sibling of `around` (which carries the shaped seed).

```
// query
context.references(
  ref:string,
  limit?:number=20,
  order?:'asc'|'desc'=desc,
  edges?:string[],
  to?:string[],
  direction?:'out'|'in'|'any'=any,
  since?:string,
  preview?:number=0,
  maxBytes?:number=16 KiB,
  cursor?:string
) → ReferencesResult{references,more,…}
```

### `context.referencesMany`

The batch form of `references`: up to 25 refs (paths and conversation ids mixable) → `{ results, unresolved, truncated }`, one result per ref with `truncated` saying its per-source limit filled.

```
// query
context.referencesMany(
  refs:string[],
  limit?:number=20,
  order?:'asc'|'desc'=desc,
  edges?:string[],
  to?:string[],
  direction?:'out'|'in'|'any'=any,
  since?:string,
  preview?:number=0
) → ReferencesBatchResult{results,unresolved,…}
```

## conversations

### `conversations.create`

Open a conversation in the required room (`roomId` or `roomSlug`), which must be a WORKROOM.

```
// mutate
conversations.create(
  body:string,
  roomId|roomSlug:string,
  modifiedEntries?:{ kind: 'file'|'folder'; id: string }[],
  title?:string[≤72],
  idempotencyKey?:string,
  agents?:ConversationSeatInput[]
) → { conversation: Conversation; message: PostedMessageRow; replayed?: true; unappliedSeats?: Array<{ agentId: string; reason: 'agent_unavailable' | 'execution_config_stale' | 'execution_locked' }> } & AddressedReport
```

### `conversations.delete`

The one HARD delete — drops the conversation row and every message in the thread (cascade).

```
// mutate
conversations.delete(
  id:string
) → ConversationDeleteManifest{cancelFailures,…}
```

### `conversations.follow`

Make people FOLLOW a conversation — quietly.

```
// mutate
conversations.follow(
  id:string,
  followers:string|string[]
) → ConversationParticipants{roomId,followers,agents,…}
```

### `conversations.list`

The one list verb, and it FILTERS NOTHING by default: a bare page is every conversation in the workspace — scheduled runs included, one flat page ordered by activity.

```
// query
conversations.list(
  roomId|roomSlug?:string,
  contains?:string,
  participant?:string,
  involving?:string,
  ids?:string[],
  updatedAfter?:string,
  updatedBefore?:string,
  createdAfter?:string,
  createdBefore?:string,
  relevantTo?:string,
  scheduled?:boolean,
  scheduleId?:string,
  taskId|taskReference?:string,
  working?:boolean,
  queued?:boolean,
  finished?:boolean,
  sort?:'activity'|'created'=activity,
  total?:boolean=false,
  summary?:boolean=true,
  cursor?:string,
  limit?:number=25,
  maxBytes?:number=16 KiB,
  order?:'asc'|'desc'=desc
) → {conversations,more,…}
```

### `conversations.listMessages`

A page of the conversation as a person reads it.

```
// query
conversations.listMessages(
  id:string,
  include?:'narrative'|'all'=narrative,
  turnId?:string,
  author?:string,
  mentions?:string,
  landedAfter?:string,
  landedBefore?:string,
  limit?:number=20,
  order?:'asc'|'desc'=desc,
  maxBytes?:number=16 KiB,
  cursor?:string,
  afterId?:string[excludes cursor,order=desc],
  ids?:string[][excludes limit,cursor,order,afterId,include,turnId,author,mentions,landedAfter,landedBefore],
  edges?:boolean=false
) → {messages,more}
```

### `conversations.participants`

Who follows THIS thread, and which agents have worked in it.

```
// query
conversations.participants(
  id:string
) → ConversationParticipants{roomId,followers,agents,…}
```

### `conversations.post`

Post into an existing conversation.

```
// mutate
conversations.post(
  id:string,
  body:string,
  modifiedEntries?:{ kind: 'file'|'folder'; id: string }[],
  idempotencyKey?:string
) → { message: PostedMessageRow; replayed?: true } & AddressedReport
```

### `conversations.read`

One conversation's row.

```
// query
conversations.read(id:string) → { conversation: Conversation }
```

### `conversations.readMessage`

One message, whole — the read a row's `more` points at, and **the only place an ask is expanded in full**: every headline, every answer, the addendum, through whichever message you name (the one that poses the ask or the one that resolves …

```
// query
conversations.readMessage(
  id:string,
  startLine?:number=1,
  endLine?:number,
  maxBytes?:number=16 KiB,
  startOffset?:number,
  ifSha?:string,
  part?:'body'|'addendum'|`answer:${string}`=body
) → MessageRead{id,authorUserId,agentId,turnId,more,…}
```

### `conversations.unfollow`

The paired verb.

```
// mutate
conversations.unfollow(
  id:string,
  followers:string|string[]
) → ConversationParticipants{roomId,followers,agents,…}
```

### `conversations.update`

Retitle a conversation, or write its summary.

```
// mutate
conversations.update(
  id:string,
  title?:string|null[≤72],
  summary?:string|null[≤2400],
  ifSummaryVersion?:number
) → { conversation: Conversation }
```

## devices

### `devices.list`

Every device brought to this workspace, the same list for every member and agent here: its owner, `availability` and `state`, each harness's state, companion `version`, `lastSeenAt`, `placement`, and which of this workspace's agents run on…

```
// query
devices.list(cursor?:string, maxBytes?:number=16 KiB) → {devices,more}
```

### `devices.read`

One device brought to this workspace, by its `id` (from `devices.list`, or an agent's `executor.deviceId` on `agents.read`), with the agents assigned to it, ordered by id.

```
// query
devices.read(
  id:string,
  cursor?:string,
  maxBytes?:number=16 KiB
) → {more,…}
```

## docs

### `docs.list`

Cabane's user guides, in reading order: slug, title, summary, last-updated day and public URL.

```
// query
docs.list(cursor?:string, maxBytes?:number=16 KiB) → {guides,more}
```

### `docs.read`

One user guide as Markdown, the same text as its public page.

```
// query
docs.read(
  slug:string,
  startLine?:number=1,
  endLine?:number,
  maxBytes?:number=16 KiB,
  startOffset?:number,
  ifSha?:string
) → DocsGuideContent{more,…}
```

## files

### `files.archive`

Take something out of the way WITHOUT moving it — the row is stamped `archived` at the path it already had, so every link into it still opens.

```
// mutate
files.archive(path:string) → StateManifest{path,entries,…}
```

### `files.copy`

Copy without streaming bytes.

```
// mutate
files.copy(
  from:string,
  to:string,
  recursive?:boolean=false,
  onConflict?:'error'|'rename'=error
) → FileCopyResult{sha256,…}
```

### `files.delete`

Soft-delete.

```
// mutate
files.delete(
  path:string,
  recursive?:boolean=false
) → DeleteManifest{path,entries,…}
```

### `files.edit`

Modify a text file in place, atomically.

```
// mutate
files.edit(
  path:string,
  edits:FileEditInput|FileEditInput[],
  ifSha?:string
) → FileEditResult{path,sha256,…}
```

### `files.import`

The BYTE path (`write` is text-only): the SERVER fetches an https `url` into the workspace at `path` — no bytes ride through model context.

```
// mutate
files.import(
  url:string,
  path:string,
  overwrite?:boolean=false,
  mkdirs?:boolean=true,
  mime?:string
) → FileWriteResult{path,sha256,…}
```

### `files.list`

List a folder (omit `path` for the root `/`).

```
// query
files.list(
  path?:string,
  mime?:string,
  contains?:string,
  updatedAfter?:string,
  updatedBefore?:string,
  recursive?:boolean=false,
  maxBytes?:number=16 KiB
) → FolderListing{path,folders,files,…}
```

### `files.listArchive`

What is ARCHIVED — out of the way at the path it always had, not moved anywhere.

```
// query
files.listArchive(
  from?:string,
  until?:string,
  under?:string,
  rootsOnly?:boolean=false,
  limit?:number=50,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → {entries,more}
```

### `files.listTrash`

The soft-deleted entries (files + folders), most recently deleted first — pass one's `path` to `restore`.

```
// query
files.listTrash(
  from?:string,
  until?:string,
  under?:string,
  rootsOnly?:boolean=false,
  limit?:number=50,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → {entries,more}
```

### `files.listVersions`

The file's append-only version history, newest-first (`order: 'asc'` for v1-up), paginated.

```
// query
files.listVersions(
  path:string,
  limit?:number=50,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → FileVersionsResult{path,fileId,versions,more,…}
```

### `files.mkdir`

Create a folder.

```
// mutate
files.mkdir(path:string, recursive?:boolean=false) → {path,…}
```

### `files.move`

Move or rename.

```
// mutate
files.move(
  from:string,
  to:string
) → { from: string; to: string; kind: 'file' | 'folder'; invalidated: MoveInvalidation }
```

### `files.read`

Read one file.

```
// query
files.read(
  path:string,
  startLine?:number=1,
  endLine?:number,
  maxBytes?:number=16 KiB,
  startOffset?:number,
  ifSha?:string
) → FileRead{path,sha256,more,…}
```

### `files.readMany`

MANY files under a glob, in one call, projected to what you need — `{ entries, more }`, always a collection.

```
// query
files.readMany(
  pattern:string,
  include?:'properties'|'content'|'both'=properties,
  includeArchived?:boolean=false,
  maxBytes?:number=16 KiB,
  cursor?:string
) → ReadManyResult{entries,more,…}
```

### `files.readVersion`

The bytes of one older version (`versionId` from a `listVersions` row).

```
// query
files.readVersion(
  path:string,
  versionId:string,
  startLine?:number=1,
  endLine?:number,
  maxBytes?:number=16 KiB,
  startOffset?:number,
  ifSha?:string
) → VersionContentResult{path,versionId,more,…}
```

### `files.restore`

Lift the tombstone on a soft-deleted entry, addressed by the `path` it had when deleted (match a `listTrash()` entry).

```
// mutate
files.restore(path:string) → RestoreManifest{path,entries,…}
```

### `files.restoreVersion`

Restore an earlier version's bytes onto the file — the recovery half of the history surface.

```
// mutate
files.restoreVersion(
  path:string,
  versionId:string
) → RestoreVersionResult{path,sha256,…}
```

### `files.setProperties`

Set a document's **properties** (its leading `---` block) as data instead of find-and-replacing text.

```
// mutate
files.setProperties(
  path:string,
  properties:Record<string,unknown>,
  ifSha?:string
) → FileEditResult{path,sha256,…}
```

### `files.stat`

A cheap existence/metadata probe that NEVER throws on a missing path — it returns `{ exists: false }`.

```
// query
files.stat(path:string) → FileStat{sha256,…}
```

### `files.unarchive`

Bring an archived entry back to the live tree, addressed by its path.

```
// mutate
files.unarchive(path:string) → StateManifest{path,entries,…}
```

### `files.write`

Create a file.

```
// mutate
files.write(
  path:string,
  content:string,
  overwrite?:boolean=false,
  mkdirs?:boolean=true,
  mime?:string,
  ifSha?:string[requires overwrite=true],
  properties?:'keep'|object[excludes dangerouslyClearProperties],
  dangerouslyClearProperties?:boolean=false
) → FileWriteResult{path,sha256,…}
```

## find

### `find.paths`

Names/paths by glob (anchored at root; `**` crosses segments) — reads no bodies, cheap.

```
// query
find.paths(
  pattern:string,
  maxBytes?:number=16 KiB,
  cursor?:string,
  includeArchived?:boolean=false
) → PathsResult{paths,more,…}
```

### `find.scan`

Every matching LINE under a subtree — a bounded WORKSPACE SCAN that reads file bodies (the EXPENSIVE discovery verb).

```
// query
find.scan(
  pattern:string,
  under?:string,
  includeArchived?:boolean=false,
  ignoreCase?:boolean=false,
  fixed?:boolean=false,
  maxBytes?:number=16 KiB,
  cursor?:string
) → ScanResult{matches,more,…}
```

### `find.search`

Ranked, indexed, CHEAP discovery across file names/content AND conversation titles/messages — the default first move when you don't know where a thing lives.

```
// query
find.search(
  q:string,
  tiers?:Array<'named'|'distilled'|'raw'>,
  from?:{ conversationId?: string; roomId?: string; roomSlug?: string },
  explain?:boolean=false,
  matchOn?:'name'|'content'|'all'=all,
  types?:Array<'files'|'conversations'|'tasks'>,
  conversationId?:string[excludes roomId,roomSlug,under],
  author?:string,
  mentions?:string,
  landedAfter?:string,
  landedBefore?:string,
  conversationMode?:'grouped'|'messages'=grouped,
  roomId|roomSlug?:string,
  under?:string,
  limit?:number=30,
  maxBytes?:number=16 KiB,
  cursor?:string,
  includeArchived?:boolean=false
) → SearchResult{tiers,results,more,…}
```

## people

### `people.list`

The workspace's people (owner first), each `{ id, membershipId, name, handle, avatarUrl, email, role }`.

```
// query
people.list(cursor?:string, maxBytes?:number=16 KiB) → {members,more}
```

## rooms

### `rooms.archive`

Soft-archive a room — which CLOSES it (CT1639).

```
// mutate
rooms.archive(id|slug:string) → { room: RoomRow }
```

### `rooms.create`

Create a PUBLIC room and its `/rooms/{slug}` reserved folder (with its `ABOUT.md` charter).

```
// mutate
rooms.create(
  name:string[≤80],
  type?:'chatroom'|'workroom'=workroom,
  charter?:string,
  people?:string|string[],
  defaultAgent?:string|null,
  visibility?:'public'=public
) → { room: RoomRow }
```

### `rooms.join`

Put people in the room — quietly.

```
// mutate
rooms.join(
  id|slug:string,
  people:string|string[]
) → RoomReadResult{people,agents,pins,…}
```

### `rooms.leave`

Take people out of the room — the paired verb.

```
// mutate
rooms.leave(
  id|slug:string,
  people:string|string[]
) → RoomReadResult{people,agents,pins,…}
```

### `rooms.list`

The workspace's rooms.

```
// query
rooms.list(
  cursor?:string,
  maxBytes?:number=16 KiB,
  includeArchived?:boolean=false,
  member?:string,
  type?:'chatroom'|'workroom'
) → {rooms,more}
```

### `rooms.pin`

Pin one of the room's OWN files to it — the shortlist under the room header's Files chevron, where everyone in the room sees it.

```
// mutate
rooms.pin(
  id|slug:string,
  path:string
) → RoomReadResult{people,agents,pins,…}
```

### `rooms.read`

One room whole: its row, the path of its charter (`ABOUT.md`), the people who joined it and the agents in it.

```
// query
rooms.read(id|slug:string) → RoomReadResult{people,agents,pins,…}
```

### `rooms.repairFolder`

Repair legacy record/folder desync by moving the named reserved room folder to the record's canonical `homeFolder`.

```
// mutate
rooms.repairFolder(
  id|slug:string,
  fromPath:string,
  replaceGeneratedHome?:boolean=false
) → RoomFolderRepairReceipt
```

### `rooms.unarchive`

Restore a soft-archived room to the sidebar, its reserved folder with it — and reopen it: posting, opening conversations and waking work again (CT1639).

```
// mutate
rooms.unarchive(id|slug:string) → { room: RoomRow }
```

### `rooms.unpin`

Unpin a file from the room.

```
// mutate
rooms.unpin(
  id|slug:string,
  path:string
) → RoomReadResult{people,agents,pins,…}
```

### `rooms.update`

The one configure verb — change a room's name, description or seated agent in one partial write.

```
// mutate
rooms.update(
  id|slug:string,
  name?:string[≤80],
  description?:string|null[≤1000],
  defaultAgent?:string|null,
  owner?:string
) → { room: RoomRow }
```

## schedules

### `schedules.cancel`

Cancel a recurring schedule — a terminal stop (use `pause` for a reversible hold).

```
// mutate
schedules.cancel(id:string) → { schedule: ScheduleRow }
```

### `schedules.create`

Create a recurring schedule that dispatches `agent` on the cron cadence, into exactly one target: `targetRoomId`, where each fire opens a new conversation, or `targetConversationId`, where each fire posts a message into that existing conve…

```
// mutate
schedules.create(
  agent:string,
  title:string,
  prompt:string[≤16KiB],
  targetRoomId|targetRoomSlug|targetConversationId:string,
  cron:string,
  tz:string,
  principal?:'creator'|'workspace'=creator
) → { schedule: ScheduleRow }
```

### `schedules.list`

The workspace's live RECURRING schedules (active/paused), soonest-created first.

```
// query
schedules.list(
  agent?:string,
  state?:ScheduleState|ScheduleState[],
  targetRoomId|targetRoomSlug?:string,
  targetConversationId?:string,
  limit?:number=25,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → {schedules,more}
```

### `schedules.pause`

Pause a recurring schedule — stops firing until resumed (missed fires are skipped, never backfilled).

```
// mutate
schedules.pause(id:string) → { schedule: ScheduleRow }
```

### `schedules.read`

One schedule whole — either kind, any state — including the standing `prompt` a list row only excerpts.

```
// query
schedules.read(id:string) → { schedule: ScheduleView }
```

### `schedules.resume`

Resume a paused recurring schedule — its next run is recomputed from now.

```
// mutate
schedules.resume(id:string) → { schedule: ScheduleRow }
```

### `schedules.runNow`

Fire a schedule ONCE, right now — the way to find out whether a schedule works without waiting for its cadence.

```
// mutate
schedules.runNow(
  id:string
) → { schedule: ScheduleRow } & ConversationLabels
```

### `schedules.update`

Patch a recurring schedule — any of title / prompt / recurrence (pass BOTH `cron` and `tz`) / targetRoomId or targetConversationId / agent / principal.

```
// mutate
schedules.update(
  id:string,
  title?:string,
  prompt?:string[≤16KiB],
  cron?:string[requires tz],
  tz?:string[requires cron],
  targetRoomId|targetRoomSlug|targetConversationId?:string,
  agent?:string,
  principal?:'creator'|'workspace'
) → { schedule: ScheduleRow }
```

## skills

### `skills.list`

Every skill reachable this turn, Cabane-origin first, then the turn's room's, then the workspace's.

```
// query
skills.list(cursor?:string, maxBytes?:number=16 KiB) → {skills,more}
```

### `skills.read`

One skill by name — the file as authored, frontmatter included.

```
// query
skills.read(
  name:string,
  startLine?:number=1,
  endLine?:number,
  maxBytes?:number=16 KiB,
  startOffset?:number,
  ifSha?:string
) → SkillContent{path,roomId,more,…}
```

## tags

### `tags.create`

Create a trimmed, case-insensitive unique tag.

```
// mutate
tags.create(name:string) → { tag: TaskTag }
```

### `tags.delete`

Remove an unused tag.

```
// mutate
tags.delete(
  id:string,
  ifVersion:number
) → { deleted: true; tag: TaskTag }
```

### `tags.list`

Workspace-wide tags, paged.

```
// query
tags.list(
  limit?:number=100,
  cursor?:string,
  maxBytes?:number=16 KiB
) → {tags,more}
```

### `tags.update`

Rename a tag everywhere without changing its identity.

```
// mutate
tags.update(id:string, name:string, ifVersion:number) → { tag: TaskTag }
```

## tasks

### `tasks.bind`

Bind the task to an existing conversation in its board's room — your own conversation included.

```
// mutate
tasks.bind(
  id|reference:string,
  ifVersion:number,
  conversationId:string
) → TaskWriteResult{told,…}
```

### `tasks.close`

Close the task now as done or dropped, whatever steps remain, with an optional reason.

```
// mutate
tasks.close(
  id|reference:string,
  ifVersion:number,
  kind:'done'|'dropped',
  reason?:string
) → TaskWriteResult{told,…}
```

### `tasks.complete`

Mark the current step done, with optional `output`.

```
// mutate
tasks.complete(
  id|reference:string,
  ifVersion:number,
  stepId|stepName:string,
  output?:string
) → TaskWriteResult{told,…}
```

### `tasks.create`

Create a task and its description together, unstarted: nobody is told and no conversation opens until `tasks.start`.

```
// mutate
tasks.create(
  title:string[≤60],
  boardId|boardSlug|boardPrefix:string,
  idempotencyKey:string,
  intent?:string[≤2000],
  supervisor?:string,
  template?:string,
  slots?:Record<string,string>,
  steps?:StepInput[],
  tagIds?:string[],
  tagNames?:string[],
  body?:string,
  conversationId?:string
) → TaskWriteResult{told,…}
```

### `tasks.delete`

Delete the task outright.

```
// mutate
tasks.delete(id|reference:string, ifVersion:number) → {told,…}
```

### `tasks.hand`

Hand a step to another agent (`to: null` returns it to the supervisor).

```
// mutate
tasks.hand(
  id|reference:string,
  ifVersion:number,
  stepId|stepName:string,
  to:string|null
) → TaskWriteResult{told,…}
```

### `tasks.list`

Workspace tasks, filtered before pagination.

```
// query
tasks.list(
  limit?:number=50,
  cursor?:string,
  maxBytes?:number=16 KiB,
  createdAfter?:string,
  createdBefore?:string,
  updatedAfter?:string,
  updatedBefore?:string,
  involving?:string,
  boardId|boardSlug|boardPrefix?:string,
  state?:TaskState|TaskState[],
  tagIds?:string[],
  tagNames?:string[],
  untagged?:boolean,
  roomIds?:string[],
  roomSlugs?:string[],
  contains?:string,
  owner?:string,
  supervisor?:string,
  ready?:boolean,
  includeArchived?:boolean=false,
  started?:boolean,
  order?:'asc'|'desc'=desc
) → TaskListResult{tasks,counts,more,…}
```

### `tasks.openConversation`

Open the task's conversation now, without waiting for a row to open one — the same conversation the system would open, in the board's room, bound to the task, so later rows land in it.

```
// mutate
tasks.openConversation(id|reference:string) → TaskWriteResult{told,…}
```

### `tasks.plan`

Edit the plan: add steps (after a named step, or first), remove to-do steps, move steps, and name the plan's release step (`releases`).

```
// mutate
tasks.plan(
  id|reference:string,
  ifVersion:number,
  add?:(StepInput & { afterId?: string|null; afterName?: string|null })[],
  removeIds?:string[],
  removeNames?:string[],
  move?:{ stepId?: string; stepName?: string; afterId?: string|null; afterName?: string|null }[],
  releasesId|releasesName?:string|null
) → TaskWriteResult{told,…}
```

### `tasks.read`

One task by `id` or `reference` (`CT945`), whichever you hold, with its full plan — an object, never JSON text.

```
// query
tasks.read(
  id|reference:string,
  maxBytes?:number=16 KiB,
  part?:string,
  startLine?:number,
  startOffset?:number,
  ifVersion?:number,
  ifSha?:string,
  fromStep?:number
) → TaskReadResult{more,…}
```

### `tasks.reopen`

Put a done or skipped step back to to-do; every step after it goes back too, and a closed task reopens.

```
// mutate
tasks.reopen(
  id|reference:string,
  ifVersion:number,
  stepId|stepName:string
) → TaskWriteResult{told,…}
```

### `tasks.skip`

Skip a step with a reason.

```
// mutate
tasks.skip(
  id|reference:string,
  ifVersion:number,
  stepId|stepName:string,
  reason:string
) → TaskWriteResult{told,…}
```

### `tasks.start`

Start the task: its first step becomes current, its row lands in the task's conversation (opened if none is bound) and its owner is told, with your `note` on the row — you included, when the first step is yours: you're woken there, and `to…

```
// mutate
tasks.start(
  id|reference:string,
  ifVersion:number,
  note?:string
) → TaskWriteResult{told,…}
```

### `tasks.update`

Update title, intent, supervisor or tags with ifVersion from the last read, or set `autostart`.

```
// mutate
tasks.update(
  id|reference:string,
  ifVersion:number,
  title?:string[≤60],
  intent?:string[≤2000],
  supervisor?:string,
  tagIds?:string[],
  tagNames?:string[],
  autostart?:boolean
) → TaskWriteResult{told,…}
```

## turns

### `turns.interrupt`

Force a running agent to read a message that is waiting behind their turn, NOW, by stopping that turn.

```
// mutate
turns.interrupt(messageId:string, agent:string) → InterruptResult
```

### `turns.list`

List completed turns in this workspace, newest first by default.

```
// query
turns.list(
  from?:string,
  until?:string,
  roomId|roomSlug?:string,
  requestedBy?:string,
  conversationId?:string,
  agent?:string,
  runtime?:string,
  model?:string,
  outcome?:TurnOutcome|TurnOutcome[],
  resultReason?:TurnResultReason['kind'],
  limit?:number=50,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → {turns,more}
```

### `turns.listActivity`

A page of one turn's thinking and tool rows — **what a turn actually did**, step by step.

```
// query
turns.listActivity(
  id:string,
  phase?:'start'|'done'|'error',
  limit?:number=50,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=asc
) → {rows,more}
```

### `turns.readActivity`

One activity row's scalar, sliced — a tool call's `input`, `result` or `label`, or a thinking block's `text`.

```
// query
turns.readActivity(
  id:string,
  startLine?:number=1,
  endLine?:number,
  maxBytes?:number=16 KiB,
  startOffset?:number,
  ifSha?:string,
  part?:'summary'|'label'|'input'|'result'|'text'
) → ActivityRead{id,turnId,more,…}
```

### `turns.running`

Who is executing RIGHT NOW — every running turn in the workspace, with its conversation and latest visible line.

```
// query
turns.running(
  roomId|roomSlug?:string,
  conversationId?:string,
  agent?:string,
  limit?:number=50,
  maxBytes?:number=16 KiB,
  cursor?:string,
  order?:'asc'|'desc'=desc
) → {turns,more}
```

## workspace

### `workspace.clearAvatar`

Clear the workspace's avatar back to the letter mark.

```
// mutate
workspace.clearAvatar() → { workspace: WorkspaceDetail }
```

### `workspace.read`

The workspace you are acting in — its name, slug, short id, default agent, avatar and who may edit its charter.

```
// query
workspace.read() → { workspace: WorkspaceDetail }
```

### `workspace.setAvatar`

Set the workspace's avatar.

```
// mutate
workspace.setAvatar(ref:AvatarRef) → { workspace: WorkspaceDetail }
```
