OS tools
The os.* tools a run can call: execution labels, gate summaries, questions for the CEO, self-improvement, mail, company state, shared context, unblocking runs, planning, the CEO’s brief and chat threads.
Generated from the built-in tools registry. Do not edit by hand.
os.* tools are answered by the OS itself. They need no vendor binding, and none of them calls a vendor. A task still declares each one it calls in its tools, like any other tool.
Shared context (os.set_context, os.get_context, os.delete_context) is a JSON value one run stores and a later run of any task can read. A task version names the key prefixes it wants, and those keys arrive in the run input as context before the model is called. It is bookkeeping between runs. It is not memory, and it is not a place for a secret.
Everything the OS does not have to do itself (send mail, post a message, file an issue) is a vendor tool the enterprise binds, never an os.* tool. The CEO chat has a few os.* tools of its own: see Chat tools.
The company-state tools (os.inspect_enterprise, os.directory, os.list_blockers, os.list_open_gates) only read. None of them approves, rejects, retries or clears anything, and calling one never opens or resolves a gate.
The unblocking tools (os.retry_run, os.route_run, os.clear_run, os.escalate_run) move the runs on Blockers: start one again, send its work to another stage of its pipeline, take one off, or hand one to its team lead or the CEO. None of them decides a gate.
The brief tool (os.file_daily_brief) files the CEO’s brief in the member’s chat thread: what is waiting on the CEO, composed by the OS from its own records. It sends no mail and opens no gate.
The chat thread tools (os.chat_list, os.chat_read, os.chat_write, os.chat_open) are a member’s conversations with the enterprise’s people, as the mail tools are their mailbox. Every caller reads and writes as who it is. A run is its assigned member: it sees that member’s threads only, and what it writes is the member speaking, left for the people to read, so the run never waits for a reply. An API token or the MCP server is the person’s side: what it writes is a message to the member, who replies. A message a run writes is not something the member remembers when a person later talks to them in that thread.
| Tool | What it does | Effect |
|---|---|---|
os.set_execution_name |
Replace the execution's default title with a human-readable name. | write |
os.add_execution_link |
Attach an http(s) link to the execution: an issue, a pull request, a preview. | write |
os.set_gate_summary |
Write what the person approving this run's next gate is deciding on. | write |
os.ask_ceo |
Ask the CEO questions, each with a recommended answer, and end the run on their decision. | write |
os.list_recommendations |
List self-improvement recommendations: pending, decided, task-record, or the open issues. | read |
os.get_recommendation |
One recommendation with its evidence. | read |
os.decide_recommendation |
Decide one pending recommendation: decline, actioned, or implement. | write |
os.mail_list |
List the assigned member's inbox, newest first: metadata and a snippet, no bodies. | read |
os.mail_read |
Read one message from the assigned member's inbox. | read |
os.mail_reply |
Reply to a stored mail from the address it arrived at, once the CEO approves. | write |
os.mail_send |
Send a new mail from the assigned member’s mailbox, once the CEO approves. | write |
os.mail_set_label |
Set the labels on a mail in the assigned member’s inbox. | write |
os.mail_set_handled |
Record whether a mail in the assigned member’s inbox has been handled. | write |
os.mail_set_many |
Set the labels, the handled state, or both on many mails in the assigned member’s inbox at once. | write |
os.mail_quiet_threads |
The threads the assigned member replied to that have gone quiet, oldest first, at most ten. | read |
os.mail_mark_chased |
Record that a quiet thread was answered today, so no second run today raises it again. | write |
os.mail_snooze |
Leave a quiet thread alone until a date, instead of drafting a follow-up. | write |
os.escalate_mail |
The gate a mail that needs the CEO opens: the mail, why it matters, and what is suggested. | write |
os.mail_reply_draft |
The gate a drafted reply opens, so no reply leaves without a decision on it. | write |
os.list_kpis |
List the signed-off KPIs the run's own team owns and those its task is the source of, with their lights. | read |
os.record_kpi_reading |
Record a reading for a KPI whose source is this run's task. | write |
os.get_enterprise_kpis |
The company's KPI targets, bands, cadence and source. | read |
os.set_enterprise_kpis |
Replace the company's KPI target list. | write |
os.get_team_kpis |
A team's KPI targets, including its key KPI. | read |
os.set_team_kpis |
Replace a team's KPI target list and key KPI. | write |
os.set_context |
Store a JSON value for the enterprise under a key, so a later run can read it. | write |
os.get_context |
Read one enterprise context value, or null when the key has never been set. | read |
os.delete_context |
Remove one enterprise context key. A missing key is a success. | write |
kpi.record_reading |
Record one reading of a KPI by hand: the value, when it was taken, and who or what took it. | write |
kpi.list_readings |
One KPI's readings, newest first, corrected ones included. | read |
role.list_kpis |
A role's KPIs, oldest first, with the team KPI each serves, its sign-off, light, trend and readings. | read |
role.define_kpi |
Propose a KPI on a role, waiting for a person to sign it off. | write |
role.edit_kpi |
Change a role KPI's fields; a signed-off one only a person changes. | write |
os.inspect_enterprise |
A snapshot of the company: teams, members, roles, and the tasks of the roles each member holds. | read |
os.directory |
Teams, members, roles, and the tasks of the roles each member holds: who to offload work to. | read |
os.list_blockers |
What has stopped: failed, stalled, blocked and spend-paused runs, and stopped pipelines, with who owns each. | read |
os.list_open_gates |
Every gate waiting on a decision, oldest first: the CEO inbox's own rows. | read |
os.retry_run |
Start a stopped run again, with the same input, so its work moves. | write |
os.route_run |
Send a stopped run's work to another task of its pipeline, instead of running the same task again. | write |
os.clear_run |
Take a stopped run off Blockers without running it again. | write |
os.escalate_run |
Hand a stopped run to its team's lead, or to the CEO, and record that you did. | write |
os.read_enterprise |
The whole enterprise: teams, charters, roles, members and every task in full. | read |
os.search_catalog |
Published catalog tasks that match a plain-English intent, best first. | read |
os.list_tool_bindings |
The tool types this enterprise has bound, at which layer, and the tools each exposes. | read |
os.list_built_in_tools |
Every built-in tool a run can call, with its summary and whether it reads or writes. | read |
os.read_task |
One task in full, its pending recommendations, and the tasks that start it. | read |
os.assess_impact |
What points at a role, team, task or member a plan would delete. | read |
os.write_plan |
Write or replace this chat session's plan, checked before the CEO sees it. | write |
os.read_plan |
This chat session's plan as it stands, with its version. | read |
os.file_daily_brief |
File the CEO's brief in your chat thread: what is waiting on them, and the time on their clock. | write |
os.chat_list |
List chat threads, most recently active first: who each is with, its state and its last message. | read |
os.chat_read |
Read one chat thread: its messages, oldest first. | read |
os.chat_write |
Write one message in a chat thread, as yourself. | write |
os.chat_open |
Open a new chat thread with a member. | write |
os.set_execution_name
Replace the execution's default title with a human-readable name.
What the model reads:
Set a human-readable name for this execution, shown on Executions and run detail instead of the first task name. Later calls replace it. Pass name, e.g. "GitHub Issue #105 - Work keeps moving while decisions wait on the CEO".
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Shown on Executions instead of the first task name |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. The last successful call in the execution wins, so a repeat leaves the same name.
- Called by: a run, the API.
- Returns: The name as stored: whitespace collapsed and trimmed.
- API:
POST /v1/runs/self/execution-name(run:self)
| Error | When | What happens |
|---|---|---|
name_required |
name is missing, or blank once whitespace is trimmed. |
Tool error |
name_too_long:200 |
name is over 200 characters. |
Tool error |
Example:
{
"name": "Issue #12 - Export the monthly report as CSV"
}
os.add_execution_link
Attach an http(s) link to the execution: an issue, a pull request, a preview.
What the model reads:
Attach a helpful http(s) link to this execution (issue, PR, preview, CRM profile). Shown on Executions and run detail. The same URL later updates the label. Pass name and url.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Label, e.g. GitHub issue, Pull request, Preview |
url |
string | yes | http(s) URL |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. The same URL updates its label rather than adding a second link.
- Called by: a run, the API.
- API:
POST /v1/runs/self/execution-links(run:self)
| Error | When | What happens |
|---|---|---|
name_required |
name is missing, or blank once whitespace is trimmed. |
Tool error |
name_too_long:200 |
name is over 200 characters. |
Tool error |
url_required |
url is missing or blank. |
Tool error |
url_too_long:2000 |
url is over 2000 characters. |
Tool error |
url_invalid |
url is not an http: or https: URL. |
Tool error |
Example:
{
"name": "Pull request",
"url": "https://github.com/acme/app/pull/34"
}
os.set_gate_summary
Write what the person approving this run's next gate is deciding on.
What the model reads:
Write, for whoever approves this run’s next gate, what they are deciding on. Markdown bullets: what the action does, and anything to watch out for. Shown on the approval card in Gates and the run page. Required before github.merge_pull_request and os.mail_reply — either call is handed back until you write one. A later call replaces it, but never rewrites a gate already waiting. Pass summary.
| Field | Type | Required | Description |
|---|---|---|---|
summary |
string | yes | Markdown. Short headed sections of "- " bullets, e.g. "## What changed" then "## Watch out for". No preamble. |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. A later call replaces the summary, so a repeat leaves the same one.
- Called by: a run, the API.
- Needs:
- Called before
github.merge_pull_request,os.mail_reply, which is handed back until it is.
- Called before
- Returns: The summary as stored. The gate copies it when it opens, so a later call never rewrites a gate already waiting or decided.
- API:
POST /v1/runs/self/gate-summary(run:self)
| Error | When | What happens |
|---|---|---|
summary_required |
summary is missing or blank. |
Tool error |
summary_too_short:40 |
summary is under 40 characters. |
Tool error |
summary_too_long:6000 |
summary is over 6000 characters. |
Tool error |
summary_needs_bullets |
summary has no markdown bullet (- , * or 1. ). |
Tool error |
gate_summary_required |
A merge or reply call arrives before this run has written a summary. | Handed back |
Example:
{
"summary": "## What changed\n- Adds CSV export to the monthly report\n\n## Watch out for\n- A migration runs on release"
}
os.ask_ceo
Ask the CEO questions, each with a recommended answer, and end the run on their decision.
What the model reads:
Ask the CEO questions and stop. This run waits for their decision, then ends. Pass questions: one decision each, numbered from 1, each answerable on its own, each with context (why it blocks, the options you see) and ctoRecommendation (the answer you recommend and why; required). The CEO answers each one: accepts your recommendation, answers it themselves, or asks you to explain further. Submit → outcome ceo_approved, Reject → ceo_rejected; the next task gets their answers as ceoAnswers and any note as ceoNote. Call it last.
| Field | Type | Required | Description |
|---|---|---|---|
questions |
array of object, at least 1 | yes | |
questions[].number |
integer, at least 1 | yes | |
questions[].question |
string | yes | One decision, answerable on its own |
questions[].context |
string | no | Why it blocks, and the options you see |
questions[].ctoRecommendation |
string | yes | The answer you recommend, and a one-line reason. Never empty. |
context |
string | no | One line on what is blocked, and the issue URL |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. The gate is found again by run and tool call, so a repeat never asks twice.
- Called by: a run, the API.
- Needs:
- A gate: the CEO decides every call, whatever the task declares.
- Returns: The CEO's decision, as the run's outcome:
ceo_approvedwithceoAnswers(one per question) andceoNote, orceo_rejectedwithceoNote. The next task receives them. - API:
POST /v1/runs/self/ask-ceo(run:self)
| Error | When | What happens |
|---|---|---|
ceo_questions_invalid |
No questions, or a question with a repeated or invalid number, no text or no ctoRecommendation. Recorded as ceo_questions_invalid:<why>, before any gate opens. |
Handed back |
Example:
{
"questions": [
{
"number": 1,
"question": "Should the export include archived reports?",
"context": "Customers asked for both; including them doubles the file size.",
"ctoRecommendation": "No: archived reports are rarely opened, and a filter can add them later."
}
],
"context": "Scoping https://github.com/acme/app/issues/12"
}
os.list_recommendations
List self-improvement recommendations: pending, decided, task-record, or the open issues.
What the model reads:
List self-improvement recommendations. view: pending (default, oldest first, to decide), decided (the last 60 days across every task, to spot recurrence), task-record (actioned for implementation in the last 14 days, to follow up their issues), or issues (every open self-improvement issue in the repo, read by the harness: look here before filing, never rely on a search). Optional limit.
| Field | Type | Required | Description |
|---|---|---|---|
view |
string, one of pending, decided, task-record, issues |
no | |
limit |
integer, at least 1 | no |
No other fields are accepted.
- Effect: read.
- Side effects: View
issuesrefreshesissueOutcomeon recommendations whose issue was still open, and returns what ended since the last call assettled. - Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- A
repoinput, for viewissuesonly. - A GitHub credential in the run's cascade, for view
issuesonly: the open issues are read with it.
- A
- API:
POST /v1/runs/self/tools/os.list_recommendations(run:self)
| Error | When | What happens |
|---|---|---|
view_invalid |
view is not one of the four. |
Tool error |
repo_required |
View issues on a run with no repo input. |
Tool error |
issues_unavailable |
View issues could not read the repository. It fails rather than report an empty backlog. |
Tool error |
Example:
{
"view": "pending",
"limit": 10
}
Example:
{
"view": "issues"
}
os.get_recommendation
One recommendation with its evidence.
What the model reads:
One recommendation with its evidence: the source run (status, recent events, failed tool calls), task versions newer than the one the source run ran, and every other recommendation for the same task. Pass id.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.get_recommendation(run:self)
| Error | When | What happens |
|---|---|---|
id_required |
id is missing. |
Tool error |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000001"
}
os.decide_recommendation
Decide one pending recommendation: decline, actioned, or implement.
What the model reads:
Decide one pending recommendation. decision: decline (reason required), actioned (issueUrl of the GitHub issue or PR that takes the work, and lane: os, repo-commands or local-stack), or implement (the task-record lane: issueUrl of the issue that tracks it; starts the implementer). issueUrl must be in this repo and must not be a closed issue or an unmerged closed pull request. Pass id, decision, reason, issueUrl, lane.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | |
decision |
string, one of decline, actioned, implement |
yes | |
reason |
string | yes | |
issueUrl |
string | no | https://github.com/<owner>/<repo>/issues/<n> or /pull/<n> |
lane |
string, one of os, repo-commands, local-stack, task-record |
no |
No other fields are accepted.
- Effect: write.
- Side effects:
implementstarts the implementing task. - Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Keyed on the tool call: the same call sent twice decides once and returns the first answer. A different call on a decided recommendation is refused.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.decide_recommendation(run:self)
| Error | When | What happens |
|---|---|---|
id_required |
id is missing. |
Tool error |
recommendation_not_pending:<status> |
Someone else already decided it. Only a pending recommendation can be decided. |
Tool error |
decision_invalid |
decision is not one of the three. |
Tool error |
lane_invalid |
actioned without a valid lane. |
Tool error |
task_record_uses_implement |
actioned with lane task-record: decide implement instead. |
Tool error |
issue_url_invalid |
implement without a GitHub issue URL. |
Tool error |
issue_url_wrong_repo |
issueUrl is not in the run's repo. |
Tool error |
issue_closed |
issueUrl is a closed issue. |
Tool error |
pull_request_closed_unmerged |
issueUrl is a pull request closed without merging. |
Tool error |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000001",
"decision": "decline",
"reason": "Not reproduced on the source run"
}
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000002",
"decision": "actioned",
"reason": "Covered by the retry change",
"issueUrl": "https://github.com/acme/app/issues/12",
"lane": "os"
}
os.mail_list
List the assigned member's inbox, newest first: metadata and a snippet, no bodies.
What the model reads:
List mail in the inbox of the member doing this run, newest first. Metadata and a snippet only, no bodies: read one with os.mail_read. Optional unreadOnly (only messages not yet opened), label (only mail carrying that label), handledState (only mail in that state), threadId (every mail in one conversation), order (desc, the default, or asc), page and limit (default 25, max 200). Filters combine: label VIP with handledState unhandled lists only unhandled VIP mail. A full row is large: about 30 fit in one result. To work through a long backlog pass compact true, which returns only id, fromAddress, subject, receivedAt, labels and handledState, so about 100 rows fit.
| Field | Type | Required | Description |
|---|---|---|---|
unreadOnly |
boolean | no | Only messages nobody has opened yet |
label |
string | no | Only mail carrying this label, e.g. VIP |
handledState |
string, one of unhandled, replied, skipped, snoozed, escalated |
no | Only mail in this handled state |
threadId |
string | no | Only mail in this conversation, from a row’s threadId |
order |
string, one of desc, asc |
no | By receivedAt. Default desc |
page |
integer, at least 1 | no | |
limit |
integer, 1 to 200 | no | |
compact |
boolean | no | Only id, fromAddress, subject, receivedAt, labels and handledState on each row |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ data, total, page, limit, pages }. Each row hasid,fromAddress,fromName,subject,snippet,receivedAt,readAt,hasAttachments,threadKey(the same on every message in one conversation),direction(inboundwhen it arrived,outboundwhen the member sent it),to,cc,threadId,labels(free text,[]when none),handledState(unhandled,replied,skipped,snoozedorescalated) andsnoozedUntil(only whensnoozed). Withcompact, each row has onlyid,fromAddress,subject,receivedAt,labelsandhandledState. - API:
GET /v1/members/:id/emails(members:read)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
handled_state_invalid |
handledState is not one of the handled states. | Tool error |
Example:
{
"unreadOnly": true,
"limit": 10
}
Example:
{
"handledState": "unhandled",
"order": "asc",
"compact": true,
"limit": 100
}
os.mail_read
Read one message from the assigned member's inbox.
What the model reads:
Read one message from the assigned member’s inbox: sender, subject, text and sanitised HTML bodies, and attachment names (not their contents). Reading marks it read for the member too. Pass id from os.mail_list.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | A message id from os.mail_list |
No other fields are accepted.
- Effect: read.
- Side effects: Marks the message read for the member, the same as opening it in the portal.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns: The message: sender, subject,
textBody, sanitisedhtmlBody,receivedAt,readAt,attachments(names and sizes, not contents),to,cc,threadId,labels(free text,[]when none),handledState(unhandled,replied,skipped,snoozedorescalated) andsnoozedUntil(only whensnoozed). - API:
GET /v1/members/:id/emails/:emailId(members:read)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
email_not_found |
The id is not in this member's mailbox, including another member's message. | Tool error |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000003"
}
os.mail_reply
Reply to a stored mail from the address it arrived at, once the CEO approves.
What the model reads:
Reply to one message in the assigned member’s inbox. Pass id from os.mail_list and the reply as text (optional html for a formatted body); the subject, the recipient and the sending address are taken from the message being answered, so a mail sent to the member’s Zero Human address is answered from that address. The reply threads under the original in the recipient’s mail client. Every send waits for the CEO to approve this exact payload, and approval sends that and nothing else. The sent reply is filed in the member’s mailbox in the original’s thread, and the original is marked read and moves to handled state replied.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | A message id from os.mail_list: the mail this answers |
text |
string | yes | The reply, as plain text |
html |
string | no | Optional HTML body. Plain text is sent either way |
No other fields are accepted.
- Effect: write.
- Side effects: Files the sent reply in the member’s mailbox, in the original’s thread, marks the original read and, once sent, moves it to handled state
replied(clearing any snooze). - Retry: Not retry-safe: a repeat acts again. A repeat sends a second reply. Call it again only if the first reported a refusal.
- Called by: a run, the API.
- Needs:
- A gate: the CEO decides every call, whatever the task declares.
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- Returns:
{ status, id }:sent, orsend_failedwith the provider’serrorwhen it did not go out.idis the copy filed in the mailbox either way. - API:
POST /v1/runs/self/tools/os.mail_reply(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
email_not_found |
The id is not in this member's mailbox, including another member's message. | Tool error |
recipient_invalid |
The stored message names no address that can be replied to. | Tool error |
sender_not_own_mailbox |
The address the original arrived at does not deliver to this member's mailbox. | Tool error |
gate_summary_required |
The run has not written the recipient and gist the CEO should read on the approval card. | Handed back |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000003",
"text": "Thursday at 10:00 works."
}
os.mail_send
Send a new mail from the assigned member’s mailbox, once the CEO approves.
What the model reads:
Send a new message from the assigned member’s mailbox: to, subject and text (optional html). It goes out from the member’s own address unless from names another address that delivers to the same mailbox — any other sender is refused, whatever the payload says. Every send waits for the CEO to approve this exact payload, and approval sends that and nothing else. The sent message is filed in the member’s mailbox. To answer a message already in the inbox, use os.mail_reply instead, so it threads.
| Field | Type | Required | Description |
|---|---|---|---|
to |
string | yes | The recipient, one email address |
subject |
string | yes | |
text |
string | yes | The message, as plain text |
html |
string | no | Optional HTML body. Plain text is sent either way |
from |
string | no | Another address that delivers to this member’s mailbox. Defaults to their own |
No other fields are accepted.
- Effect: write.
- Side effects: Files the sent message in the member’s mailbox.
- Retry: Not retry-safe: a repeat acts again. A repeat sends a second message. Call it again only if the first reported a refusal.
- Called by: a run, the API.
- Needs:
- A gate: the CEO decides every call, whatever the task declares.
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- Returns:
{ status, id }:sent, orsend_failedwith the provider’serrorwhen it did not go out.idis the copy filed in the mailbox either way. - API:
POST /v1/runs/self/tools/os.mail_send(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
recipient_invalid |
to is not an email address. |
Tool error |
sender_not_own_mailbox |
from names an address that does not deliver to this member's mailbox. Leave from out to send from the member's own address. |
Tool error |
Example:
{
"to": "[email protected]",
"subject": "Thursday",
"text": "Thursday at 10:00 works for the CEO."
}
os.mail_set_label
Set the labels on a mail in the assigned member’s inbox.
What the model reads:
Set the labels triage gives one message in the assigned member’s inbox. Pass id from os.mail_list and labels, the whole set the mail should carry: it replaces whatever labels it had, and an empty list clears them. Labels are free text (e.g. VIP, Needs reply); there is no fixed list to choose from. List mail by label with os.mail_list.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | A message id from os.mail_list |
labels |
array of string | yes | Every label the mail should carry. Replaces the current set |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ id, labels }: the mail and the labels it now carries. - API:
POST /v1/runs/self/tools/os.mail_set_label(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
email_not_found |
The id is not in this member's mailbox, including another member's message. | Tool error |
labels_invalid |
labels is not a list of non-empty strings of at most 64 characters. | Tool error |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000003",
"labels": [
"VIP"
]
}
os.mail_set_handled
Record whether a mail in the assigned member’s inbox has been handled.
What the model reads:
Record where one message in the assigned member’s inbox stands. Pass id from os.mail_list and handledState: unhandled, replied, skipped, snoozed or escalated. A mail is in one state at a time. snoozed needs snoozedUntil, the date and time it comes back (ISO 8601); every other state takes no date and clears any snooze. os.mail_reply moves the mail it answers to replied on its own.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | A message id from os.mail_list |
handledState |
string, one of unhandled, replied, skipped, snoozed, escalated |
yes | |
snoozedUntil |
string | no | ISO 8601 date and time the mail comes back. Only with handledState snoozed |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ id, handledState, snoozedUntil }: the state the mail is now in. - API:
POST /v1/runs/self/tools/os.mail_set_handled(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
email_not_found |
The id is not in this member's mailbox, including another member's message. | Tool error |
handled_state_invalid |
handledState is not one of the handled states. | Tool error |
snoozed_until_required |
handledState is snoozed and snoozedUntil is missing or not a date. | Tool error |
snoozed_until_not_allowed |
snoozedUntil was given with a handledState other than snoozed. | Tool error |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000003",
"handledState": "snoozed",
"snoozedUntil": "2026-10-01T09:00:00Z"
}
os.mail_set_many
Set the labels, the handled state, or both on many mails in the assigned member’s inbox at once.
What the model reads:
Record the same triage decision on many messages in the assigned member’s inbox in one call. Name the mail one way: ids (up to 200 message ids from os.mail_list), or fromAddress (every unhandled mail that arrived from that exact sender address; mail already handled and mail the member sent are never touched). Then pass labels (the whole set each mail should carry, replacing what it had), handledState (unhandled, replied, skipped, snoozed or escalated; snoozed needs snoozedUntil), or both. Use it to close a run of look-alike mail without reading each one. An id that is not in this mailbox changes nothing and comes back in notFound.
| Field | Type | Required | Description |
|---|---|---|---|
ids |
array of string, at least 1 | no | Up to 200 message ids from os.mail_list. Not together with fromAddress |
fromAddress |
string | no | A sender address, matched exactly: every unhandled mail that arrived from it. Not together with ids |
labels |
array of string | no | Every label each mail should carry. Replaces the current set |
handledState |
string, one of unhandled, replied, skipped, snoozed, escalated |
no | |
snoozedUntil |
string | no | ISO 8601 date and time the mail comes back. Only with handledState snoozed |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ updated, notFound }: how many mails were changed, and the ids given that are not in this mailbox (always[]when selecting byfromAddress). - API:
POST /v1/runs/self/tools/os.mail_set_many(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
member_not_found |
The run's member no longer exists. | Tool error |
mail_selector_invalid |
Neither ids nor fromAddress was given, or both were. | Tool error |
mail_ids_invalid |
ids is not a list of 1 to 200 non-empty message ids. | Tool error |
mail_change_required |
Neither labels nor handledState was given, so there is nothing to record. | Tool error |
labels_invalid |
labels is not a list of non-empty strings of at most 64 characters. | Tool error |
handled_state_invalid |
handledState is not one of the handled states. | Tool error |
snoozed_until_required |
handledState is snoozed and snoozedUntil is missing or not a date. | Tool error |
snoozed_until_not_allowed |
snoozedUntil was given with a handledState other than snoozed. | Tool error |
Example:
{
"fromAddress": "[email protected]",
"labels": [
"Newsletter"
],
"handledState": "skipped"
}
Example:
{
"ids": [
"5f0c1d2e-0000-4000-8000-000000000003",
"5f0c1d2e-0000-4000-8000-000000000004"
],
"handledState": "skipped"
}
os.mail_quiet_threads
The threads the assigned member replied to that have gone quiet, oldest first, at most ten.
What the model reads:
List the conversations in the assigned member’s mailbox where the member had the last word and the correspondent has not answered for three clear weekdays, counted in the enterprise’s timezone. A thread already chased or snoozed today, one snoozed until a later date, and one whose original names no address a reply could go to are left out, so every row is one to act on today. At most the ten oldest.
Takes no arguments: no fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ data }: each row hasthreadKey,replyToEmailId(the newest inbound message: pass it to os.mail_reply asid),correspondent,subjectandlastOutboundAt. An empty list when nothing is quiet or the member has no mailbox. - API:
POST /v1/runs/self/tools/os.mail_quiet_threads(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
Example:
{}
os.mail_mark_chased
Record that a quiet thread was answered today, so no second run today raises it again.
What the model reads:
Record that one thread from os.mail_quiet_threads has been dealt with today. Call it before os.mail_reply on that thread: the reply waits for the CEO, and a second run the same day must not propose a second follow-up while the first is still waiting. The thread is offered again on a later weekday if it is still quiet.
| Field | Type | Required | Description |
|---|---|---|---|
threadKey |
string | yes | A threadKey from os.mail_quiet_threads |
No other fields are accepted.
- Effect: write.
- Side effects: Stores when the thread was last chased on the member's chase record for it. Nothing is sent.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ threadKey, chasedAt }. - API:
POST /v1/runs/self/tools/os.mail_mark_chased(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
thread_not_found |
The threadKey names no conversation in this member's mailbox, including another member's. | Tool error |
Example:
{
"threadKey": "<[email protected]>"
}
os.mail_snooze
Leave a quiet thread alone until a date, instead of drafting a follow-up.
What the model reads:
Snooze one thread from os.mail_quiet_threads until a later date (YYYY-MM-DD or an ISO timestamp): it is not offered again until that date has passed or the member writes in the thread again, and it counts as dealt with today. Use it instead of a follow-up when chasing now would be premature. Nothing is sent.
| Field | Type | Required | Description |
|---|---|---|---|
threadKey |
string | yes | A threadKey from os.mail_quiet_threads |
until |
string | yes | When to raise it again: a date or timestamp after now |
No other fields are accepted.
- Effect: write.
- Side effects: Stores the snooze on the member's chase record for the thread. Nothing is sent.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- The run's assigned member. The mailbox is always theirs: a
memberIdin the payload is ignored, and an OS-owned task has no mailbox.
- The run's assigned member. The mailbox is always theirs: a
- Returns:
{ threadKey, snoozedUntil }. - API:
POST /v1/runs/self/tools/os.mail_snooze(run:self)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
The run has no assigned member, so no mailbox. | Tool error |
thread_not_found |
The threadKey names no conversation in this member's mailbox, including another member's. | Tool error |
snooze_until_invalid |
until is not a date, or is not after now. |
Tool error |
Example:
{
"threadKey": "<[email protected]>",
"until": "2026-10-12"
}
os.escalate_mail
The gate a mail that needs the CEO opens: the mail, why it matters, and what is suggested.
What the model reads:
Escalate a mail the CEO has to see (a VIP, something sensitive, or a question only they can answer), then stop. The gate carries the mail, why it matters and which of its three choices you suggest. It appears on the gates page and in the assigned member’s chat thread, where the CEO chooses Reply, Snooze or Leave it. Reply and Snooze end this run with outcome ceo_approved, the choice as ceoChoice (reply or snooze), this mail as mailId, and ceoNote: what the reply should say, or the ISO 8601 time the snooze ends. Leave it cancels this run and archives the mail. Call it last.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The mail the escalation is about |
mailId |
string | no | The same id, under the key both mail gates use |
subject |
string | yes | |
fromAddress |
string | yes | |
summary |
string | yes | Why this mail needs the CEO |
suggestion |
string | yes | Which choice you suggest (Reply, Snooze or Leave it) and why, in one or two sentences |
No other fields are accepted.
- Effect: write.
- Side effects: Opens one CEO gate for this run and parks the run until it is decided.
- Retry: Not retry-safe: a repeat acts again. Escalating the same mail again opens a second gate carrying its own summary, so the CEO is asked twice. Whether a mail has already been escalated is triage’s to track, not this gate’s.
- Called by: a run, the OS itself, on nobody’s call.
- Needs:
- A gate: the CEO decides every call, whatever the task declares.
- The gate offers these choices in place of Approve and Reject. Reply (
reply): A reply is drafted and comes back to you to approve before anything is sent. Snooze (snooze): Nothing is sent. The mail is set aside until the time you pick. Leave it (leave): No reply is sent and the mail is archived.
- Returns: The CEO’s choice, to the task that runs next:
ceoChoice(replyorsnooze),ceoNoteandmailId. Leave it hands nothing on: the run is cancelled and the mail archived.
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000003",
"subject": "Renewal before Friday",
"fromAddress": "[email protected]",
"summary": "A VIP contact wants to renew before Friday and has asked for a call.",
"suggestion": "Reply: confirm the renewal terms and offer Thursday for the call."
}
os.mail_reply_draft
The gate a drafted reply opens, so no reply leaves without a decision on it.
What the model reads:
Nobody calls this: the OS opens this gate itself when the CEO’s instruction on an escalated mail is to reply. It carries the mail the reply answers and the drafted body, and waits for a decision before anything is sent.
| Field | Type | Required | Description |
|---|---|---|---|
mailId |
string | yes | The mail this reply answers |
body |
string | yes | The drafted reply, as it would be sent |
No other fields are accepted.
- Effect: write.
- Retry: Not retry-safe: a repeat acts again. A second draft for the same mail opens a second gate, decided on its own: nothing folds it into the first.
- Called by: the OS itself, on nobody’s call.
- Returns: The decision on the draft: approved and it may be sent, or not, with the decider's note.
Example:
{
"mailId": "5f0c1d2e-0000-4000-8000-000000000003",
"body": "Thursday works — I will send an invitation shortly."
}
os.list_kpis
List the signed-off KPIs the run's own team owns and those its task is the source of, with their lights.
What the model reads:
List the KPIs this run may read: its task's team KPIs, and any KPI whose source is this run's task, whichever layer owns it. Only KPIs a person has signed off are listed. Each row carries its target (value, direction, AMBER band, on-target-worsening rule), cadence, owner (layer and id), source (kind and ref), latest reading, current light and whether it is overdue. The light is null when the KPI has no target or no reading yet. No arguments. Never another team's KPI unless its source is this run's task, and never another enterprise's.
Takes no arguments: no fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Returns:
{ data }: one row per KPI, each withid,name,ownerLayer(company,team,role,task, or null for a company KPI set before layers),ownerId,sourceKind(taskorperson),sourceRef(the task or person id),target,cadenceMinutes,light,overdueandlastReading(id,value,takenAt,recordedBy,runId), or null before the first reading. - API:
POST /v1/runs/self/tools/os.list_kpis(run:self)
Example:
{}
os.record_kpi_reading
Record a reading for a KPI whose source is this run's task.
What the model reads:
Record one reading for the KPI named by kpiId (from os.list_kpis). Only a run of the task that KPI names as its source (sourceKind task, sourceRef this run's task) may record one, and only once a person has signed the KPI off. Pass value and, optionally, takenAt (ISO 8601; defaults to now). No other field is accepted: no os.* tool lets a run change a target, band, cadence or source. The reading is recorded as taken by this run (`recordedBy: run:<runId>`); a run cannot record on another run's behalf. It lands in the same history HQ's board and the KPI readings API read, so the light they show moves with it.
| Field | Type | Required | Description |
|---|---|---|---|
kpiId |
string | yes | A KPI id from os.list_kpis |
value |
number | yes | |
takenAt |
string | no | ISO 8601. Defaults to now. |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Keyed on the tool call, like every other os.* write tool: a repeat of the same call records once and returns the same reading.
- Called by: a run, the API.
- Returns: The recorded reading and the KPI it landed on:
kpiId,name,readingId,value,takenAt,recordedByandrunId(this run),target, and the KPI’slightas it now stands. - API:
POST /v1/runs/self/tools/os.record_kpi_reading(run:self)
| Error | When | What happens |
|---|---|---|
kpi_id_required |
kpiId is missing. |
Tool error |
value_required |
value is missing or not a number. |
Tool error |
taken_at_invalid |
takenAt is not a timestamp anything can read. |
Tool error |
kpi_not_found |
kpiId does not name a KPI this run may record: another enterprise's, one whose source is not this run's task, or one nobody has signed off yet. |
Tool error |
Example:
{
"kpiId": "5f0c1d2e-0000-4000-8000-000000000009",
"value": 42
}
os.get_enterprise_kpis
The company's KPI targets, bands, cadence and source.
What the model reads:
The company's KPI targets as set: unit, target and direction, AMBER band, cadence and source. A KPI waiting for a target has targetValue null.
Takes no arguments: no fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: the API, the OS MCP server.
- API:
GET /v1/enterprise/kpi-targets(enterprise:read)
os.set_enterprise_kpis
Replace the company's KPI target list.
What the model reads:
Replace the company's complete KPI target list. A target needs its unit, direction, cadence and source; a no-target KPI is accepted. Uses the same validation as the portal.
| Field | Type | Required | Description |
|---|---|---|---|
kpis |
array of object | yes | The complete KPI target list. Every item is validated by the API. |
kpis[].id |
string | no | |
kpis[].name |
string | yes | |
kpis[].unit |
string | no | |
kpis[].targetValue |
number | no | |
kpis[].targetDirection |
string, one of at_least, at_most |
no | |
kpis[].amberBand |
number, at least 0 | no | |
kpis[].amberBandType |
string, one of absolute, percentage |
no | |
kpis[].onTargetWorseningRule |
boolean | no | |
kpis[].cadenceMinutes |
integer, at least 1 | no | |
kpis[].sourceKind |
string, one of task, person |
no | |
kpis[].sourceRef |
string | no | |
kpis[].isKey |
boolean | no |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state.
- Called by: the API, the OS MCP server.
- API:
PATCH /v1/enterprise/kpi-targets(enterprise:write)
| Error | When | What happens |
|---|---|---|
invalid_kpis |
A KPI is incomplete or malformed. | Tool error |
os.get_team_kpis
A team's KPI targets, including its key KPI.
What the model reads:
A team's own KPI targets as set, including which one is its key KPI. This is the target list, not the role and task KPI tree.
| Field | Type | Required | Description |
|---|---|---|---|
teamId |
string | yes | The team whose KPI targets are read. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: the API, the OS MCP server.
- API:
GET /v1/teams/:teamId/kpi-targets(teams:read)
| Error | When | What happens |
|---|---|---|
team_not_found |
The team is not in this enterprise. | Tool error |
os.set_team_kpis
Replace a team's KPI target list and key KPI.
What the model reads:
Replace a team's complete KPI target list. Two key KPIs are refused; a list with none marked is stored with no key KPI. Uses the same validation as the portal.
| Field | Type | Required | Description |
|---|---|---|---|
teamId |
string | yes | The team whose KPI targets are replaced. |
kpis |
array of object | yes | The complete KPI target list. Every item is validated by the API. |
kpis[].id |
string | no | |
kpis[].name |
string | yes | |
kpis[].unit |
string | no | |
kpis[].targetValue |
number | no | |
kpis[].targetDirection |
string, one of at_least, at_most |
no | |
kpis[].amberBand |
number, at least 0 | no | |
kpis[].amberBandType |
string, one of absolute, percentage |
no | |
kpis[].onTargetWorseningRule |
boolean | no | |
kpis[].cadenceMinutes |
integer, at least 1 | no | |
kpis[].sourceKind |
string, one of task, person |
no | |
kpis[].sourceRef |
string | no | |
kpis[].isKey |
boolean | no |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state.
- Called by: the API, the OS MCP server.
- API:
PATCH /v1/teams/:teamId/kpi-targets(teams:write)
| Error | When | What happens |
|---|---|---|
team_not_found |
The team is not in this enterprise. | Tool error |
invalid_kpis |
A KPI is incomplete or two KPIs are marked key. | Tool error |
os.set_context
Store a JSON value for the enterprise under a key, so a later run can read it.
What the model reads:
Store value at key for this enterprise. A later call with the same key replaces the value. Any later run of any task in the enterprise can read it, and a task version that declares a prefix of the key receives it in input.context when a run starts. value is JSON and at most 16KB. This is bookkeeping between runs, not memory, and not a place for a secret. A repeat of the same tool call returns the value that call stored and does not write again.
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | A short key, such as sitemap:https://example.com/pricing. No spaces. Not a secret. |
value |
any | yes | Any JSON value, at most 16KB. Not a secret. |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Keyed on the tool call, like every other os.* write tool: a repeat of the same call stores once and returns the value that call stored.
- Called by: a run, the API.
- Returns:
{ key, value }: the key and the JSON value now stored for it. - API:
POST /v1/runs/self/tools/os.set_context(run:self)
| Error | When | What happens |
|---|---|---|
key_required |
key is missing. |
Tool error |
key_invalid |
key is empty, longer than 512 characters, or contains whitespace or a control character. |
Tool error |
value_required |
value is missing. |
Tool error |
value_not_json |
value is not JSON. |
Tool error |
value_too_large |
value is over 16KB. |
Tool error |
Example:
{
"key": "sitemap:https://example.com/pricing",
"value": {
"status": "absorbed"
}
}
os.get_context
Read one enterprise context value, or null when the key has never been set.
What the model reads:
Read the JSON value stored at key for this enterprise. Returns value null when the key has never been set. A run whose task declares a prefix of the key already has it in input.context; this reads one key that was not loaded, or a key another run has changed since this run started.
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | A short key, such as sitemap:https://example.com/pricing. No spaces. Not a secret. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Returns:
{ key, value }.valueis null when the key has never been set. - API:
POST /v1/runs/self/tools/os.get_context(run:self)
| Error | When | What happens |
|---|---|---|
key_required |
key is missing. |
Tool error |
key_invalid |
key is empty, longer than 512 characters, or contains whitespace or a control character. |
Tool error |
Example:
{
"key": "sitemap:https://example.com/pricing"
}
os.delete_context
Remove one enterprise context key. A missing key is a success.
What the model reads:
Remove key from this enterprise. A key that is not there is a success. Used when the thing the key tracked has gone, such as a URL that left the sitemap. A repeat of the same tool call returns the result that call stored and does not delete a key a later call has set.
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | yes | A short key, such as sitemap:https://example.com/pricing. No spaces. Not a secret. |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Keyed on the tool call, like every other os.* write tool: a repeat of the same call removes the key once and returns that call’s result.
- Called by: a run, the API.
- Returns:
{ key, deleted }.deletedis false when the key was not there. - API:
POST /v1/runs/self/tools/os.delete_context(run:self)
| Error | When | What happens |
|---|---|---|
key_required |
key is missing. |
Tool error |
key_invalid |
key is empty, longer than 512 characters, or contains whitespace or a control character. |
Tool error |
Example:
{
"key": "sitemap:https://example.com/pricing"
}
kpi.record_reading
Record one reading of a KPI by hand: the value, when it was taken, and who or what took it.
What the model reads:
Record one reading of a KPI by hand: the value, when it was taken, and who or what took it. Every reading is kept — correcting a wrong one means recording a new reading, never editing or deleting the old one — so a KPI history shows how it got to where it is. The light and the trend are read off the readings, so recording one is what moves them. A KPI nobody has signed off takes no reading: it is refused with 409 kpi_not_signed_off.
| Field | Type | Required | Description |
|---|---|---|---|
kpiId |
string | yes | The KPI the reading belongs to. |
value |
number | yes | The number read, in the KPI's own unit. |
takenAt |
string | yes | When the reading was taken, as an ISO 8601 timestamp, e.g. 2026-01-08T00:00:00Z. |
recordedBy |
string | yes | Who or what took the reading: a member id, or the agent or tool that read it. |
No other fields are accepted.
- Effect: write.
- Retry: Not retry-safe: a repeat acts again. Every call appends a reading, so a repeat records a second one.
- Called by: the API, the OS MCP server.
- Returns: The reading as recorded:
id,kpiId,value,takenAt(ISO 8601) andrecordedBy. - API:
POST /v1/kpis/:kpiId/readings(kpis:write)
| Error | When | What happens |
|---|---|---|
kpi_not_found |
kpiId names no KPI in this enterprise, another enterprise's included. |
Tool error |
kpi_not_signed_off |
Nobody has signed the KPI off yet, so it takes no reading. | Tool error |
Example:
{
"kpiId": "5f0c1d2e-0000-4000-8000-000000000009",
"value": 3.5,
"takenAt": "2026-01-08T00:00:00Z",
"recordedBy": "weekly-metrics-export"
}
kpi.list_readings
One KPI's readings, newest first, corrected ones included.
What the model reads:
One KPI's readings, newest first: the value, when it was taken and who or what took it. This is the whole history behind the KPI's light and its trend, including any reading a later one corrected.
| Field | Type | Required | Description |
|---|---|---|---|
kpiId |
string | yes | The KPI whose readings to list. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: the API, the OS MCP server.
- Returns: Every reading of the KPI, newest by
takenAtfirst, each withid,kpiId,value,takenAtandrecordedBy. - API:
GET /v1/kpis/:kpiId/readings(kpis:read)
| Error | When | What happens |
|---|---|---|
kpi_not_found |
kpiId names no KPI in this enterprise, another enterprise's included. |
Tool error |
Example:
{
"kpiId": "5f0c1d2e-0000-4000-8000-000000000009"
}
role.list_kpis
A role's KPIs, oldest first, with the team KPI each serves, its sign-off, light, trend and readings.
What the model reads:
A role's KPIs, oldest first: each one's target, direction, AMBER band, cadence, source, the team KPI it serves (servesName names it; served is false for one that serves none), whether a person has signed it off, its light and trend (null until it is signed off) and its readings. Not found for a role in another enterprise.
| Field | Type | Required | Description |
|---|---|---|---|
roleId |
string | yes | The role whose KPIs to list. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: the API, the OS MCP server.
- API:
GET /v1/roles/:roleId/kpis(roles:read)
| Error | When | What happens |
|---|---|---|
role_not_found |
roleId names no role in this enterprise, another enterprise's included. |
Tool error |
role.define_kpi
Propose a KPI on a role, waiting for a person to sign it off.
What the model reads:
Propose a KPI on a role, with the same fields as a KPI on any other layer. It waits for a person to sign it off, and until then draws no light and takes no reading. A KPI may serve only a KPI of the role’s own team (400 role_kpi_serves_other_team otherwise); leave servesKpiId out for one that serves none. The response is the role's KPIs after the change.
| Field | Type | Required | Description |
|---|---|---|---|
roleId |
string | yes | The role the KPI belongs to. |
name |
string | yes | What is measured, up to 200 characters. |
targetValue |
number | no | The target, in the KPI’s own unit. Set it with targetDirection, or leave both out for no target. |
targetDirection |
string, one of at_least, at_most |
no | at_least when higher is better, at_most when lower is better. |
amberBand |
number, at least 0 | no | How far past the target still reads AMBER rather than RED. |
onTargetWorseningRule |
boolean | no | true to read AMBER when on target but worse than the reading before. |
cadenceMinutes |
integer, at least 1 | no | How often it is read, in minutes; a reading older than this is overdue. |
servesKpiId |
string | no | The KPI of the role’s own team this one serves. |
sourceKind |
string, one of task, person |
no | Where readings come from: task (a task records them) or person (entered by hand). |
sourceRef |
string | no | The task id, or the person’s human-user id, sourceKind names. |
No other fields are accepted.
- Effect: write.
- Retry: Not retry-safe: a repeat acts again. Every call adds a KPI to the role, so a repeat proposes a second one.
- Called by: the API, the OS MCP server.
- API:
POST /v1/roles/:roleId/kpis(roles:write)
| Error | When | What happens |
|---|---|---|
role_not_found |
roleId names no role in this enterprise, another enterprise's included. |
Tool error |
role_kpi_serves_other_team |
servesKpiId names a KPI that is not one of the role’s own team. |
Tool error |
role.edit_kpi
Change a role KPI's fields; a signed-off one only a person changes.
What the model reads:
Change a role KPI's fields; the ones left out keep their value. A KPI a person has signed off is changed only by a person, so this is refused with 403 kpi_signed_off_person_only for it. The response is the role's KPIs after the change.
| Field | Type | Required | Description |
|---|---|---|---|
roleId |
string | yes | The role the KPI belongs to. |
name |
string | no | What is measured, up to 200 characters. |
targetValue |
number | no | The target, in the KPI’s own unit. Set it with targetDirection, or leave both out for no target. |
targetDirection |
string, one of at_least, at_most |
no | at_least when higher is better, at_most when lower is better. |
amberBand |
number, at least 0 | no | How far past the target still reads AMBER rather than RED. |
onTargetWorseningRule |
boolean | no | true to read AMBER when on target but worse than the reading before. |
cadenceMinutes |
integer, at least 1 | no | How often it is read, in minutes; a reading older than this is overdue. |
servesKpiId |
string | no | The KPI of the role’s own team this one serves. |
sourceKind |
string, one of task, person |
no | Where readings come from: task (a task records them) or person (entered by hand). |
sourceRef |
string | no | The task id, or the person’s human-user id, sourceKind names. |
kpiId |
string | yes | The KPI to change. |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. The same fields set twice leave the KPI as the first call did.
- Called by: the API, the OS MCP server.
- API:
PATCH /v1/roles/:roleId/kpis/:kpiId(roles:write)
| Error | When | What happens |
|---|---|---|
role_not_found |
roleId names no role in this enterprise, another enterprise's included. |
Tool error |
role_kpi_not_found |
kpiId names no KPI still on this role. |
Tool error |
role_kpi_serves_other_team |
servesKpiId names a KPI that is not one of the role’s own team. |
Tool error |
kpi_signed_off_person_only |
A person has signed the KPI off, so only a person may change it. | Tool error |
os.inspect_enterprise
A snapshot of the company: teams, members, roles, and the tasks of the roles each member holds.
What the model reads:
Read the company as it is now: every team (id, name, slug) and every member (id, name, email, persona, roles), with each member’s tasks: the tasks of the roles they hold on that task’s team (not OS tasks). The same snapshot as os.directory. Use this to name who owns what before you assign, route or escalate work. Read-only; pass no arguments.
No declared fields: the schema accepts any object.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Returns:
{ teams, members }: each team's id, name and slug, and each member's id, name, email, persona, roles, and the tasks of the roles they hold on that task's team (not OS tasks). - API:
POST /v1/runs/self/tools/os.inspect_enterprise(run:self)
os.directory
Teams, members, roles, and the tasks of the roles each member holds: who to offload work to.
What the model reads:
List teams, members, roles, and each member’s tasks: the tasks of the roles they hold on that task’s team (not OS tasks). Use this to find someone to offload work to.
No declared fields: the schema accepts any object.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the CEO chat, the API.
- Returns:
{ teams, members }: each team's id, name and slug, and each member's id, name, email, persona, roles, and the tasks of the roles they hold on that task's team (not OS tasks). The same snapshot asos.inspect_enterprise. - API:
POST /v1/runs/self/tools/os.directory(run:self)
os.list_blockers
What has stopped: failed, stalled, blocked and spend-paused runs, and stopped pipelines, with who owns each.
What the model reads:
What is stopping work right now, as the Blockers page shows it: failed runs not yet cleared (one page, with failedTotal counting them all), stalled, blocked and spend-paused runs, stopped pipelines, missing model tiers, exhausted LLM capacity, task definition problems, and teams with no lead, no metric or an overdue retro. Read-only: nothing is retried or cleared. Waiting gates are not here; use os.list_open_gates.
| Field | Type | Required | Description |
|---|---|---|---|
failedPage |
integer, at least 1 | no | The page of failed runs to read, from 1. Defaults to 1. A page past the last reads the last. |
pageSize |
integer, 1 to 100 | no | Failed runs per page, up to 100. Defaults to 100 in a run, and to 20 over the API. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API, the OS MCP server.
- Returns: What the portal shows as blockers. To a run:
runs, each stopped run once, oldest first, withrunId, thesectionsit is listed in (failed, stopped, blocked, stalled, paused),status,reason,blockClass, itstask,team, teamleadandassignee, therepo,issueandprfrom its input,since,taskRetries(how many times tasks have retried its chain) andescalation(to whom,toYou, the note, when) once someone escalated it; thenfailedTotal(every uncleared failed run), thefailedPageread and itspageSize, the team-health rows (missingLead,overdueRetro, andkpiHealth: a KPI that needs attention, with itscondition(no_key_kpi,no_target,overdueorlight),team,kpi,layer,light,trendandowner),spendEnabledanddraining. Over the API: the portal rows as they are. Waiting gates are not here: read them withos.list_open_gates. - API:
GET /v1/blockers(blockers:read)
os.list_open_gates
Every gate waiting on a decision, oldest first: the CEO inbox's own rows.
What the model reads:
Every gate waiting on a decision, oldest first: the run and task it belongs to, the tool call it holds (toolName, payload), its summary when the run wrote one, who owns the decision, when it times out, and whether the run can be sent back with changes requested. Read-only: deciding a gate is not a tool here.
Takes no arguments: no fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API, the OS MCP server.
- Returns: Each waiting gate:
runId,taskSlug,toolName,owner,payload,summary(when the run wrote one),createdAtand its age. - API:
GET /v1/gates?status=waiting(gates:read)
os.retry_run
Start a stopped run again, with the same input, so its work moves.
What the model reads:
Retry one stopped run: start a fresh run of the same task with the same input. Only a run on Blockers: failed, blocked, or finished on an outcome nothing follows, and not already cleared or retried. Pass runId and note (one line on why another run should get through, recorded on the run). Tasks may retry one chain 3 times; past that, escalate it.
| Field | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The run id, as os.list_blockers names it |
note |
string | yes | Why another run should get through, in one line |
No other fields are accepted.
- Effect: write.
- Side effects: Starts a paid run of the stopped run's task. A blocked run is cancelled as it is retried, and the issue it works is marked in flight again.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Keyed on the tool call: the same call sent twice starts one run. A retried run is refused afterwards, so a later call starts nothing either.
- Called by: a run, the API.
- Returns:
{ runId, retryRunId, status, taskRetries }: the new run,queued, orpendingwhile its task or member is busy, and how many times tasks have now retried the chain. - API:
POST /v1/runs/self/tools/os.retry_run(run:self)
| Error | When | What happens |
|---|---|---|
run_id_required |
runId is missing. |
Tool error |
note_required |
note is missing or empty. |
Tool error |
run_not_found |
No run with that id in this enterprise. | Tool error |
own_run |
The run named is the one calling. | Tool error |
not_stopped:<status> |
The run is not on Blockers: it was cancelled, it finished on an outcome its task follows, or (to retry or clear) it is still active. A stalled or spend-paused run can only be escalated. | Tool error |
already_retried |
A later run already retried or routed it. Act on that run instead. | Tool error |
already_cleared |
Someone already cleared it. | Tool error |
retry_limit_reached |
Tasks have already retried this chain 3 times. | Tool error |
task_not_runnable |
The run's task is deprecated, has no current version, or has nobody to run it. Escalate it instead. | Tool error |
Example:
{
"runId": "5f0c1d2e-0000-4000-8000-000000000001",
"note": "Ran out of steps after pushing most of the change; the next run continues from the branch"
}
os.route_run
Send a stopped run's work to another task of its pipeline, instead of running the same task again.
What the model reads:
Route one stopped run: start a run of another task of its pipeline with the same input, in the same execution, when running the same task again would stop the same way. The task is one that already ran on the run's issue or pull request (send it back to an earlier stage), or one the stopped run's task names as a successor (move it on past this stage). Only a run on Blockers, as for os.retry_run. Pass runId, task (the slug to run next) and note (what that task should do, and why; the new run reads it as routedFrom in its input). Routes and retries share one limit: tasks may retry or route one chain 3 times; past that, escalate it.
| Field | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The run id, as os.list_blockers names it |
task |
string | yes | The slug of the task to run next |
note |
string | yes | What that task should do with it, and why, in a line or two |
No other fields are accepted.
- Effect: write.
- Side effects: Starts a paid run of the task named. A blocked run is cancelled as it is routed, and the issue it works is marked in flight again.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Keyed on the tool call: the same call sent twice starts one run. A routed run is refused afterwards, so a later call starts nothing either.
- Called by: a run, the API.
- Returns:
{ runId, routedRunId, task, status, taskRetries }: the new run,queued, orpendingwhile its task or member is busy, and how many times tasks have now retried or routed the chain. - API:
POST /v1/runs/self/tools/os.route_run(run:self)
| Error | When | What happens |
|---|---|---|
run_id_required |
runId is missing. |
Tool error |
note_required |
note is missing or empty. |
Tool error |
run_not_found |
No run with that id in this enterprise. | Tool error |
own_run |
The run named is the one calling. | Tool error |
not_stopped:<status> |
The run is not on Blockers: it was cancelled, it finished on an outcome its task follows, or (to retry or clear) it is still active. A stalled or spend-paused run can only be escalated. | Tool error |
already_retried |
A later run already retried or routed it. Act on that run instead. | Tool error |
already_cleared |
Someone already cleared it. | Tool error |
task_required |
task is missing. |
Tool error |
same_task |
task is the stopped run's own task. |
Tool error |
task_not_in_pipeline |
No run of that task is on the run's issue or pull request, and its task names no such successor. | Tool error |
retry_limit_reached |
Tasks have already retried or routed this chain 3 times. | Tool error |
task_not_runnable |
The task named is deprecated, has no current version, or has nobody to run it. Escalate it instead. | Tool error |
Example:
{
"runId": "5f0c1d2e-0000-4000-8000-000000000004",
"task": "write-red-suite",
"note": "The locked tests never cover the last acceptance criterion; add them, then implement runs on"
}
os.clear_run
Take a stopped run off Blockers without running it again.
What the model reads:
Take one stopped run off Blockers without running it again, because nothing more is needed: its issue or pull request is closed or merged, or it is a leftover of a wait someone already ended. A failed run, or one that finished on an outcome nothing follows, is cleared; a blocked run is cancelled. Pass runId and note (why nothing more is needed, recorded on the run). Its issue stops being marked blocked once no later run exists.
| Field | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The run id, as os.list_blockers names it |
note |
string | yes | Why nothing more is needed, in one line |
No other fields are accepted.
- Effect: write.
- Side effects: Removes the blocked label from the run's issue when no later run on it exists.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. The same call sent twice clears once. A cleared run is refused afterwards.
- Called by: a run, the API.
- Returns:
{ runId, status, cleared }: the run as it now stands. - API:
POST /v1/runs/self/tools/os.clear_run(run:self)
| Error | When | What happens |
|---|---|---|
run_id_required |
runId is missing. |
Tool error |
note_required |
note is missing or empty. |
Tool error |
run_not_found |
No run with that id in this enterprise. | Tool error |
own_run |
The run named is the one calling. | Tool error |
not_stopped:<status> |
The run is not on Blockers: it was cancelled, it finished on an outcome its task follows, or (to retry or clear) it is still active. A stalled or spend-paused run can only be escalated. | Tool error |
already_retried |
A later run already retried or routed it. Act on that run instead. | Tool error |
already_cleared |
Someone already cleared it. | Tool error |
Example:
{
"runId": "5f0c1d2e-0000-4000-8000-000000000002",
"note": "The pull request merged by hand on 28 Sep"
}
os.escalate_run
Hand a stopped run to its team's lead, or to the CEO, and record that you did.
What the model reads:
Hand one stopped run to someone who can decide it, and record it on the run so nobody hands it on twice. to: lead (the lead of the run's team, or the COO where it has none; they own the decision from then on) or ceo (record it before you put the question to the CEO with os.ask_ceo). Pass runId, to and note (what is stuck, what was tried, and the move you recommend). os.list_blockers shows the escalation on the row from then on.
| Field | Type | Required | Description |
|---|---|---|---|
runId |
string | yes | The run id, as os.list_blockers names it |
to |
string, one of lead, ceo |
yes | |
note |
string | yes | What is stuck, what was tried, and the move you recommend |
No other fields are accepted.
- Effect: write.
- Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. Escalating a run again to the same person records nothing new.
- Called by: a run, the API.
- Returns:
{ runId, to, memberId, memberName, alreadyEscalated }: who now owns the decision.alreadyEscalatedis true when the run was already escalated to them, and nothing new is recorded. - API:
POST /v1/runs/self/tools/os.escalate_run(run:self)
| Error | When | What happens |
|---|---|---|
run_id_required |
runId is missing. |
Tool error |
note_required |
note is missing or empty. |
Tool error |
run_not_found |
No run with that id in this enterprise. | Tool error |
own_run |
The run named is the one calling. | Tool error |
not_stopped:<status> |
The run is not on Blockers: it was cancelled, it finished on an outcome its task follows, or (to retry or clear) it is still active. A stalled or spend-paused run can only be escalated. | Tool error |
already_retried |
A later run already retried or routed it. Act on that run instead. | Tool error |
already_cleared |
Someone already cleared it. | Tool error |
to_invalid |
to is not lead or ceo. |
Tool error |
no_lead |
Nobody leads the run's team (or it has none), and nobody holds the COO role to act as lead. | Tool error |
Example:
{
"runId": "5f0c1d2e-0000-4000-8000-000000000003",
"to": "lead",
"note": "Loop cap on recover-red-suite (2/2): each lap found the same failing test outside the issue. Recommend closing the loop and filing the test separately."
}
os.read_enterprise
The whole enterprise: teams, charters, roles, members and every task in full.
What the model reads:
Read the whole enterprise a plan is proposed against: every team with its charter, roles and members (persona and roles), the members on no team, and every task, OS-owned ones included, in full: goal, metric, guardrails, tools, gates, spend, successors, its assigneeRole and the members holding it (roleHolderIds; assigneeMemberId when exactly one does), and lastRunStatus. A task is assigned to a role, never pinned to a member. With no arguments it returns everything. When that is too big to hand back (tool_result_truncated), read it in slices, each with its tasks in full: noTeam: true for the members and tasks on no team (OS-owned tasks among them), then team (a team id or slug) for each team, with its roles, members and tasks. When one team is too big, read it role by role: team with role (a role id or slug from allRoles in that team slice) for its tasks that run as that role, and team with noRole: true for its tasks that run as no role. Every slice lists allTeams, the id and slug of every team; every team slice also lists allRoles, every role its tasks can run as. Together the slices cover every member and task. OS-owned tasks only.
| Field | Type | Required | Description |
|---|---|---|---|
team |
string | no | A team id or slug: that team, its roles, members and tasks. |
noTeam |
boolean | no | true: the members and tasks on no team. |
role |
string | no | With team only. A role id or slug from allRoles: that team's tasks that run as it. |
noRole |
boolean | no | With team only. true: that team's tasks that run as no role. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.read_enterprise(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
team_not_found:<team> |
No team in this enterprise has that id or slug. Read allTeams with noTeam: true. | Tool error |
team_selector_conflict |
Both team and noTeam are set. |
Tool error |
role_requires_team |
role or noRole is set without team: they narrow one team. |
Tool error |
role_not_found:<role> |
No task on that team can run as a role with that id or slug. The message lists the team's allRoles. | Tool error |
role_selector_conflict |
Both role and noRole are set. |
Tool error |
Example:
{}
Example:
{
"noTeam": true
}
Example:
{
"team": "engineering"
}
Example:
{
"team": "engineering",
"role": "cto"
}
Example:
{
"team": "engineering",
"noRole": true
}
os.search_catalog
Published catalog tasks that match a plain-English intent, best first.
What the model reads:
Find published catalog tasks that match a plain-English intent, best match first (at most 10), so a plan proposes something proven instead of inventing it. Scored on the words of the intent found in a task slug or name; an intent of only filler words matches nothing. Pass intent.
| Field | Type | Required | Description |
|---|---|---|---|
intent |
string | yes |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.search_catalog(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
Example:
{
"intent": "Triage a new GitHub issue"
}
os.list_tool_bindings
The tool types this enterprise has bound, at which layer, and the tools each exposes.
What the model reads:
List the tool types this enterprise has bound, the layer each is bound at, and the individual tools each one exposes, asked of its MCP server. A proposal may only name tools of a bound type. A server that will not answer lists no tools, and its binding is still reported.
Takes no arguments: no fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- Needs:
- Each binding's own credential, to ask its server which tools it exposes.
- API:
POST /v1/runs/self/tools/os.list_tool_bindings(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
Example:
{}
os.list_built_in_tools
Every built-in tool a run can call, with its summary and whether it reads or writes.
What the model reads:
List every built-in tool a run can call: its name, a one-line summary, and its effect (read or write). A task may declare a built-in tool (os.*, workspace.*, memory.*, web.*) only from this list: saving a task that names any other is refused, as a tool that does not exist (tool_unknown) or one only the CEO chat offers (tool_not_run_callable). A row with gate is a tool a task may hold only if it also declares a CEO gate on the tool gate names: saving it without that gate is refused (gate_required_for_tool). Vendor tools are not here: os.list_tool_bindings lists what the enterprise has bound. Takes no arguments.
Takes no arguments: no fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API, the OS MCP server.
- Returns:
[{ name, summary, effect, gate? }]: one row per built-in tool a run can call, in reference order.gateis only on a tool a task may hold only with a CEO gate, and names the tool that gate is declared on. - API:
GET /v1/tools/built-in?callable=run(tools:read)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
Example:
{}
os.read_task
One task in full, its pending recommendations, and the tasks that start it.
What the model reads:
One task in full (as os.read_enterprise reads a task), the improvement recommendations still pending against it, and incomingSuccessors: the tasks whose successors start it. Its own successors are what it starts. Pass taskId.
| Field | Type | Required | Description |
|---|---|---|---|
taskId |
string | yes |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.read_task(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
task_id_required |
taskId is missing. |
Tool error |
task_not_found |
No task in this enterprise has that id. | Tool error |
Example:
{
"taskId": "5f0c1d2e-0000-4000-8000-000000000001"
}
os.assess_impact
What points at a role, team, task or member a plan would delete.
What the model reads:
What points at a role, team, task or member a plan would delete. role: the members holding it and the tasks assigned to it. team: the roles, members and tasks on it. task: the tasks whose successors name it and its runs in flight. member: the roles they hold, the teams they sit on and the tasks their roles run. Reported, never refused: the plan says what the deletion breaks. Pass kind and id.
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string, one of role, team, task, member |
yes | |
id |
string | yes |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.assess_impact(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
id_required |
id is missing. |
Tool error |
kind_invalid |
kind is not role, team, task or member. |
Tool error |
Example:
{
"kind": "role",
"id": "5f0c1d2e-0000-4000-8000-000000000002"
}
os.write_plan
Write or replace this chat session's plan, checked before the CEO sees it.
What the model reads:
Write this chat session's plan for the CEO, or replace it: one plan per chat session, and a replace carries the version os.read_plan returned. Every item is checked first, and the first problem comes back as the reason: a tool whose type this enterprise has not bound, a slug it already has, a task pinned to a member (set assigneeRole), or a task definition a saved task would fail. Fix it and write again. Pass chatSessionId, summary, ceoIntent, kind (default os_change), items and, to replace, version.
| Field | Type | Required | Description |
|---|---|---|---|
chatSessionId |
string | yes | |
summary |
string | yes | |
ceoIntent |
string | no | |
kind |
string | no | |
items |
array of object | yes | |
version |
integer, at least 1 | no |
No other fields are accepted.
- Effect: write.
- Side effects: Moves the plan to
awaiting_ceo. - Retry: Idempotent: a repeat of the same call acts once, or leaves the same state. A repeat replaces the same plan with the same content rather than adding a second. A repeat carrying a version the plan has moved past is refused as
plan_version_conflict. - Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.write_plan(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
chat_session_id_required |
chatSessionId is missing. |
Tool error |
summary_required |
summary is missing. |
Tool error |
plan_payload_invalid |
An item is not one the plan can apply (a missing handle, an unknown type). | Tool error |
tool_type_not_bound:<type> |
An item names a tool whose type this enterprise has not bound. | Tool error |
slug_collision:<slug> |
An item creates a task, role or team with a slug the enterprise already has. | Tool error |
assignee_member_removed |
A create_task item names assigneeMemberId. A task is assigned to a role: set assigneeRole. |
Tool error |
definition_issue:<code> |
A proposed task fails the check a saved task must pass (an unknown successor, a deterministic task with tools). | Tool error |
plan_version_conflict |
version is not the version the plan is at now. Read it back with os.read_plan and write again. |
Tool error |
Example:
{
"chatSessionId": "chat-42",
"ceoIntent": "Hire a support lead",
"summary": "Creates a Support Lead role",
"kind": "os_change",
"items": [
{
"type": "create_role",
"handle": "r1",
"slug": "support-lead",
"name": "Support Lead"
}
]
}
os.read_plan
This chat session's plan as it stands, with its version.
What the model reads:
Read this chat session's plan as it stands, with the version a replacing os.write_plan must carry, so a feedback round continues it rather than starting again. Null when the session has no plan. With no chatSessionId, the plan this run wrote.
| Field | Type | Required | Description |
|---|---|---|---|
chatSessionId |
string | no |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API.
- API:
POST /v1/runs/self/tools/os.read_plan(run:self)
| Error | When | What happens |
|---|---|---|
os_owned_only |
The run's task is not OS-owned. These tools read every team's work, so only an OS task may call them. | Tool error |
Example:
{
"chatSessionId": "chat-42"
}
os.file_daily_brief
File the CEO's brief in your chat thread: what is waiting on them, and the time on their clock.
What the model reads:
File the CEO's brief as one message in your chat thread with them. The OS composes it: every gate still waiting on the CEO on a run of one of your tasks (what it asks, answered on Gates), an escalations section, and the time on the CEO’s clock in the enterprise timezone. It is filed even when nothing is waiting, and then says nothing needs the CEO. Pass edition: morning or evening. Sends no mail and opens no gate. Call it once per run.
| Field | Type | Required | Description |
|---|---|---|---|
edition |
string, one of morning, evening |
no | Which sitting the brief is for. Defaults to the edition in your run input. |
No other fields are accepted.
- Effect: write.
- Side effects: One assistant message in the member's open chat thread with the CEO.
- Retry: Not retry-safe: a repeat acts again. A repeat files a second brief in the thread: call it once per run.
- Called by: a run, the API.
- Returns:
{ filed: true, edition, messageId, waiting }:waitingis how many things the brief named. - API:
POST /v1/runs/self/tools/os.file_daily_brief(run:self)
| Error | When | What happens |
|---|---|---|
daily_brief_edition_invalid |
Neither the call nor the run input names morning or evening. | Tool error |
daily_brief_timezone_invalid |
The enterprise's timezone is not one the OS knows. | Tool error |
daily_brief_no_member |
The run has no member whose thread the brief could be filed in. | Tool error |
Example:
{
"edition": "morning"
}
os.chat_list
List chat threads, most recently active first: who each is with, its state and its last message.
What the model reads:
List chat threads, most recently active first: each thread’s id, its state (idle, replying or closed), and its last message. In a run these are the threads of the member doing the run, and memberId is ignored. Over the API they are every thread of the enterprise, or only one member’s when you pass memberId. No message bodies beyond the last: read a thread with os.chat_read. A conversation opened for one job, such as designing a task, is never listed.
| Field | Type | Required | Description |
|---|---|---|---|
memberId |
string | no | Over the API: only threads with this member. A run always lists its own member’s. |
No other fields are accepted.
- Effect: read.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API, the OS MCP server.
- Needs:
- In a run, the run's assigned member: the threads are always theirs, and an OS-owned task has none. Over the API and the MCP server, any thread of the enterprise the token can reach.
- Returns: To a run:
{ threads }, each withid,status(idle,replyingorclosed),lastMessage(role,content,createdAt, or null in an empty thread),lastReadAt(when a person last opened it, or null),createdAtandupdatedAt. Over the API: the Chat page’s rows as they are, which add thememberand theunreadandunreadCountof the person’s side. - API:
GET /v1/chats(chats:read)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
A run with no assigned member called it, so there is no thread. | Tool error |
Example:
{}
os.chat_read
Read one chat thread: its messages, oldest first.
What the model reads:
Read one chat thread: its messages, oldest first, each with who wrote it. Pass id from os.chat_list, and optionally limit to read only the newest messages (up to 200). In a run you get what was said (people’s messages and the member’s), the newest 50 unless you pass limit, and reading does not mark the thread read. Over the API you get every row, tool calls included, and reading marks the thread read, as opening it in the portal does.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | A thread id from os.chat_list |
limit |
integer, 1 to 200 | no | Only the newest messages, up to 200. In a run it defaults to 50; over the API, to all of them. |
No other fields are accepted.
- Effect: read.
- Side effects: Over the API and the MCP server, marks the thread read, the same as opening it in the portal. In a run, nothing.
- Retry: Safe: repeating the call changes nothing.
- Called by: a run, the API, the OS MCP server.
- Needs:
- In a run, the run's assigned member: the threads are always theirs, and an OS-owned task has none. Over the API and the MCP server, any thread of the enterprise the token can reach.
- Returns: To a run:
{ id, status, total, messages }, wheretotalcounts every message said in the thread and each message hasid,role(userfor a person or an API token,assistantfor the member),content,sender(the name of whoever wrote ausermessage, else null) andcreatedAt. Over the API: the thread as the portal reads it, with itsmemberand every row, each with itssender. - API:
GET /v1/chats/:id(chats:read)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
A run with no assigned member called it, so there is no thread. | Tool error |
chat_not_found |
No thread has that id: in a run, including another member's thread and a conversation opened for one job. | Tool error |
Example:
{
"id": "5f0c1d2e-0000-4000-8000-000000000021",
"limit": 20
}
os.chat_write
Write one message in a chat thread, as yourself.
What the model reads:
Write one message in a chat thread, as yourself. Pass content: plain text, shown as written with line breaks kept and no formatting. In a run you write as the member doing the run, for the enterprise’s people to read: it shows as unread until someone opens the thread, nobody replies to the run, and the run carries on. Leave id out to write in the member’s open thread (one is opened if there is none), or pass id from os.chat_list. At most 4000 characters in a run. For a question you need answered, use os.ask_ceo. Over the API, id is required, you write as the person or token calling, and the member replies: the response is the thread with that reply in it. Sends no mail and opens no gate. A repeat writes the message again.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | no | The thread, from os.chat_list. A run may leave it out to write in its member’s open thread. Required over the API. |
content |
string | yes | The message, as plain text |
No other fields are accepted.
- Effect: write.
- Side effects: In a run, one message from the member in the thread. Over the API, one message from the caller and a reply turn by the member, which spends on the enterprise’s model keys.
- Retry: Not retry-safe: a repeat acts again. A repeat writes the message again: call it once for each thing you have to say.
- Called by: a run, the API, the OS MCP server.
- Needs:
- In a run, the run's assigned member: the threads are always theirs, and an OS-owned task has none. Over the API and the MCP server, any thread of the enterprise the token can reach.
- Returns: To a run:
{ written: true, id, messageId }, the thread it went in and the message. Over the API: the thread as os.chat_read returns it, with the member’s reply. - API:
POST /v1/chats/:id/messages(chats:write)
| Error | When | What happens |
|---|---|---|
no_assigned_member |
A run with no assigned member called it, so there is no thread. | Tool error |
chat_not_found |
No thread has that id: in a run, including another member's thread and a conversation opened for one job. | Tool error |
empty_message |
content is missing, not text, or blank. |
Tool error |
message_too_long |
In a run, content is longer than 4000 characters. |
Tool error |
chat_closed |
The thread is closed. | Tool error |
chat_replying |
Over the API, the member is still replying to an earlier message in the thread. | Tool error |
Example:
{
"content": "New enquiry\nFrom: [email protected]\nAsking about: the Team plan"
}
Example, Over the API: a person’s message to the member, answered in the response.:
{
"id": "5f0c1d2e-0000-4000-8000-000000000021",
"content": "Where did last week’s report go?"
}
os.chat_open
Open a new chat thread with a member.
What the model reads:
Open a new, empty chat thread with a member, and get its id to write in with os.chat_write. Pass memberId. Nothing is said and nothing is spent until a message is written. A run cannot call it: a run’s os.chat_write opens its member’s thread when there is none.
| Field | Type | Required | Description |
|---|---|---|---|
memberId |
string | yes | The member to talk to |
No other fields are accepted.
- Effect: write.
- Retry: Not retry-safe: a repeat acts again. A repeat opens another thread with the same member.
- Called by: the API, the OS MCP server.
- Returns: The new thread: its
id,memberId,status(idle) and times. - API:
POST /v1/chats(chats:write)
| Error | When | What happens |
|---|---|---|
memberId_required |
memberId is missing. |
Tool error |
member_not_found |
No member of this enterprise has that id. | Tool error |
Example:
{
"memberId": "5f0c1d2e-0000-4000-8000-000000000007"
}