# MCP tool reference

> Every tool sanda's MCP server offers, with the scope it needs, its parameters and what it does. Generated from the tool definitions the server runs.

This is every tool sanda's MCP server can offer an assistant. The names, titles, scopes and parameters below are read from the same tool definitions the server runs, so they cannot drift from what a client sees.

## How a credential is offered tools

`tools/list` returns the tools **this credential** may use, not every tool. A tool is left off the list, and refused if called anyway, when any of these is true:

- **The credential does not hold the scope.** Each tool needs one scope. A wider scope includes a narrower one, so a credential holding `sql:write` also gets everything `sql:run` and `query:run` give. See [scopes](https://docs.sanda-os.com.au/reference/scopes).
- **The edition does not include it.** The sanda search tools (`search_documents` and every tool needing `search:manage`) are on Standard and Enterprise.
- **The person behind it is no longer allowed.** A privileged scope is honoured only while the person who granted it is an owner or admin.
- **The workspace is closed.** Then only the tools that read remain: asking questions, looking up the docs and seeing connections.

`action_status` is offered only to a credential that holds a scope one of the held tools needs. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals).

A token created with no extra switches is offered `search_fluid`, `describe_table`, `run_query`, `search_documents` and `search_docs`.

## Reading an entry

Each entry starts with the tool's name (the string a client sends), then a row of markers: the tool's title, what kind of tool it is, the scope it needs and, where it applies, the editions that include it. The kind comes from the tool's annotations, which a client can use to skip a confirmation prompt or add one:

- **Read only** means the tool changes nothing, so calling it again is safe.
- **Writes** means the tool changes something.
- **Destructive** means the tool can remove or overwrite something, such as taking tables off the map, dropping a view or committing SQL.

The parameter table lists every top-level parameter with its type, whether it is required, and what it means. Where a parameter is an object or a list, its fields are described in the parameter's own text or in the entry's notes.

## Limits every tool shares

| Limit | Value |
|---|---|
| Rows or hits returned by `run_query`, `run_sql` and `search_documents` | At most 50 |
| Rows returned by `execute_sql` | 50 by default, at most 500 |
| Rows loaded or added in one `load_rows` or `append_rows` call | 10,000 |
| Request body | 1,000,000 bytes |
| Time a tool holds a call open when it builds or runs something | 55 seconds. A longer job belongs on a schedule |
| Time a `run_query` or `run_sql` statement may run | 20 seconds |
| Held actions a workspace may have waiting | 50 |

A tool that cannot answer returns a sentence and `isError: true` rather than a failure, for example that two tables would fan out or that a column does not exist. An assistant is expected to read it and try again differently.

## Resources

sanda also serves two resources for clients that read them. Both need `fluid:read`, like `search_fluid`. A credential without it is listed no resources, and reading one is refused the way a tool is, with a step-up challenge for a connected app:

| Resource | What it holds |
|---|---|
| `becca://fluid/brief` | The one-page map of the workspace: its tables and the names of every metric, dimension, filter and category. Read this first |
| `becca://fluid/context` | Everything in full: every column, expression, sample value and join. Large, so prefer the brief plus tool calls |

## cherry and MCP

cherry, the console's own agent, uses most of these tools too, under the signed-in person's role and the capabilities switched on under **Settings · cherry**. It is not offered `execute_sql`, `set_intake`, `append_rows` or the tools that manage sanda search, and `action_status` is for MCP credentials alone. cherry also has tools of its own for building reports, which are never served over MCP. See [cherry](https://docs.sanda-os.com.au/cherry/capabilities).


## Read the fluid and your data

Find what the workspace has modelled, look closely at a table, ask a question and search the text inside rows.

### search_fluid

<p class="dx-chips"><span class="dx-chip">Search the fluid</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-read">fluid:read</a></p>

Finds what the workspace has modelled: metrics, dimensions, named filters, tables and columns. An assistant calls it before using any name it has not already seen in the workspace brief or an earlier answer, because a metric found here is the definition the business agreed on.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | No | A word or fragment to match against names, synonyms, descriptions, definitions and column names. Leave it empty to list everything. |
| `kind` | string, one of `all`, `metrics`, `dimensions`, `filters`, `tables`, `columns` | No | Narrows the search to one kind of thing: `all` (the default), `metrics`, `dimensions`, `filters`, `tables` or `columns`. |

The answer is text, one line per match. A search that finds nothing says so, which is an answer: try a synonym before concluding the question cannot be asked.

### describe_table

<p class="dx-chips"><span class="dx-chip">Describe a table</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-read">fluid:read</a></p>

Describes one table in full: every column with its type, the grain, the primary key, the time column, the metrics and dimensions measured on it and every join it can follow. An assistant calls it before it names a column.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `table` | string | Yes | The table's business name, its schema-qualified relation or its identifier, for example `order details`, `mapping.order_details` or `order_details`. Case and separators do not matter. |

A table that is not on the map is not found, and the answer names the closest tables that are.

### run_query

<p class="dx-chips"><span class="dx-chip">Run a composed query</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#query-run">query:run</a></p>

Answers a question with rows. The assistant names what it wants by reference (metrics, dimensions, filters, conditions) and sanda composes the SQL, resolves the joins and runs it, so a number means what the workspace says it means. This is the tool for almost every question.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `metrics` | array of strings | No | Metric names from `search_fluid`. These are the numbers the business has defined. |
| `dimensions` | array of strings | No | Dimension names to break the numbers down by. |
| `granularity` | string, one of `day`, `week`, `month`, `quarter`, `year` | No | How coarsely to group a time dimension: `day`, `week`, `month`, `quarter` or `year`. Empty means one row per underlying value. |
| `columns` | array of objects | No | Raw columns for questions no metric covers, each as a fully-qualified `schema.table.column` and an `aggregate` (`sum`, `avg`, `min`, `max`, `count`, `count_distinct`, or empty to list the value). A well-behaved assistant says when it has leaned on one, because the answer rests on a column name rather than an agreed definition. |
| `filters` | array of strings | No | Named filter names from `search_fluid`: conditions the workspace has already agreed on. |
| `conditions` | array of objects | No | Extra narrowing for this question only. Each condition has a `field` (a dimension name or a fully-qualified column), an `op`, a list of `values` and an optional named period in `relative`. Operators are the same ones the console's filters use. |
| `order_by` | array of objects | No | How to sort, first entry first, as `by` (a metric, dimension or column already in the query) and `direction` (`asc` or `desc`). Left out, a number broken down by a label comes back largest first and a time series in time order. |
| `limit` | integer | No | How many rows to return. The default and the maximum are both 50. |

Operators and named periods are listed in [filters](https://docs.sanda-os.com.au/reference/filters). The schema marks every key of a condition as required, so send all four: use an empty list for `values` and an empty string for `relative` when they do not apply. `relative` is only for `in_range` and names a period such as `last_month`; sanda resolves it in UTC and states the window it used.

```json
{
  "metrics": ["net revenue"],
  "dimensions": ["region", "issue date"],
  "granularity": "month",
  "conditions": [
    { "field": "issue date", "op": "in_range", "values": [], "relative": "last_6_months" }
  ],
  "limit": 10
}
```

The answer is text: a table of rows, the window any named period resolved to, and the SQL sanda composed. Metrics measured on different tables can be asked for together, and each is counted on its own table, so revenue beside order count counts every order once. Each metric keeps its own condition. Two tables that would fan out if joined come back as a sentence explaining the refusal, not as a wrong number. A column whose name looks like it holds a secret is never returned.

### search_documents

<p class="dx-chips"><span class="dx-chip">Search the text inside the rows</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#query-run">query:run</a><span class="dx-chip">Standard · Enterprise</span></p>

Finds rows by what their text says, for questions about wording rather than about a number, such as which invoices mention water damage. It searches meaning and words together and answers with rows and never a total.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | Yes | What to look for, in words. A phrase or a sentence works better than a keyword, because meaning is half of how this searches. |
| `service` | string | No | Which search service to search, by name. Leave it out when the workspace has only one. |
| `k` | integer | No | How many rows to return. The default is 10 and the maximum is 50. |
| `filters` | array of objects | No | Narrows the search by a service's attribute columns, each as `column`, `op` (`eq`, `neq`, `in`, `contains`, `gt`, `gte`, `lt`, `lte`) and `value`. Only columns the service declares as attributes can be filtered. |

Every hit carries the value of its table's key column. To turn hits into a number, take those keys and call `run_query` with a condition on them.

This is a question of the data, so it needs only `query:run`, and it is offered to any client holding that scope on an edition with sanda search (Standard and Enterprise). If the workspace has no search service yet, a call answers that there is nothing to search. Managing services is a separate, privileged scope (`search:manage`).

### run_sql

<p class="dx-chips"><span class="dx-chip">Run SQL you wrote</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#sql-run">sql:run</a></p>

Runs one read-only SELECT the assistant wrote itself. It is the fallback for a question `run_query` cannot express, such as a window function or a cohort. It bypasses the workspace's own definitions, so the number is the assistant's rather than the business's.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `sql` | string | Yes | One PostgreSQL SELECT (a leading WITH is fine). No semicolon, no comments, no DDL or DML. Every relation must be one `describe_table` listed. |
| `why` | string | Yes | One sentence naming what `run_query` could not express. It is required, and a client is expected to show it to the person beside the answer. |

Runs through the reader role inside a read-only transaction, returns at most 50 rows and stops after 20 seconds. A column whose name looks like a credential or an identity number is refused, whatever the statement calls it.

A default integration token does not see this tool at all: it is listed only for credentials granted `sql:run`. cherry is offered it only when its sql capability is switched on under **Settings · cherry**.

### search_docs

<p class="dx-chips"><span class="dx-chip">Search the sanda docs</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-read">fluid:read</a></p>

Searches sanda's own documentation at docs.sanda-os.com.au for how the product works and how to do something in it. An assistant calls it for a question about sanda, never for a question about your data, and gives you the link it used.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `query` | string | Yes | What to look for, in a few words, for example `connect xero`, `incremental sync` or `invite a member`. |
| `limit` | integer | No | How many sections to return, from 1 to 5. The default is 3. |

The answer is text: for each match, the page and section title, the address of the section, and the section's own words. A search that finds nothing says so and points at the docs site.

This reads public pages and nothing of the workspace's data, so it needs only `fluid:read`, and a call is never held for approval. It makes no model call, so it costs nothing on the AI limit. cherry uses it too, to answer how-to questions with a link.

## See and refresh the workspace

Check which connections and warehouses exist, how fresh and how full they are, and start a sync.

### list_connections

<p class="dx-chips"><span class="dx-chip">List connections</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#workspace-read">workspace:read</a></p>

Lists every data connection in the workspace: the system it pulls from, its health, when it last synced, how many rows it has landed, its schedule and where it lands. An assistant uses it to answer whether the data is fresh.

Takes no parameters.

Never returns a credential.

### connection_status

<p class="dx-chips"><span class="dx-chip">One connection, in detail</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#workspace-read">workspace:read</a></p>

Shows one connection's last ten runs with status, rows, duration and any error text, and the dataset its rows became. While a sync is running it also reports the stage, rows read so far, the average rate and each stream's state.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection` | string | Yes | The connection's name or id, from `list_connections`. |

### list_warehouses

<p class="dx-chips"><span class="dx-chip">List warehouses</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#workspace-read">workspace:read</a></p>

Lists each warehouse with its storage against its limit, compute, table and row counts, and whether ingest is paused. An assistant uses it before a large load to check how much room is left.

Takes no parameters.

Never returns a connection string.

### sync_now

<p class="dx-chips"><span class="dx-chip">Start a sync</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#workspace-manage">workspace:manage</a></p>

Starts one connection's sync now instead of waiting for its schedule. The rows land in the background, and `connection_status` shows how far the run has got.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `connection` | string | Yes | The connection's name or id, from `list_connections`. |

A sync reads from the source system and lands rows in the warehouse, so it costs compute like any other sync. This is the only tool that drives a connection.

## Write the semantic fluid

Put landed tables on the map, define metrics, dimensions, filters and relationships, and review what sanda proposed. Definitions go live at once.

### list_warehouse_tables

<p class="dx-chips"><span class="dx-chip">List tables in the warehouse</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Lists every table that has landed in the warehouse, with its columns, a row estimate and whether it is already on the semantic map. This is the raw layer `describe_table` cannot see, and the first step before `import_tables`.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `only` | string | No | One schema-qualified relation to describe on its own, for example `raw.xero__invoices`. Leave it empty to list them all. |

### import_tables

<p class="dx-chips"><span class="dx-chip">Put warehouse tables on the map</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Puts landed tables on the semantic map so the fluid can query them. sanda reads each table's shape, publishes a view over the columns chosen and files it under its connector's dataset, then reports the kind, key and time column it guessed and the joins it drew.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `relations` | array of strings | Yes | Schema-qualified relations to add, for example `raw.xero__invoices`, from `list_warehouse_tables`. Up to 60 at a time. |
| `columns` | object | No | Optional. For each relation, the column names to keep. The key and time columns are always kept. Leave a relation out to take every column. |
| `kinds` | object | No | Optional. For each relation, `fact` or `dimension`, when the caller already knows. A fact records things that happened (orders, invoices) and a dimension records things that are (products, customers). This overrides sanda's guess. |
| `relationships` | string, one of `infer`, `none` | No | `infer` (the default) draws joins that the column names assert, such as a column named exactly like another table's key. `none` draws none. Ambiguous matches are reported and never drawn. |

Importing a table that is already on the map refreshes it and does not duplicate it. Nothing here changes the rows in the warehouse.

### set_table_kind

<p class="dx-chips"><span class="dx-chip">Mark a table as a fact or a dimension</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Says whether a table on the map is a fact or a dimension. Getting this right is what stops a query summing a price list. An assistant calls it after `import_tables` when sanda's guess was wrong.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `table` | string | Yes | The table as the fluid names it, its relation or its identifier. Case and separators do not matter. |
| `kind` | string, one of `fact`, `dimension` | Yes | `fact` or `dimension`. |

### retire_tables

<p class="dx-chips"><span class="dx-chip">Take tables off the map</span><span class="dx-chip is-danger">Destructive</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Takes tables off the semantic map. The rows in the warehouse are untouched, but every metric, dimension, filter and relationship written against those tables goes with them.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `tables` | array of strings | Yes | Business names, relations or identifiers, as `import_tables` named them. |

Destructive to the model, not to the data. A well-behaved assistant reads the table names back to the person before calling it. cherry always asks first, whatever its mode.

### define

<p class="dx-chips"><span class="dx-chip">Define something in the fluid</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Creates or replaces one definition in the fluid, live at once: a metric, dimension, filter, relationship, alias, category or dataset, or a statement of what a table already on the map means.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string, one of `metric`, `dimension`, `filter`, `relationship`, `alias`, `category`, `dataset`, `table` | Yes | What to define: `metric`, `dimension`, `filter`, `relationship`, `alias`, `category`, `dataset` or `table`. |
| `definition` | object | Yes | The fields for that kind, as an object. Each kind's fields are listed in the notes below. |

A definition with the same name replaces the old one, and names are matched without regard to case. The reply names the table the definition landed on and says if an expression uses a column that table does not have.

| Kind | Fields |
|---|---|
| `metric` | `name`, `table`, `expression` (one SQL fragment over that table's own columns, for example `sum(total_ex_gst)`), and optionally `aggregation` (`sum`, `avg`, `count`, `count_distinct`, `min`, `max`, `ratio`, `custom`), `filters`, `unit`, `format` (`currency`, `percent`, `integer`, `decimal`, `duration`), `description`, `synonyms` |
| `dimension` | `name`, `column` (as `schema.table.column`), and optionally `kind` (`categorical`, `time`, `geo`, `boolean`, `numeric`), `description`, `synonyms` |
| `filter` | `name`, `table`, `expression` (a named WHERE condition), optionally `description` |
| `relationship` | `left_column`, `right_column`, `key` (the business name of the shared identity), optionally `left_cardinality`, `right_cardinality` (`one` or `many`), `description` |
| `alias` | `column`, `term`, optionally `description` |
| `category` | `name`, optionally `description` |
| `dataset` | `name`, optionally `description` |
| `table` | `table`, and any of `description`, `grain`, `primary_key`, `time_column`, `kind`, `name`. Only the fields given change |

```json
{
  "kind": "metric",
  "definition": {
    "name": "net revenue",
    "table": "invoices",
    "expression": "sum(total_ex_gst)",
    "format": "currency",
    "synonyms": ["turnover"]
  }
}
```

### list_proposals

<p class="dx-chips"><span class="dx-chip">List proposals awaiting review</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Lists everything sanda's own learning pass has proposed and nobody has signed off: tables, metrics, dimensions, filters and relationships. Proposals are invisible to queries until they are accepted.

Takes no parameters.

### review_proposal

<p class="dx-chips"><span class="dx-chip">Accept or reject a proposal</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Accepts or rejects one proposal. An accept may carry corrections, so a nearly right proposal is one call. A relationship is decided on both sides together.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string, one of `metric`, `dimension`, `filter`, `mapping`, `table`, `category`, `dataset`, `glue` | Yes | What is being reviewed: `metric`, `dimension`, `filter`, `mapping`, `table`, `category`, `dataset` or `glue`. |
| `id` | string | Yes | The proposal's id, from `list_proposals`. |
| `action` | string, one of `accept`, `reject` | Yes | `accept` or `reject`. |
| `edits` | object | No | Optional corrections applied when accepting, in the same shape `define` takes for that kind. |

Accept a relationship's tables before the relationship.

### delete_definition

<p class="dx-chips"><span class="dx-chip">Delete a definition</span><span class="dx-chip is-danger">Destructive</span><a class="dx-chip is-scope" href="../reference/scopes#fluid-write">fluid:write</a></p>

Removes one metric, dimension, filter, mapping, category, dataset or glue definition from the fluid for good. Redefining with `define` is better than deleting and recreating, because a delete loses the definition's history.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string, one of `metric`, `dimension`, `filter`, `mapping`, `category`, `dataset`, `glue` | Yes | What to remove: `metric`, `dimension`, `filter`, `mapping`, `category`, `dataset` or `glue`. Use `retire_tables` for a table. |
| `id` | string | Yes | The definition's id. |

cherry always asks first, whatever its mode.

## Load and change warehouse data

Land tables of an agent's own rows, add rows to tables opened for intake, and run one committed SQL statement of its own.

### load_rows

<p class="dx-chips"><span class="dx-chip">Load rows into the warehouse</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-write">warehouse:write</a></p>

Lands a table of an agent's own rows in the warehouse as `raw.csv__<table>`, through the same loader and storage limit a sync uses. Typical uses are a forecast it computed or a lookup it was given. It cannot touch a table a sync landed.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `table` | string | Yes | A short name. It becomes `raw.csv__<name>`, lower-cased with underscores. |
| `columns` | array of objects | Yes | The columns in order, each with a `name` and an optional `type` (`text`, `bigint`, `integer`, `numeric`, `boolean`, `date`, `timestamptz` or `jsonb`). Leave the type empty to infer it from the values. Up to 250 columns. |
| `rows` | array of anys | Yes | The rows, each either a list of values in column order or an object keyed by column name. Empty cells are null. Up to 10,000 rows per call. |
| `mode` | string, one of `replace`, `append` | No | `replace` (the default) swaps the table for the new rows, in their new shape, and keeps the views built on it. It is refused, naming the view, when one of your own views reads a column the new rows drop or retype. `append` adds to it and must match its columns. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |
| `import` | boolean | No | When true, also puts the table on the semantic map so `run_query` can reach it. |
| `intake` | boolean | No | When true, also opens the table for intake, so credentials holding `warehouse:append` may add rows to it. |

Send `rows: []` with declared types to create an empty table. For more than 10,000 rows, append in batches.

```json
{
  "table": "sales forecast",
  "columns": [{ "name": "month", "type": "date" }, { "name": "forecast", "type": "numeric" }],
  "rows": [["2026-10-01", 125000], ["2026-11-01", 131500]],
  "import": true
}
```

### set_intake

<p class="dx-chips"><span class="dx-chip">Open or close a table for intake</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-write">warehouse:write</a></p>

Opens a hand-loaded table for intake, or closes it. While a table is open, credentials holding `warehouse:append` may add rows to it and do nothing else. Closing leaves the rows and the map as they are.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `table` | string | Yes | The table as `load_rows` named it. `dad habits`, `csv__dad_habits` and `raw.csv__dad_habits` are one table. |
| `open` | boolean | Yes | `true` opens the table, `false` closes it. |
| `note` | string | No | Optional. What the table is for, shown beside it in the **Intake tables** panel of **Settings · Integrations**. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

The shape for a log a person keeps by talking to an assistant: land the table once with `load_rows`, open it here, then give the person a credential holding only `warehouse:append`. Not offered to cherry.

### append_rows

<p class="dx-chips"><span class="dx-chip">Add rows to an intake table</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-append">warehouse:append</a></p>

Adds rows to a table an owner or admin has opened for intake. It only ever appends: it cannot create, replace or delete a table, and a table that is not open refuses.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `table` | string | Yes | The table, as it was opened for intake. |
| `columns` | array of strings | No | Only when rows are lists: the column names in the order the values come. |
| `rows` | array of anys | Yes | The rows, each an object keyed by column name or a list in `columns` order. Send an empty list to be told the table's columns and types. Up to 10,000 rows per call. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

Send dates as `YYYY-MM-DD`, numbers as numbers and booleans as `true` or `false`, and leave a field null when the value is not known: the warehouse refuses a value a column cannot hold. If the same day is told twice, append a second row and let the workspace's model decide which counts.

This is the one write any member may grant, because its reach is only the tables an owner or admin has opened. Not offered to cherry.

```json
{
  "table": "daily log",
  "rows": [{ "day": "2026-09-30", "steps": 8200, "note": "Long walk" }]
}
```

### execute_sql

<p class="dx-chips"><span class="dx-chip">Run and commit SQL you wrote</span><span class="dx-chip is-danger">Destructive</span><a class="dx-chip is-scope" href="../reference/scopes#sql-write">sql:write</a></p>

Runs one SQL statement the agent wrote on a warehouse, as the warehouse's builder role, and commits it. The builder reads every landing table and writes only in the derived schema. This is the SQL shell held by a machine, and there is no undo.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `sql` | string | Yes | Exactly one PostgreSQL statement. Semicolons inside a string are fine and a second statement is refused. Qualify every relation with its schema (`raw.`, `derived.` or `mapping.`). |
| `why` | string | Yes | One sentence on what the statement is for. It is required and is kept in the audit trail beside the statement. |
| `mode` | string, one of `write`, `read` | No | `write` (the default) runs and commits the statement. `read` runs it inside a read-only transaction the database enforces, for looking before deciding. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |
| `rows` | integer | No | How many rows to return from a statement that returns rows. The default is 50 and the maximum is 500. |

A sync's landing tables, a published `mapping` view, the search indexes and sanda's own bookkeeping are out of reach whatever the statement says. A statement that changes rows answers with its command tag and the number of rows touched.

When the workspace asks for approval before an agent acts, a write-mode call is held for an owner or admin and does not run until one approves it. A `read` call is never held. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals). Not offered to cherry.

## Views and procedures

Define, build and tune views, materialized views and procedures in the derived schema of a warehouse.

### list_sources

<p class="dx-chips"><span class="dx-chip">List what a view may be built on</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Lists every relation in a warehouse that a view may be built on, with columns and types: landing tables, derived objects already defined, and the `mapping` tables. It is read as the role that would own the view, so it lists exactly what a `CREATE` will accept.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

A published `mapping.*` view is not listed and cannot be built on. Build on the table behind it.

### list_views

<p class="dx-chips"><span class="dx-chip">List derived views and procedures</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Lists every view, materialized view and procedure the workspace has defined in a warehouse's derived schema: its definition, its version, when a materialized view was last built and whether it is on the semantic map.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

### create_view

<p class="dx-chips"><span class="dx-chip">Define a view or materialized view</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Defines a view in the derived schema from one SELECT over anything `list_sources` names. A materialized view stores its result and is built now, on the workspace's compute. The view goes on the semantic map in the same call unless told not to.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | A lowercase identifier. It becomes `derived.<name>`. |
| `sql` | string | Yes | One SELECT (or WITH ... SELECT) over the relations `list_sources` names, by schema-qualified name. One statement, no semicolons, no comments. |
| `materialized` | boolean | No | When true, stores the rows and rebuilds them on refresh, instead of computing on every read. |
| `unique_key` | array of strings | No | Materialized views only: the columns that identify one row, so a refresh can run without blocking readers. |
| `description` | string | No | What the view is for, in a sentence. |
| `replace` | boolean | No | When true, redefines `derived.<name>` if it already exists. |
| `publish` | boolean | No | Whether to put the view on the semantic map. The default is true. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

A materialized view's build runs on the workspace's compute and is billed to it, which is why the scope that allows this is privileged. See [usage](https://docs.sanda-os.com.au/billing/usage).

### refresh_view

<p class="dx-chips"><span class="dx-chip">Rebuild a materialized view</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Rebuilds one materialized view now and reports how long it ran and what it cost. An assistant calls it after the tables behind the view have synced.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The materialized view's name, from `list_views`. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

Runs for up to 55 seconds inside the call. A build that needs longer, up to 14 minutes, belongs on a schedule.

### drop_view

<p class="dx-chips"><span class="dx-chip">Drop a derived object</span><span class="dx-chip is-danger">Destructive</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Drops one view, materialized view or procedure from the derived schema, taking it off the semantic map first. It is refused while another derived view reads it, because sanda never cascades. The definition stays in the history and the rows do not.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The object's name, from `list_views`. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

When the workspace asks for approval before an agent acts, this call is held for an owner or admin. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals). cherry always asks first, whatever its mode.

### create_procedure

<p class="dx-chips"><span class="dx-chip">Define a procedure</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Defines a procedure in the derived schema: a multi-step transform in plpgsql or SQL that builds, merges or reshapes derived tables, with `COMMIT` allowed between steps. It runs as the warehouse's builder role.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | A lowercase identifier. It becomes `derived.<name>`. |
| `body` | string | Yes | The procedure body: the plpgsql block, or the SQL statements. |
| `args` | string | No | The arguments as `name type, name type`, for example `since date, region text`. Empty for none. Types are `text`, `integer`, `bigint`, `numeric`, `boolean`, `date`, `timestamptz`, `timestamp`, `interval`, `jsonb` and `uuid`. |
| `language` | string, one of `plpgsql`, `sql` | No | `plpgsql` or `sql`. |
| `description` | string | No | What the procedure does, in a sentence. |
| `replace` | boolean | No | When true, redefines `derived.<name>` if it already exists. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

The builder role may read landing tables, `derived` objects and the `mapping` tables, and may create and write only in `derived`. It cannot write a row anywhere else, whatever the body says. Run it with `call_procedure`.

### call_procedure

<p class="dx-chips"><span class="dx-chip">Run a procedure</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Runs one procedure now, with its arguments in declared order, and reports how long it ran and what it cost.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The procedure's name, from `list_views`. |
| `args` | array of anys | No | Argument values in declared order. Leave it out for a procedure with none. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

Runs for up to 55 seconds inside the call. A run that needs longer belongs on a schedule.

### warehouse_advice

<p class="dx-chips"><span class="dx-chip">What is slowing this warehouse down</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Reads a warehouse's own statistics and says what is slowing it down or costing it storage: a missing index, a table autovacuum has fallen behind on, stale statistics after a sync, an unused index, storage a rewrite would give back, or a table large enough to partition. It writes nothing.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

Each finding carries an id, the figures behind it and the statement sanda would run. A finding with no statement is one sanda will not act on, and it says why. Pass the ids you want acted on to `optimise_warehouse`.

### optimise_warehouse

<p class="dx-chips"><span class="dx-chip">Apply what the advice found</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Runs the findings you name by id from `warehouse_advice`: a vacuum, an analyse, an index created or dropped. sanda re-reads the warehouse first and runs only findings that are still current. It never runs SQL you send it.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | array of strings | Yes | Finding ids from `warehouse_advice`, for example `vacuum:raw.xero__invoices`. Up to eight at a time. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

Each statement runs on the workspace's compute, is recorded on the run ledger and is billed as the seconds it takes. A finding that would lock a table against readers says so in the advice, so read it before sending its id.

## Schedules

Run views and procedures on a clock or after a sync, and read what each run did and cost.

### list_schedules

<p class="dx-chips"><span class="dx-chip">List schedules</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Lists every task graph in a warehouse: its steps, its cadence, whether it is paused or has suspended itself after failures, when it next runs and how its last run went.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

### create_schedule

<p class="dx-chips"><span class="dx-chip">Schedule a task graph</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Defines a task graph: an ordered list of steps on the warehouse, each a materialized view to refresh or a procedure to call, and when it runs. Steps run in order, and a step is skipped when the one before it failed.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | A name for the graph, for example `nightly rebuild`. |
| `tasks` | array of objects | Yes | The steps in order, from one to 20. Each is `{ "refresh": "view_name" }` or `{ "call": "procedure_name", "args": [ ... ] }`, with an optional step `name`. |
| `cron` | string | No | A five-field cron expression evaluated in UTC. sanda's scheduler runs on a 15 minute grid, so the minute field must be 0, 15, 30 or 45. Leave it out for a graph that runs only after a sync or by hand. |
| `timezone` | string | No | The zone the cron was written for, kept for display. Evaluation is always UTC. |
| `after_sync` | string | No | A connection, by name or id. The graph runs each time that connection lands rows, which is usually the better trigger. It may be combined with `cron`. |
| `suspend_after_failures` | integer | No | Pause the graph after this many failures in a row, from 1 to 100. The default is 3 and 0 never pauses. |
| `description` | string | No | What the graph is for, in a sentence. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

One run has at most 14 minutes, and every run is billed as the seconds its statements executed.

```json
{
  "name": "nightly rebuild",
  "tasks": [{ "refresh": "revenue_by_month" }, { "call": "close_month", "args": ["2026-09-01"] }],
  "cron": "0 18 * * 1-5"
}
```

That cron is weekdays at 18:00 UTC.

### run_now

<p class="dx-chips"><span class="dx-chip">Run a task graph now</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Queues one task graph to run now rather than waiting for its schedule. Because the scheduler runs on a 15 minute grid, it starts at the next quarter hour. A graph already queued or running is not queued again.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The graph's name, from `list_schedules`. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

### pause_schedule

<p class="dx-chips"><span class="dx-chip">Pause or resume a task graph</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Pauses a task graph so nothing fires it (not its schedule, not a sync landing, not `run_now`), or resumes one that is paused or has suspended itself. Resuming resets its failure count.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The graph's name, from `list_schedules`. |
| `resume` | boolean | No | `true` to resume. Leave it out or set it to `false` to pause. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

### run_history

<p class="dx-chips"><span class="dx-chip">Run history</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#warehouse-model">warehouse:model</a></p>

Shows what ran and what it cost: the recent runs of one task graph with each step's outcome and seconds, or the versions, builds and calls of one derived view or procedure.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `schedule` | string | No | A task graph, by name, to see its runs. |
| `view` | string | No | A derived object, by name, to see its versions and runs. Give one of `schedule` or `view`. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

## Manage sanda search

Create, change, build and remove search services over tables on the map, and estimate what indexing costs first.

### list_search_services

<p class="dx-chips"><span class="dx-chip">List search services</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Lists every sanda search service in a warehouse: the table and columns it indexes, how many rows and how large the index is, when it last built and what that cost. It also says whether sanda search is switched on for the workspace at all.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

The search tools are available on Standard and Enterprise. Not offered to cherry.

### describe_search_service

<p class="dx-chips"><span class="dx-chip">Describe a search service</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Describes one search service in full: its definition and state, every build with the rows it embedded and what it cost, and every version its definition has had. An assistant uses it to find out why a service is broken.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The service, by name. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

### estimate_search_build

<p class="dx-chips"><span class="dx-chip">Estimate what indexing would cost</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Estimates what indexing a table would cost before anything is created: rows, tokens, dollars and how that compares with the workspace's daily AI limit.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `source` | string | Yes | The table on the map, as `mapping.<table>`. |
| `text_columns` | array of strings | Yes | The columns whose text would be indexed. |
| `key_column` | string | No | The column that identifies a row. |
| `model` | string | No | The embedding model to use. Leave it out for sanda's default. |
| `chunk_chars` | integer | No | How much text one indexed piece holds. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

`create_search_service` refuses without `confirm: true` when a build would use more than half a day's AI limit. This is how to know that in advance.

### create_search_service

<p class="dx-chips"><span class="dx-chip">Create a search service</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Indexes the text of a table so it can be searched by meaning as well as by words. The first build sends the text of the columns named to an embedding model outside Australia and is charged against the workspace's daily AI limit.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | A lowercase name for the service. It becomes a table in the warehouse. |
| `source` | string | Yes | A table on the semantic map, as `mapping.<table>`. A view in the derived layer must be published to the map first. |
| `key_column` | string | No | The column that identifies a row. It must be unique and never empty, because a hit is only useful if it can be joined back to a metric. |
| `text_columns` | array of strings | Yes | The columns whose text is indexed. At least one. |
| `attribute_columns` | array of strings | No | Columns kept beside the text as filters, such as status, region or a date. They are not indexed as text. |
| `model` | string | No | The embedding model to use. Leave it out for sanda's default. |
| `chunk_chars` | integer | No | How much text one indexed piece holds. The default is 1200 characters. |
| `fts_config` | string, one of `simple`, `english` | No | Word matching: `simple`, or `english`, which stems words and drops common stop words. |
| `after_sync` | string | No | A connection, by name or id. The service rebuilds each time it lands rows, which is usually the right trigger. |
| `cron` | string | No | A five-field UTC cron on the quarter hour (minute 0, 15, 30 or 45), for a service rebuilt on a clock instead. With neither trigger the service is rebuilt when asked. |
| `timezone` | string | No | The zone the cron was written for, kept for display. Evaluation is always UTC. |
| `description` | string | No | What the service is for, in a sentence. |
| `confirm` | boolean | No | Set to true to go ahead when the estimated cost is a large share of the day's AI limit. |
| `replace` | boolean | No | Set to true to replace a service of the same name, rebuilding it from scratch. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

sanda search must be switched on by a workspace owner in the console before this works. An agent holding the scope cannot switch it on, because it is a decision about where a business's data may be processed. A column whose name says it holds a secret is refused outright. Not offered to cherry.

### update_search_service

<p class="dx-chips"><span class="dx-chip">Change a search service</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Redefines a search service or changes when it rebuilds. Changing the model, text columns, chunk size, word matching, key or source rebuilds the whole index at the cost of a first build. Changing only the attribute columns, description or cadence costs nothing.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The service to change, by name. |
| `source` | string | No | A new source table on the map. |
| `key_column` | string | No | A new key column. |
| `text_columns` | array of strings | No | The new list of columns to index. |
| `attribute_columns` | array of strings | No | The new list of filter columns. |
| `model` | string | No | A different embedding model. |
| `chunk_chars` | integer | No | A new chunk size. |
| `fts_config` | string, one of `simple`, `english` | No | `simple` or `english`. |
| `after_sync` | string | No | A connection, by name or id. Null clears it. |
| `cron` | string | No | A five-field UTC cron on the quarter hour. Null clears the schedule. |
| `timezone` | string | No | The zone the cron was written for, kept for display. |
| `description` | string | No | A new description. |
| `enabled` | boolean | No | `false` pauses rebuilding. Searches keep working on what is already indexed. |
| `suspend_after_failures` | integer | No | Pause rebuilding after this many failures in a row. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

A well-behaved assistant tells the person before it rebuilds a large table. Not offered to cherry.

### build_search_service

<p class="dx-chips"><span class="dx-chip">Index a search service now</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Queues an index build now instead of waiting for the service's own trigger. Only rows whose text changed since the last build are sent to the model, so it is cheap on an unchanged table.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The service, by name. |
| `full` | boolean | No | When true, discards the index and rebuilds every row. It costs what the first build cost. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

This only queues the build. sanda's scheduler runs on a 15 minute grid, and indexing then runs in slices across as many ticks as it takes. Use `run_search_build` to spend the call indexing now. Not offered to cherry.

### run_search_build

<p class="dx-chips"><span class="dx-chip">Index a search service now, inside this call</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Queues a build and then spends up to 55 seconds of the call indexing it, so a small index finishes inside the call. It reports what it read, embedded and cost.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The service, by name. |
| `full` | boolean | No | When true, rebuilds every row and not only what changed. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

An index too large for one call keeps its place and says there is more to do: call again to carry it on, or leave it to the scheduler. This builds an index and does not query one. Use `search_documents` for that. Not offered to cherry.

### drop_search_service

<p class="dx-chips"><span class="dx-chip">Remove a search service</span><span class="dx-chip is-danger">Destructive</span><a class="dx-chip is-scope" href="../reference/scopes#search-manage">search:manage</a><span class="dx-chip">Standard · Enterprise</span></p>

Removes a search service and deletes its index from the warehouse. Searches against it stop at once, and rebuilding it later costs a full first build. The definition and every build it ran stay on record.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The service to remove, by name. |
| `warehouse` | string | No | Which warehouse, by name or id, when the workspace has more than one. |

When the workspace asks for approval before an agent acts, this call is held for an owner or admin. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals). Not offered to cherry.

## Email reports

Schedule, change, pause, send and remove report deliveries.

### list_deliveries

<p class="dx-chips"><span class="dx-chip">List report deliveries</span><span class="dx-chip is-read">Read only</span><a class="dx-chip is-scope" href="../reference/scopes#reports-deliver">reports:deliver</a></p>

Lists every report delivery: the report it sends, its cadence, who it goes to, whether it is paused, when it next goes and how its last run went, with each delivery's id. It also names the reports that have no delivery.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `report` | string | No | Only this report's deliveries, by name or id. |

Reading deliveries needs the same scope as creating them, because the list is who receives what.

### create_delivery

<p class="dx-chips"><span class="dx-chip">Schedule a report delivery</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#reports-deliver">reports:deliver</a></p>

Emails a report on a schedule. At each firing sanda asks every block on the report again and sends the answers to the recipients. Email only: Slack and webhook deliveries are set up on the Report schedules page, because their address is a secret.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `report` | string | Yes | The report to send, by name or id. |
| `recipients` | array of strings | Yes | Email addresses, up to 25. |
| `cron` | string | Yes | A five-field cron on the quarter hour (minute 0, 15, 30 or 45), for example `0 8 * * 1` for Mondays at 8. |
| `timezone` | string | No | An IANA zone the cron is local to, for example `Australia/Sydney`. It stays on local time across daylight saving. Leave it out and the cron is UTC. |
| `note` | string | No | A closing note for the email, up to 500 characters. |

A workspace may limit which email domains a delivery can go to, and the answer says so when a recipient is outside them. To send a report once, create the delivery, send it with `send_delivery`, then pause it with `pause_delivery`.

When the workspace asks for approval before an agent acts, a call naming a recipient outside the domains its members use is held for an owner or admin, because a report emailed to someone cannot be recalled. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals). cherry always asks before an email, whatever its mode.

```json
{
  "report": "Board pack",
  "recipients": ["cfo@example.com.au"],
  "cron": "0 8 * * 1",
  "timezone": "Australia/Sydney"
}
```

### update_delivery

<p class="dx-chips"><span class="dx-chip">Change a report delivery</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#reports-deliver">reports:deliver</a></p>

Changes who a delivery goes to, its closing note or when it goes. `recipients` is the whole new list, so to add someone pass the current recipients with them, and to remove someone leave them out.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `report` | string | No | The report the delivery sends, by name or id. |
| `delivery` | string | No | The delivery's id, from `list_deliveries`, when the report has more than one. |
| `recipients` | array of strings | No | The whole new list of email addresses. |
| `cron` | string | No | A new five-field cron on the quarter hour. It stays on the delivery's own clock unless `timezone` names another. |
| `timezone` | string | No | The IANA zone a new cron is local to. Leave it out to keep the delivery's own. |
| `note` | string | No | A new closing note. An empty string removes it. |

Held for approval on the same terms as `create_delivery` when a recipient is outside the domains the workspace's members use.

### send_delivery

<p class="dx-chips"><span class="dx-chip">Send a report delivery now</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#reports-deliver">reports:deliver</a></p>

Sends a delivery now, to its recipients, instead of waiting for its schedule. It goes out within a minute or two.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `report` | string | No | The report the delivery sends, by name or id. |
| `delivery` | string | No | The delivery's id, when the report has more than one. |

A paused delivery can still be sent by hand. cherry always asks first, whatever its mode.

### pause_delivery

<p class="dx-chips"><span class="dx-chip">Pause or resume a report delivery</span><span class="dx-chip is-write">Writes</span><a class="dx-chip is-scope" href="../reference/scopes#reports-deliver">reports:deliver</a></p>

Pauses a delivery so its schedule stops sending it, or resumes a paused one. A resumed delivery next goes at its next firing from now.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `report` | string | No | The report the delivery sends, by name or id. |
| `delivery` | string | No | The delivery's id, when the report has more than one. |
| `resume` | boolean | No | `true` to resume. Leave it out or set it to `false` to pause. |

### delete_delivery

<p class="dx-chips"><span class="dx-chip">Remove a report delivery</span><span class="dx-chip is-danger">Destructive</span><a class="dx-chip is-scope" href="../reference/scopes#reports-deliver">reports:deliver</a></p>

Removes a delivery. The report stays, and the record of every run it made stays on the timeline. Only the schedule and its recipients go.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `report` | string | No | The report the delivery sends, by name or id. |
| `delivery` | string | No | The delivery's id, when the report has more than one. |

`pause_delivery` is the way to stop one for a while. cherry always asks first, whatever its mode.

## Approvals

Find out what happened to an action the workspace held for a person to approve.

### action_status

<p class="dx-chips"><span class="dx-chip">Check an action waiting for approval</span><span class="dx-chip is-read">Read only</span><span class="dx-chip is-scope">any held scope</span></p>

Reports what happened to an action the workspace held for approval: still waiting, declined, expired, or approved and what the tool answered when it ran. With no id it lists this credential's ten most recent actions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | No | The id the held call answered with. Leave it out to list recent actions. |

A credential sees only its own actions. The tool is listed only for credentials that hold a scope a held tool needs (`sql:write`, `warehouse:model`, `search:manage` or `reports:deliver`), because a credential that can only ask questions never has anything waiting. It is read-only, and it is never held itself.

A well-behaved assistant never tells a person a held action is done until this says it ran. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals).
