Skip to content

Tools

Last updated 11 Nov 2026

Seventeen tools, plus the three attachment tools when the instance has an attachment store configured (it does in every standard deployment). Those marked write require a token with the write scope. The rest only read, and say so to the client as readOnlyHint.

ToolDoes
workspace_overviewOne-call bootstrap: workspace name, every space with its lists and item counts, label groups, members (human and agent), document tree roots, every goal. Call this first.
space_getItems of one space as compact rows. Filter by list, label, assignee, state, free text, or updated_since for delta reads. detail=board adds what a board card shows (a one-line summary, size, open blockers, attachment count, the goals each card serves). detail=full adds that and the whole description.
item_getFull detail for up to 50 items by key: description, comments, checklists, links and backlinks, attachments, optionally the activity tail. Old keys from cross-space moves resolve automatically.
item_writewriteBatch item mutations. See below.
goal_listEvery goal with its status and measured progress, plus a summary: counts by status, how many are past their date or quiet for a month, and the work across all goals in flight. See Goals.
goal_getUp to 20 goals in full: description, followers, ancestors, sub-goals with their own progress, every card counted toward the goal with its assignees, and the check-ins. include: ["tree"] adds each card's parent and open blockers.
goal_writewriteBatch goal mutations. See Goals.
space_writewriteCreate and change spaces and lists: create (with the kanban6 template), update (name, description, done window, and the space's icon: see Space icons), archive, list_create, list_update, list_archive.
doc_treeThe document hierarchy as a flat, depth-annotated list. Optionally scoped to a subtree.
doc_getOne document: frontmatter, body, rev. include: ["sections"] adds the heading map with per-section hashes; "versions" the history; "backlinks" the referrers; "comments" the comments, with anchor status for inline ones. at_version reads an old revision.
doc_writewriteBatch document mutations. See below.
searchFull-text across item titles and descriptions, comments and documents. Returns type, key or slug, title, snippet.
activity_queryThe audit trail. Filter by entity, actor, actor_kind, verb pattern (item.*), and since for a cheap delta.
response_timesTime to first response and time to resolution, per space and per label, as count, median and P90, plus cards received and still waiting. See Response times.
label_writewritegroup_create (with space for one space, without for every space), group_update, label_create, label_update, label_reorder, label_merge (folds one label into another and reassigns every item), label_delete.
webhook_writewriteOutbound webhooks: create (url plus event patterns, optional HMAC secret), update, delete.
rule_writewriteAutomation rules from a fixed catalogue. See below.
attachment_addwriteAttach a URL, or inline base64 up to 1 MB, to an item or document.

Writing items ​

item_write takes up to 100 ops and returns {op_id, ok, key, rev} for each.

OpNotes
createLabels, assignees and checklists inline.
updateOptional if_rev for an optimistic lock.
moveAcross spaces too, which re-keys the item and leaves an alias.
comment[[@handle]] in the body notifies that member.
checklist_setReplaces a named checklist.
label, assignAdd and remove.
archive, restore
complete, reopen
depends_on, undependDeclare that one item waits for another. Cycles are refused.
sizeA unitless estimate, and whether the item is a milestone.
deletePermanent, and refused unless the item is archived first.

Every op carries an op_id you choose. The same op_id applied twice does the work once, so a retry after a timeout is safe:

json
{
  "ops": [
    {
      "op": "create",
      "op_id": "a1",
      "space": "ENG",
      "list": "To Do",
      "title": "Rotate the signing key",
      "labels": ["security"]
    },
    {
      "op": "comment",
      "op_id": "a2",
      "key": "ENG-142",
      "body": "Blocked until [[ENG-140]] lands. cc [[@dana]]"
    }
  ]
}

Goals ​

A goal is referenced by its number, 12 or "G-12". goal_write takes up to 100 ops and returns {op_id, ok, key: "G-12", rev} for each.

OpNotes
createtitle, description, owner (defaults to you if you are a person, null for none), parent, status, start_date and target_date (epoch ms), metric {name, unit?, start, target, current?}, items (card keys), followers.
updateAny of the fields above except status and parent. null clears. Optional if_rev.
set_parentMakes a goal part of another, at any depth. Cycles are refused. null makes it top-level.
linkadd and remove card keys. A card can serve several goals, and everything that is part of it counts too.
check_instatus, body and metric_value, at least one. Moves the goal's status and metric, and notifies its owner and followers.
check_in_update, check_in_deleteEdit is author only. Delete follows the workspace's comment rule and does not rewind the status.
followadd and remove handles. People only.
archive, restore
deletePermanent, refused unless archived. Sub-goals become top-level; cards are untouched.

Every goal row carries two signals that must not be confused. status is the owner's judgement, set by check-ins. progress is measured from the cards on every read: cards_total, cards_done, cards_active, cards_waiting, cards_overdue, and percent, weighted by card size with unsized cards counting as 1. The rows also carry overdue (in flight and past its target date), stale (in flight with no check-in for 30 days), elapsed (how much of the date window has gone) and metric with its own percent.

goal_get lists the cards a goal counts itself in items, each with assignees (handles) so it is clear who is on what. include: ["tree"] adds two fields per card for drawing the work as a tree: parent, the card it is directly a part of, and blocked_by, its open blockers as {key, title, space}. A blocker that is completed or archived drops out, and one may live on any space without serving the goal.

space_get takes goal to return only the cards serving it, sub-goals and parts included, and item_get returns the goals each card serves, with via naming the card the link is on when it is inherited.

A card being linked or unlinked is an event on the card (item.goal_linked, item.goal_unlinked), so automation rules can act on it. Goal events (goal.created, goal.updated, goal.checked_in, goal.status_changed, goal.parented, goal.archived and the rest) go to webhooks and the activity feed.

json
{
  "ops": [
    {
      "op": "create",
      "op_id": "g1",
      "title": "Ship licensing v2",
      "target_date": 1798761600000,
      "items": ["ST-140", "ST-152"]
    },
    {
      "op": "check_in",
      "op_id": "g2",
      "goal": "G-1",
      "status": "at_risk",
      "body": "Activation is done. Waiting on [[@dana]] for the refund flow."
    }
  ]
}

Space icons ​

A space can carry an icon: a picture shown beside its name in the sidebar and in the breadcrumb trail, the way a client has a logo. Only a workspace admin sets it, and so only an agent acting for an admin can.

space_write's update takes it as inline base64, up to 1 MB of PNG, JPEG, GIF, WebP, AVIF or SVG, or null to remove it:

json
{
  "ops": [
    {
      "op": "update",
      "op_id": "studio-icon",
      "key": "ST",
      "icon": { "mime": "image/png", "content_base64": "iVBORw0KGgo..." }
    }
  ]
}

workspace_overview lists a space's icon_url when it has one, and space_get always returns it, null without one. It is a path on the instance (/api/v1/space-icons/<id>.png), readable by anyone signed in. A new picture gets a new address, and the old one stops being served.

Over REST the same op goes to POST /api/v1/spaces/write. The Settings page uploads the file itself instead: POST /api/v1/spaces/<key>/icon with the image as the body and its type as content-type, and DELETE /api/v1/spaces/<key>/icon to remove it. Both answer { icon_url }.

Writing documents ​

OpNotes
create
replaceNeeds if_rev.
patch_sectionNeeds the section slug and if_hash from doc_get. Conflicts only when that same section changed.
appendNo read needed. Ideal for logs and running notes.
moveparent (a slug, or null for the top level) appends as the last child. before or after a sibling slug places it there instead. Refuses the page itself or any of its subpages as the new parent. The slug never changes.
rename
set_layoutPage width, default or wide. Presentation only: no rev bump, and the body is untouched.
set_visibilityvisibility: private (the owner only) or workspace (every member). Owner only. Sharing shares the pages under it. A page inside a private page cannot be shared alone. Pages an agent creates are shared.
archive
deleteHard delete, leaf pages only.
commentA page comment, or an inline one with anchor. Never changes the document or its rev.
comment_updateEdits a comment's body.
comment_resolveResolves an inline comment. resolved: false reopens it.

patch_section is the one worth learning. Read with sections, change one, send it back:

json
{
  "ops": [
    {
      "op": "patch_section",
      "op_id": "d1",
      "ref": "manual/runbook",
      "section": "restarting-the-worker",
      "if_hash": "9f2c…",
      "content": "## Restarting the worker\n\nDrain first, then …"
    }
  ]
}

Two agents editing different sections of the same page both succeed.

Inline comments ​

An inline comment is anchored to text by quoting it. The quote must be in the page as it reads now, and if the phrase appears more than once, prefix or suffix says which one. A quote that is missing or ambiguous is refused with the reason, so nothing is created pointing at nothing.

json
{
  "ops": [
    {
      "op": "comment",
      "op_id": "c1",
      "ref": "manual/runbook",
      "body": "Should this drain step have a timeout?",
      "anchor": { "exact": "Drain first", "suffix": ", then" }
    }
  ]
}

The anchor is stored with the comment, never in the markdown, so commenting leaves the page byte for byte as it was. doc_get with include: ["comments"] returns each inline comment's anchor and an anchor_status: anchored, or detached when the quoted text has since been deleted or rewritten beyond recognition. A detached comment is kept and still listed. Light edits, text inserted around the quote and repeated phrases are all followed. Resolve with comment_resolve, which keeps the comment and marks it resolved.

Automation rules ​

rule_write manages rules from a fixed catalogue:

json
{
  "op": "create",
  "op_id": "r1",
  "name": "Triage security reports",
  "trigger": "item.created",
  "condition": "space=ENG label=security",
  "action": { "kind": "assign", "actor": "daniela" }
}

An action is an object, not a name: its kind decides which other field it carries. move_item takes list (and optionally space), apply_label takes label, assign takes actor, comment takes body, call_webhook takes url, and complete takes nothing.

Triggers are event patterns matched against event verbs, and only item events ever reach the engine. A rule triggered on doc.created or space.updated is accepted and stored, and will never fire.

Conditions use the embed-query grammar (space=X list=Y label=Z assignee=H state=open|done|archived). A condition that does not parse is refused rather than stored, because one that matched nothing would leave a rule reading as active while quietly doing nothing.

update patches in place. Send only the fields you are changing, and the rest are left alone. trigger, condition and action can all be changed without recreating the rule, which keeps its id and its history intact. Passing "condition": null clears the condition, which is deliberately different from omitting the field.

Rule actions are attributed to the system actor with caused_by chaining, and never re-trigger rules, so a pair of rules cannot loop.

A rule has no space column. Where it is shown is read from what it says: a condition naming space=X makes it space X's, and so does a move_item into space X when the condition names no space. Those are in that space's Settings, under Automation. Every other rule is in workspace Settings.

Response times ​

response_times reports how long people wait, computed from the event log.

json
{ "space": "SU", "from": 1792368000000, "to": 1792972800000 }

All three are optional. from is inclusive and to exclusive, both epoch milliseconds, and without them the range is the seven days up to now. Without space it covers every space that is not archived and leaves out the ones where nothing happened.

json
{
  "from": 1792368000000,
  "to": 1792972800000,
  "spaces": [
    {
      "key": "SU",
      "name": "Support",
      "all": {
        "received": 7,
        "unanswered": 1,
        "first_response": {
          "count": 7,
          "median_ms": 12600000,
          "p90_ms": 97200000
        },
        "resolution": { "count": 3, "median_ms": 86400000, "p90_ms": 172800000 }
      },
      "labels": [
        {
          "id": "lbl_...",
          "name": "Bug report",
          "color": "red",
          "group": "Need",
          "received": 6,
          "unanswered": 1,
          "first_response": {
            "count": 6,
            "median_ms": 12600000,
            "p90_ms": 97200000
          },
          "resolution": {
            "count": 2,
            "median_ms": 72000000,
            "p90_ms": 165600000
          }
        }
      ]
    }
  ]
}
  • First response: the first comment, move to another list, completion or reopening by a human who is not the card's creator. Agents and the system never count, including an agent acting on a person's behalf. A personal access token is its owner, so it counts. Reordering within a list does not.
  • Resolution: reaching a done-role list, or completion in a space without one. A reopened card is resolved at the last resolution, measured from creation. A card created already done has no resolution.
  • A card counts toward the range its response or resolution happened in, judged only on events before to, so a past range never changes when a card is reopened later.
  • received counts cards created in the range, and unanswered those of them with no first response before to.
  • Median and P90 interpolate between neighbours (PERCENTILE.INC). A duration with nothing to measure is null.
  • Cards are grouped by the space they are in and the labels they carry now. Durations are calendar time.

When the instance has ACTA_BASE_URL set, workspace_overview returns url_templates, where a card, a goal and a document open in the web app for your workspace. The server's instructions on initialize carry the same templates, so a model sees them as soon as it connects.

json
"url_templates": {
  "item": "https://acta.example.com/nubisco/s/{space}?item={key}",
  "goal": "https://acta.example.com/nubisco/goals/{number}",
  "doc": "https://acta.example.com/nubisco/docs/{slug}"
}

{space} is the part of a card key before the dash, so CM-35 opens at .../s/CM?item=CM-35, and G-14 at .../goals/14. Fill these rather than building addresses yourself: the routes are the web app's to change, and the templates follow them. They are given once rather than as a url on every row because every address follows from a key the result already carries, and a url per row made a compact read a third larger. Without ACTA_BASE_URL there are no templates, never relative ones.

Errors ​

A refused call comes back as a tool result with isError: true and a message, not as a JSON-RPC error, so an agent can read the reason and try something else. JSON-RPC errors are reserved for a malformed request or an unknown tool.

Conflicts are honest. update with a stale if_rev, or patch_section with a stale if_hash, fails rather than overwriting. Re-read, merge, retry.

Acta is proprietary software by Nubisco.