Content-Type: application/json
```
- **One address, POST only.** A GET answers 405 with `Allow: POST`.
- **Plain JSON-RPC 2.0.** Every message is one request and every answer is one JSON object. There is no streaming, no server-sent events and no session id, so a client needs nothing beyond an HTTP POST. A batch (a JSON array) is refused with `-32600`.
- **Request bodies up to 1,000,000 bytes.** That is enough for about ten thousand short rows in `load_rows`. A larger body is refused with 400 and `-32700`.
- **No browser origins.** A request that carries an `Origin` header other than sanda's own is refused with 403, so a web page on another site cannot call the endpoint.
- **A notification** (a request with no `id`) is answered with 202 and no body.
### Protocol versions
sanda speaks both eras of MCP on the same address, and a client using either works unchanged.
| Era | Revisions | How a client starts |
|---|---|---|
| Handshake | `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25` | `initialize`. sanda answers with the client's version when it supports it, and with `2025-11-25` otherwise |
| Stateless | `2026-07-28` | No handshake. Every request carries its protocol version and client capabilities in `params._meta`. `server/discover` replaces `initialize` |
For a `2026-07-28` request, the `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers are optional, but if sent they must agree with the body, or sanda answers 400 with `-32020`.
### Methods
`initialize`, `server/discover`, `ping`, `tools/list`, `tools/call`, `resources/list` and `resources/read`. sanda offers tools and two resources, `becca://fluid/brief` (the one-page map of the workspace) and `becca://fluid/context` (everything, in full, and large). It does not offer prompts or resource subscriptions. Prefer the brief plus tool calls to the full context. Both resources need `fluid:read`: without it, `resources/list` returns an empty list and `resources/read` is refused like a tool call, with a 403 and `insufficient_scope` for an OAuth client.
## Choose how to authenticate
| | OAuth | Integration token |
|---|---|---|
| Use it when | A person connects their own assistant, and the client can open a browser | A machine runs unattended, or the client has a header field and nothing else |
| The credential | An access token that lasts one hour, refreshed by the client | A `bat_` token that lasts until it expires or is revoked |
| Permissions | Chosen by the person on the consent screen | Chosen by an owner or admin when the token is created |
| Revoke it | **Settings · Integrations**, or `POST /api/oauth/revoke` | **Settings · Integrations** |
If both are on offer, use OAuth: there is no secret to store.
## Connect ChatGPT
ChatGPT can connect to a remote MCP server as a custom connector, on the plans that offer them. Add a connector, give it sanda's address, and choose OAuth as its authentication:
```text
https://console.sanda-os.com.au/api/mcp
```
If the form asks for a client ID or secret, leave them empty. sanda does not issue client secrets: every MCP client is a public client and proves itself with PKCE. When you connect, ChatGPT sends you to the same consent screen described in [Connect Claude](https://docs.sanda-os.com.au/mcp/connect-claude#the-consent-screen). Menu names in ChatGPT differ between plans and change over time, so check ChatGPT's own documentation for where the connector form is and whether your plan offers it.
## Sign in with OAuth from your own client
sanda implements the MCP authorization specification as both the resource server and the authorization server, on one address. All of it is public and needs no key.
| What | Address |
|---|---|
| Protected resource metadata | `https://console.sanda-os.com.au/.well-known/oauth-protected-resource` |
| Authorization server metadata | `https://console.sanda-os.com.au/.well-known/oauth-authorization-server` |
| Authorization endpoint | `https://console.sanda-os.com.au/api/oauth/authorize` |
| Token endpoint | `https://console.sanda-os.com.au/api/oauth/token` |
| Dynamic client registration | `https://console.sanda-os.com.au/api/oauth/register` |
| Revocation endpoint | `https://console.sanda-os.com.au/api/oauth/revoke` |
The discovery documents are served with `Access-Control-Allow-Origin: *` and cached for 60 seconds. The protected resource document is also served at `/.well-known/oauth-protected-resource/api/mcp`.
### Let the 401 point you at discovery
A request with no credential, or an invalid one, is refused with 401. Its `WWW-Authenticate` header names the discovery document and every scope sanda has:
```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="sanda",
resource_metadata="https://console.sanda-os.com.au/.well-known/oauth-protected-resource",
scope="fluid:read query:run sql:run fluid:write workspace:read workspace:manage warehouse:write warehouse:model search:manage sql:write warehouse:append reports:deliver"
```
When a credential was presented and refused, `error="invalid_token"` follows the realm.
### Read the metadata
```bash title="Discovery"
curl -s https://console.sanda-os.com.au/.well-known/oauth-protected-resource
curl -s https://console.sanda-os.com.au/.well-known/oauth-authorization-server
```
The first document is abridged here:
```json
{
"resource": "https://console.sanda-os.com.au/api/mcp",
"authorization_servers": ["https://console.sanda-os.com.au"],
"scopes_supported": ["fluid:read", "query:run", "sql:run", "fluid:write", "workspace:read", "workspace:manage", "warehouse:write", "warehouse:model", "search:manage", "sql:write", "warehouse:append", "reports:deliver"],
"bearer_methods_supported": ["header"]
}
```
The authorization server document tells a client what to do:
- `code_challenge_methods_supported` is `["S256"]`. PKCE with S256 is required, and `plain` is refused.
- `grant_types_supported` is `authorization_code` and `refresh_token`. There is no client credentials grant, because a person has to consent.
- `token_endpoint_auth_methods_supported` is `["none"]`. Every client is public.
- `client_id_metadata_document_supported` is `true`, and a `registration_endpoint` is also offered.
- `authorization_response_iss_parameter_supported` is `true`. sanda returns `iss` on every authorization response, success or error. Validate it.
### Identify your client
A client introduces itself in one of two ways. Use the first if you can.
**Client ID Metadata Document.** Host a JSON document at an HTTPS address that has a path, and use that address as your `client_id`. There is no registration step.
```json title="https://client.example.com/oauth/client.json"
{
"client_id": "https://client.example.com/oauth/client.json",
"client_name": "Example agent",
"redirect_uris": ["https://client.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
```
sanda fetches the document and holds it in a cache. The rules:
- `client_id` inside the document must equal the address it was fetched from, exactly.
- `client_name` is required, and so are one to 20 `redirect_uris`. Each must be `https`, or `http` on `localhost`, `127.0.0.1` or `[::1]`, and none may carry a fragment.
- The address must be a named host with a path. An IP address, `localhost` and internal suffixes such as `.local` or `.internal` are refused, as is a client ID with a user name or password in it.
- sanda does not follow redirects, waits five seconds, and reads at most 64 KB.
- The cache lasts as long as the response's `Cache-Control: max-age` says, held between five minutes and a day.
**Dynamic client registration.** Post your redirect addresses and take the client ID you are given:
```bash title="Register a client"
curl -s https://console.sanda-os.com.au/api/oauth/register \
-H 'content-type: application/json' \
-d '{"client_name":"Example agent","redirect_uris":["https://client.example.com/oauth/callback"]}'
```
The answer is 201 with a `client_id`, your `redirect_uris`, and `token_endpoint_auth_method` of `none`. Registration is open, because it grants no access on its own: a person still has to approve the client on the consent screen. Send one to 20 redirect addresses, each `https` or loopback `http`.
Redirect addresses are compared by **exact string**. A trailing slash, an added query parameter or a different port is a different address, and sanda stops before any redirect happens.
### Send the person to the consent screen
```text
https://console.sanda-os.com.au/api/oauth/authorize
?response_type=code
&client_id=https%3A%2F%2Fclient.example.com%2Foauth%2Fclient.json
&redirect_uri=https%3A%2F%2Fclient.example.com%2Foauth%2Fcallback
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&scope=fluid%3Aread%20query%3Arun
&resource=https%3A%2F%2Fconsole.sanda-os.com.au%2Fapi%2Fmcp
&state=abc123
```
| Parameter | Rule |
|---|---|
| `response_type` | Must be `code` |
| `code_challenge` | A PKCE S256 challenge of at least 43 characters. `code_challenge_method`, if sent, must be `S256` |
| `scope` | Space separated. sanda drops any scope it does not have. If none remain, the request is for `fluid:read` |
| `resource` | Optional, but if sent it must be `https://console.sanda-os.com.au/api/mcp`, or the request fails with `invalid_target`. Send it on the token request too |
| `state` | Returned to you unchanged |
An unknown client or an unregistered redirect address ends on an error page in the browser and never redirects. Any other error is sent to your redirect address as `error`, `error_description`, `state` and `iss`.
sanda sends the browser to its consent screen, where the person signs in if they must, picks a workspace, and chooses which of the scopes you asked for to grant. The screen is described in [Connect Claude](https://docs.sanda-os.com.au/mcp/connect-claude#the-consent-screen). A person can never grant a scope you did not ask for, and they can grant fewer.
If they approve, sanda redirects to your `redirect_uri` with `code`, `state` and `iss`. If they refuse, it redirects with `error=access_denied`. The consent request lasts 10 minutes, and the code lasts two minutes and works once.
### Exchange the code
```bash title="Token request"
curl -s https://console.sanda-os.com.au/api/oauth/token \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
-d client_id=https://client.example.com/oauth/client.json \
-d redirect_uri=https://client.example.com/oauth/callback \
-d code_verifier=YOUR_CODE_VERIFIER \
-d resource=https://console.sanda-os.com.au/api/mcp
```
The request is form encoded, and `code_verifier` is 43 to 128 characters. A code that is wrong, expired, already used or presented by a different client, and a verifier that does not match, all answer `invalid_grant` with no detail about which check failed. A code is spent the moment it is presented, so a mistake needs a new authorization.
```json
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "...",
"scope": "fluid:read query:run"
}
```
The `scope` is what the person granted, which may be less than you asked for.
### Refresh and revoke
An access token lasts one hour. Refresh it with the refresh token:
```bash title="Refresh"
curl -s https://console.sanda-os.com.au/api/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=YOUR_REFRESH_TOKEN
```
Refresh tokens **rotate on every use**, and each one lasts 30 days from when it was issued. Store the new refresh token each time. A token that has already been used is refused with `invalid_grant`.
An access token is bound to sanda's MCP address and is checked against it on every call. To clean up when a user disconnects, post the token to the revocation endpoint (RFC 7009). It answers 200 whether or not the token existed:
```bash title="Revoke"
curl -s https://console.sanda-os.com.au/api/oauth/revoke -d token=YOUR_TOKEN
```
## Use an integration token
An owner or admin [creates a token](https://docs.sanda-os.com.au/mcp/agent-tokens#create-a-token) and gives it to the client. The client sends it on every request as a bearer credential.
```bash title="Initialize"
curl -s https://console.sanda-os.com.au/api/mcp \
-H "Authorization: Bearer bat_your_token_here" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
```
```json title="Response, abridged"
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": { "listChanged": false },
"resources": { "listChanged": false, "subscribe": false }
},
"serverInfo": {
"name": "becca-semantic-fluid",
"title": "sanda semantic fluid",
"version": "1.6.0"
},
"instructions": "...",
"resultType": "complete"
}
}
```
`instructions` is a paragraph written for the model: which tool to call first and which to prefer. Now list the tools this token may use:
```bash title="List tools"
curl -s https://console.sanda-os.com.au/api/mcp \
-H "Authorization: Bearer bat_your_token_here" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```
A token created with no extra switches is offered five tools: `search_fluid`, `describe_table`, `run_query`, `search_documents` and `search_docs`. A tool the token may not use is absent from the list, and calling it anyway is refused. Each tool comes back with a `name`, a `title`, a `description` written for the model, an `inputSchema` and `annotations` that mark it read-only or destructive.
Call a tool with `tools/call`:
```bash title="Call a tool"
curl -s https://console.sanda-os.com.au/api/mcp \
-H "Authorization: Bearer bat_your_token_here" \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"run_query","arguments":{"metrics":["net revenue"],"dimensions":["region"],"limit":5}}}'
```
```json title="Response, abridged"
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [{ "type": "text", "text": "..." }],
"isError": false,
"resultType": "complete"
}
}
```
The result is text: for `run_query`, a table of rows followed by the SQL sanda composed. Use the names of metrics and dimensions that `search_fluid` returns for your workspace. The tools and their parameters are in the [MCP tool reference](https://docs.sanda-os.com.au/mcp/tools).
## Errors
A tool that could not answer returns a sentence and `isError: true`, for example that two tables would fan out. Refusals before a tool runs come back as HTTP status codes and JSON-RPC errors:
| Status | Code | Meaning |
|---|---|---|
| 401 | | No credential, or one that is invalid, revoked or expired. `WWW-Authenticate` points at discovery |
| 402 | | The workspace is suspended |
| 403 | | The request carried a foreign `Origin` |
| 403 | `-32602` | An OAuth client called a tool or read a resource it was not granted. `WWW-Authenticate` carries `error="insufficient_scope"` and the scope it needs, and the body names it as `data.required_scope`, so a client can ask the person to approve again |
| 405 | | Not a POST |
| 200 | `-32602` | An unknown tool or resource, or a tool or resource an integration token was not granted, which cannot step up |
| 200 | `-32601` | No such method. A `2026-07-28` request gets 404 |
| 400 | `-32600` | Not a JSON-RPC request, or a batch |
| 400 | `-32700` | The body was not valid JSON |
| 400 | `-32022` | Unsupported `2026-07-28` protocol version. `data.supported` lists what sanda speaks |
| 400 | `-32020` | A mirror header disagrees with the body |
A call that is waiting for a person to approve it is not an error. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals).
## A configuration file
Many clients read a small configuration file with the server's address and a header. The key names differ between clients, so check the client's own documentation, but the values are always these:
```json
{
"mcpServers": {
"sanda": {
"url": "https://console.sanda-os.com.au/api/mcp",
"headers": { "Authorization": "Bearer bat_your_token_here" }
}
}
}
```
## Next steps
:::links
- [Agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens): Create, scope, rotate and revoke the tokens a client holds.
- [Permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions): What each scope allows, and how actions wait for a person.
- [Scopes](https://docs.sanda-os.com.au/reference/scopes): The full list, with what each one allows.
:::
---
# Agent tokens
> Create, scope, rotate and revoke the integration tokens that Claude Code, agents and Excel use to reach your workspace, and keep them safe.
An agent token is a secret string that lets something outside sanda reach one workspace. The console calls it an **integration token**, and it always begins with `bat_`. You paste it into a client once, and the client sends it with every request.
Use a token when a person is not there to sign in: Claude Code, the Claude API, an agent harness of your own, or an Excel workbook that refreshes on a schedule. Where a client can sign a person in with OAuth, prefer that, because there is no secret to store. See [connect other MCP clients](https://docs.sanda-os.com.au/mcp/connect-other-clients).
## Where a token works
| Where | How it is sent | Editions |
|---|---|---|
| The MCP address, `https://console.sanda-os.com.au/api/mcp` | `Authorization: Bearer bat_...` | Every edition |
| The Excel and Power BI feed, `https://console.sanda-os.com.au/api/odata/` | As the password of a Basic credential, or as a bearer header | Every edition |
A token cannot sign in to the console. It can do what the MCP tools and the feed do, and nothing else.
You can create and revoke tokens on any edition. The sanda search tools answer only on an edition that includes sanda search.
## Create a token
Only an owner or admin can create tokens.
:::steps
1. **Open Settings · Integrations.** In the console, go to **Settings** and choose **Integrations**.
2. **Open the New token form.** Press **New token**.
3. **Name the token.** Under **Name**, say what will hold it, for example "Finance agent" or "Board pack workbook". A name is up to 80 characters. The name is how you will tell your tokens apart later.
4. **Tick only what it needs.** Every token can ask questions. Each switch beyond that is off until you turn it on. See the table below.
5. **Set an expiry.** Under **Expires**, choose **Never**, **30 days**, **90 days** or **One year**.
6. **Press Create token.** sanda shows the token once, with the feed address for Excel and Power BI and ready-made entries for Claude Code, the Claude API and curl.
7. **Copy it now.** sanda keeps only a hash and cannot show it again. If you lose it, create another.
:::
A workspace can hold 25 live tokens at once. Revoke one before creating another.
### The switches
Every token can ask questions of the fluid (`query:run`). The switches add to that. The scope is the permission's name in the tool reference and on the consent screen.
| Switch | Scope | What it allows |
|---|---|---|
| **May run its own SQL** | `sql:run` | A read-only `SELECT` of its own, through the same read-only role, when the fluid cannot express a question |
| **May run and commit its own SQL** | `sql:write` | One statement at a time as the warehouse's builder role, committed at once. Reads every landing table and writes only the derived schema. No undo |
| **May write the fluid** | `fluid:write` | Put tables on the map, define metrics, dimensions, filters and relationships, and accept or remove proposals. Live at once |
| **May see connections and warehouses** | `workspace:read` | What is connected, when it last synced, how full each warehouse is. Never a credential |
| **May start a sync** | `workspace:manage` | Run a connection now instead of on its schedule |
| **May load rows into the warehouse** | `warehouse:write` | Land tables of its own rows as `raw.csv__...`. It cannot touch a table a sync landed |
| **May define views and procedures** | `warehouse:model` | Build views, materialized views and procedures in the derived schema, run them and put views on the map, on your compute |
| **May add rows to intake tables** | `warehouse:append` | Append rows to tables an owner or admin has opened for intake, and nothing else |
| **May manage search services** | `search:manage` | Create, rebuild and remove sanda search services |
| **May email reports** | `reports:deliver` | Schedule report deliveries, change or pause them and send one now |
The full effect of each, including which tools it allows, is in [scopes](https://docs.sanda-os.com.au/reference/scopes). A switch that reaches beyond asking questions is a decision worth making on purpose. Leave every switch off unless the client's job needs it.
## What decides what a token can do
The switches are the ceiling. Four other things can lower it:
- **Who made it.** The privileged scopes (everything above except **May see connections and warehouses** and **May add rows to intake tables**) last only as long as the person who made the token is still an owner or admin. If they are demoted, sanda stops honouring those switches and the token's row is marked "permissions inactive". Everything else it holds still works.
- **Whether that person is still here.** A token stops working when the person who created it leaves the workspace. Its row is marked "maker left" until someone clears it away.
- **The workspace's state.** When a workspace is closed it becomes read-only, and every token is limited to asking questions and seeing connections.
- **The edition.** The search tools need Standard or Enterprise.
## See your tokens
**Settings · Integrations** lists every live token, and the apps members have connected by approving sanda's consent screen, in one list. Any member can see every token. Owners and admins also see every member's connected apps, and everyone else sees their own. Each row shows:
- the token's name and its last six characters, which is enough to match it against the string in a client's configuration and not enough to use it;
- who made it, when, when it was last used ("never used" if it has not been), and when it expires;
- one label for each thing it may do beyond questions.
| Label | Scope |
|---|---|
| fluid only | Nothing beyond asking questions |
| may run SQL | `sql:run` |
| commits SQL | `sql:write` |
| writes the fluid | `fluid:write` |
| sees connections | `workspace:read` |
| starts syncs | `workspace:manage` |
| loads rows | `warehouse:write` |
| appends rows | `warehouse:append` |
| defines views | `warehouse:model` |
| manages search | `search:manage` |
| emails reports | `reports:deliver` |
To see what a token has been doing, open **Intelligence · Agents**. It shows each credential's permissions in full sentences, and the calls it has made. See [MCP and agents](https://docs.sanda-os.com.au/mcp).
## Rotate a token
A token cannot be changed in place, and sanda cannot show you an old one. To rotate:
:::steps
1. **Create a new token** with the same name (add a date, such as "Finance agent, October") and the same switches.
2. **Put it in every place that used the old one.** For an Excel workbook, follow [change or clear the saved token](https://docs.sanda-os.com.au/odata/excel#change-or-clear-the-saved-token).
3. **Check that the new token is in use.** On **Intelligence · Agents**, or in the token list, the new one shows a "last used" time once a client has called with it.
4. **Revoke the old token.**
:::
Setting an expiry when you create a token turns rotation into a habit. A token that expires in 90 days needs replacing every 90 days, and a token nobody remembers stops working on its own.
## Revoke a token
Press **Revoke** on the token's row. Owners and admins can revoke any token. Anyone can revoke a token they made themselves.
Revoking takes effect at once: the next call the token makes fails with a 401. The token disappears from the list, and sanda keeps a record of its creation and revocation in the audit trail. There is no undo, and no way to bring the same token back. Create a new one instead.
A connected app, such as Claude, is disconnected from the same list with **Disconnect**.
## Keep tokens safe
- **Treat a token like a password.** Anyone who holds it can do whatever its switches allow, as your workspace, until it is revoked.
- **One token for each client.** When something goes wrong you can revoke that one and leave the others working. Name it after what holds it.
- **Grant the least.** A workbook needs no switches at all. A reporting agent rarely needs to write the fluid.
- **Set an expiry.** Choose 30 or 90 days for anything you would have to remember to review.
- **Keep it out of chat, tickets and source control.** The `bat_` prefix is there so that secret scanners recognise a leaked token, and it is worth switching scanning on for your repositories.
- **Keep it out of URLs.** Send it in a header, or as a Basic password in Excel. A URL is copied into logs and browser history.
- **If one leaks, revoke it first.** Then open **Intelligence · Agents** and read the **Calls** tab for what it did, and create a replacement.
- **Ask for approval on the risky actions.** An owner or admin can make committed SQL, dropped views, dropped search services and emails to outside addresses wait for a person, even for a token that holds the permission. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions#approvals).
## Next steps
:::links
- [Permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions): Decide what an agent may do, and who has to agree first.
- [Connect Excel](https://docs.sanda-os.com.au/odata/excel): Use a token as the password of a Basic credential.
- [Scopes](https://docs.sanda-os.com.au/reference/scopes): What every switch allows and which tools it gives.
:::
---
# Permissions and approvals
> What each MCP permission allows, who can grant it, how to make risky agent actions wait for a person, and where every action is recorded.
An assistant connected to sanda can do only what its credential's permissions allow. Permissions are called **scopes**. sanda checks them on every call, a person grants them on purpose, and the few actions that cannot be undone can be made to wait for an owner or admin before they run.
## Scopes
A credential is an [integration token](https://docs.sanda-os.com.au/mcp/agent-tokens) or a connected app such as Claude. Each holds a list of scopes. Asking questions of the semantic fluid is the baseline, and every other scope adds one kind of action.
A tool the credential may not use is **absent from the tool list**, not present and refusing, so an assistant never tries what it cannot do.
There are twelve scopes, in three kinds:
| Kind | Scopes | Who can grant |
|---|---|---|
| Reading | `fluid:read`, `query:run`, `workspace:read` | Any member |
| Adding rows | `warehouse:append` | Any member |
| Privileged | `sql:run`, `sql:write`, `fluid:write`, `workspace:manage`, `warehouse:write`, `warehouse:model`, `search:manage`, `reports:deliver` | An owner or admin only |
`warehouse:append` is the one write any member can grant, and it is safe for a reason: it reaches only the tables an owner or admin has opened for intake, and it can only add rows to them. It cannot create, replace or delete anything.
A wider scope includes a narrower one. `sql:run` includes `query:run`, which includes `fluid:read`. `sql:write` includes `sql:run` and `workspace:read`. `fluid:write` includes `fluid:read`. `workspace:manage`, `warehouse:write`, `warehouse:model` and `search:manage` each include `workspace:read`. `warehouse:append` includes `fluid:read`. `reports:deliver` includes nothing else, because sending a report is not asking its questions. The full list, with every tool each one allows, is in [scopes](https://docs.sanda-os.com.au/reference/scopes).
## Who can grant what
| Where the permission is granted | Who | The rule |
|---|---|---|
| The consent screen, for a connected app | Any member of the workspace except a reader | A person can grant only the scopes the app asked for, and fewer if they untick some. A privileged scope is greyed out unless they are an owner or admin |
| **Settings · Integrations**, for an integration token | An owner or admin | They can turn on any switch |
The privileged scopes and `warehouse:append` start **unticked** on the consent screen. A client that asks for everything is shown everything, and the person opts in to each one that writes. sanda enforces the rule on its own side as well as on the screen: a scope a person's role may not grant is dropped from the grant even if the request names it.
A person with the reader role cannot connect an assistant at all.
### Privileged scopes follow the person
A privileged scope lasts only as long as the person who granted it is still an owner or admin. sanda reads their current role on every call. If they are moved to member or viewer, the privileged scopes on the tokens and connected apps they made stop working, the rest keep working, and **Settings · Integrations** marks the row "permissions inactive". A credential also stops working entirely when the person behind it leaves the workspace.
A closed workspace is read-only for everyone. Every credential keeps only the reading it already had: asking questions and seeing connections if it could do those before. A credential that could only add rows to intake tables or send reports can no longer do either, and closing a workspace never lets a credential read more than it did.
## Approvals
A workspace can ask for a person to approve the few things an agent can do that sanda cannot undo. It is **off by default**, so a client that worked before keeps working exactly as it did until an owner or admin turns approvals on.
### Turn approvals on
:::steps
1. **Open Intelligence · Agents.** Only an owner or admin sees the **Approvals** panel.
2. **Press Ask before an agent acts.** The panel now shows how many actions are waiting.
3. **To stop asking, press Stop asking.** Anything already waiting stays in place, to be approved or declined. New calls run straight through.
:::
The switch applies to every integration token and every connected app in the workspace. It does not apply to people using the console: a person who drops a view in the explorer is never asked to approve themselves.
### What waits
| Call | Held when |
|---|---|
| `execute_sql` | It runs in `write` mode, which is the default. `read` mode is never held, because looking before deciding is what it is for |
| `drop_view` | Always, because it drops a view, materialized view or procedure and its rows go with it |
| `drop_search_service` | Always, because it deletes the index and rebuilding costs a full first build |
| `create_delivery` and `update_delivery` | A recipient's email domain is not one your members use. A report emailed to someone cannot be recalled |
Everything else an agent can do runs as normal. Approval is a brake for the handful of acts that cannot be undone, not a second consent screen for every write. If you want a person in front of every definition, leave `fluid:write` off, and the tools that write the fluid are not offered at all.
### What the agent sees
The tool answers with an ordinary result, not an error, that says it has **not run**, why it is waiting, and an id. The agent is told to call `action_status` with that id, and never to report the action as done until `action_status` says it ran. A result rather than an error is deliberate: a client that reads an error as "try another way" is exactly the one that must not go looking for another way.
- A call sanda could not have run anyway (a missing `why`, a second statement, an unknown warehouse) is refused in the tool's own words and never queued.
- Sending the identical call again while it waits returns the same id. It does not queue a second one or email anyone again.
- A workspace holds at most 50 waiting actions. Beyond that, calls are refused until some are decided.
`action_status` is available to a credential that holds a scope a held tool needs (`sql:write`, `warehouse:model`, `search:manage` or `reports:deliver`). An agent sees its own credential's actions and nobody else's. Called with no id, it lists the ten most recent.
### Decide
sanda emails the workspace's owners and admins when an action is held, since either can decide it. The email names the credential, the tool, why it is held and what would run. It is not sent when **Send alert emails for this workspace** is switched off in **Settings · Plan & budget**, and the action still waits on the Agents page. The extra addresses under **Also send to** are not emailed, because they cannot approve anything.
Owners and admins can decide. On **Intelligence · Agents**, each waiting action shows:
- the tool, whether the credential is an integration token or a connected app, its name, and who it acts for;
- when it was asked and when it expires;
- why it is held, and the agent's own stated reason where the tool takes one;
- everything that would run: the whole statement for SQL, the object's name and warehouse for a drop, or the full input otherwise.
Read all of it, then press **Approve and run** or **Decline**. A member does not see the queue.
### What happens when you approve
- The action is claimed in a single step, so it runs **at most once** however many people press the button at the same moment.
- sanda checks the credential again. If it has been revoked, has expired, acts for someone who has left the workspace or is no longer an owner or admin, or no longer holds the scope the tool needs, or if the workspace is suspended or closed, the action does **not** run, and the reason is recorded.
- Otherwise the stored input runs through the same handler the original call would have reached, **as that credential**. The audit record names the credential and the person it acts for, exactly as it would have without approval.
- What the tool answered is recorded, and it is what `action_status` returns.
The panel shows each outcome as a label:
| Label | Meaning |
|---|---|
| ran | It was approved and the tool answered |
| ran · answered no | It was approved and ran, and the tool answered no, for example because a view did not exist |
| not run · credential changed | It was approved, but the credential could no longer act |
| declined | An owner or admin declined it, and it did not run |
| expired | No one decided within 24 hours. It never runs |
| running | It was approved and is running now |
| no outcome recorded | It started and sanda never recorded how it ended, so check the warehouse before asking again |
Decided actions stay in **Decided in the last 7 days**, with the outcome.
Anything not decided within 24 hours expires and never runs. If it is still needed, the agent asks again and it waits again.
### cherry has its own rules
cherry, sanda's agent in the console, is not MCP and does not use this queue. It asks before actions of its own, on its own terms. See [cherry actions and approvals](https://docs.sanda-os.com.au/cherry/actions-and-approvals).
## What is recorded
- **Every tool call and resource read** is recorded with the credential, the tool, when it happened, how long it took and whether it answered no. **Intelligence · Agents** shows them on the **Calls** tab, and groups them into sessions. The record is of the call and how it went, not the rows that came back.
- **Every refusal** is recorded separately. **Refused** lists calls sanda declined to run at all: an unknown tool, a scope the credential does not hold, a protocol the client got wrong.
- **Every change** an agent makes goes through the same handler the console's button runs, with the same validation, so it lands in the audit trail as a console change does. Statements run with `execute_sql` are kept with the statement and the reason the agent gave.
- **Every step of an approval** is recorded: held, approved, declined, run, refused.
- **Creating and revoking** a token, and **granting** an app, are recorded with who did them.
## Choosing permissions
Start with nothing beyond asking questions and add only what a job needs.
| The job | Grant | Notes |
|---|---|---|
| An assistant that answers questions | Nothing beyond the defaults | Sees the fluid, runs composed queries and searches text |
| Also says how fresh the data is | `workspace:read` | Lists connections and warehouses. Never a credential |
| Also refreshes data before it asks | `workspace:manage` | Starts a sync, which costs compute like any other sync |
| An agent that models the fluid | `fluid:write` | Definitions go live at once, and approvals do not hold them. Keep `fluid:write` off if you want a person to make every change |
| A person logging their day by talking to an assistant | `warehouse:append` only | On a table an owner or admin has opened for intake. Nothing else is reachable |
| A weekly board pack by email | `reports:deliver` | Turn approvals on, so a recipient outside the email domains your members use waits for an owner |
| An agent that builds and schedules views | `warehouse:model` | Every build runs on your compute and shows on your bill. See [usage](https://docs.sanda-os.com.au/billing/usage) |
| An agent that can run its own SQL | `sql:run` first | Read-only. Move to `sql:write` only with approvals on, because it commits with no undo |
## Next steps
:::links
- [Scopes](https://docs.sanda-os.com.au/reference/scopes): Every scope, what it includes and which tools it allows.
- [Agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens): Create and revoke the credentials these permissions belong to.
- [MCP tool reference](https://docs.sanda-os.com.au/mcp/tools): The tools, their parameters and which scope each needs.
:::
---
# 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
Search the fluidRead onlyfluid:read
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
Describe a tableRead onlyfluid:read
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
Run a composed queryRead onlyquery:run
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
Search the text inside the rowsRead onlyquery:runStandard · Enterprise
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
Run SQL you wroteRead onlysql:run
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
Search the sanda docsRead onlyfluid:read
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
List connectionsRead onlyworkspace:read
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
One connection, in detailRead onlyworkspace:read
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
List warehousesRead onlyworkspace:read
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
Start a syncWritesworkspace:manage
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
List tables in the warehouseRead onlyfluid:write
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
Put warehouse tables on the mapWritesfluid:write
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
Mark a table as a fact or a dimensionWritesfluid:write
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
Take tables off the mapDestructivefluid:write
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
Define something in the fluidWritesfluid:write
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
List proposals awaiting reviewRead onlyfluid:write
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
Accept or reject a proposalWritesfluid:write
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
Delete a definitionDestructivefluid:write
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
Load rows into the warehouseWriteswarehouse:write
Lands a table of an agent's own rows in the warehouse as `raw.csv__`, 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__`, 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
Open or close a table for intakeWriteswarehouse:write
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
Add rows to an intake tableWriteswarehouse:append
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
Run and commit SQL you wroteDestructivesql:write
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
List what a view may be built onRead onlywarehouse:model
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
List derived views and proceduresRead onlywarehouse:model
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
Define a view or materialized viewWriteswarehouse:model
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.`. |
| `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.` 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
Rebuild a materialized viewWriteswarehouse:model
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
Drop a derived objectDestructivewarehouse:model
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
Define a procedureWriteswarehouse:model
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.`. |
| `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.` 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
Run a procedureWriteswarehouse:model
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
What is slowing this warehouse downRead onlywarehouse:model
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
Apply what the advice foundWriteswarehouse:model
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
List schedulesRead onlywarehouse:model
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
Schedule a task graphWriteswarehouse:model
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
Run a task graph nowWriteswarehouse:model
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
Pause or resume a task graphWriteswarehouse:model
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
Run historyRead onlywarehouse:model
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
List search servicesRead onlysearch:manageStandard · Enterprise
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
Describe a search serviceRead onlysearch:manageStandard · Enterprise
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
Estimate what indexing would costRead onlysearch:manageStandard · Enterprise
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.`. |
| `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
Create a search serviceWritessearch:manageStandard · Enterprise
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.`. 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
Change a search serviceWritessearch:manageStandard · Enterprise
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
Index a search service nowWritessearch:manageStandard · Enterprise
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
Index a search service now, inside this callWritessearch:manageStandard · Enterprise
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
Remove a search serviceDestructivesearch:manageStandard · Enterprise
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
List report deliveriesRead onlyreports:deliver
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
Schedule a report deliveryWritesreports:deliver
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
Change a report deliveryWritesreports:deliver
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
Send a report delivery nowWritesreports:deliver
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
Pause or resume a report deliveryWritesreports:deliver
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
Remove a report deliveryDestructivereports:deliver
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
Check an action waiting for approvalRead onlyany held scope
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).
---
# Excel and Power BI (OData)
> Open your semantic fluid in Excel or Power BI as live, refreshable tables. Nothing to install: paste one address and a token.
Your semantic fluid is also a live data feed that Excel, Power BI and Power Query read without an add-in, a driver or a database connection. You paste an address and a token, choose the tables you want, and they arrive as refreshable tables in the words your business agreed on, not in the warehouse's column names.
The feed uses OData, an open standard that Excel and Power BI already understand. It is on every edition.
```text
https://console.sanda-os.com.au/api/odata/
```
The trailing slash matters to some clients.
## How it works
:::steps
1. **Create an integration token.** An owner or admin creates one under **Settings · Integrations**. The token needs no switches: asking questions is all a feed does. See [agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens).
2. **Paste the address into Excel or Power BI.** Both have an OData feed connector. See [Connect Excel](https://docs.sanda-os.com.au/odata/excel) and [Connect Power BI](https://docs.sanda-os.com.au/odata/power-bi).
3. **Sign in with the token.** Excel takes it as the password of a Basic credential.
4. **Choose tables.** Load them, and refresh whenever you like.
:::
Nothing is installed, and the feed stores no copy of your data. Each refresh reads your warehouse live.
## What the feed offers
For each table on your semantic map, the feed offers up to two tables:
| Table | What it holds | When it exists |
|---|---|---|
| `invoices` | The rows. Every column the fluid can see, named the way your business named it | Every table on the map |
| `invoices_summary` | That table's metrics, grouped by its dimensions | Only where the table has a metric |
For example, a workspace with an `invoices` table, a `region` dimension, an `issue date` dimension and a `net revenue` metric offers `invoices` with the columns `issue_date`, `region` and the rest, and `invoices_summary` with `region`, `issue_date` and `net_revenue`. The names here are examples. Yours come from your own fluid.
**Use the summary tables for numbers.** `net revenue` in a summary is the definition your business signed off, composed by the same engine that answers it in sanda's own workbench. It is not a `SUM` somebody wrote in a cell and nobody reviewed. Use the rows table when you need the detail.
A few things to know about the tables:
- **Names are folded into identifiers a URL can carry.** `order details` is `order_details`, and `on-time delivery %` is `on_time_delivery`. Two names that fold to the same identifier both stay reachable, and the second gets a numeric suffix.
- **A column with a dimension takes the dimension's name.** Your header row reads `region` and `issue date`, not `cust_ctry_cd` and `issued_at`.
- **Only what is on the map.** A table that is not on the semantic map is not in the feed. A view you build under **Data · Modelling** and put on the map appears as a table like any other. See [views](https://docs.sanda-os.com.au/modelling/views).
- **Every row has a `_row` column.** It is the row's position in that read, and OData needs every row to have a key. It is not a business key and it does not survive a refresh. Ignore it, or remove the column in Power Query.
If your map is empty, the feed has no tables. Import a table under **Intelligence · Semantic fluid** and it appears.
## Signing in
An integration token is the only credential a feed needs, and it needs no special switches.
- **Excel:** choose **Basic**, and paste the token as the password. The user name is ignored.
- **Power BI:** the same Basic credential.
- **A script:** send `Authorization: Bearer bat_...`.
The token identifies the workspace, not the person holding it. An owner or admin creates it and can revoke it at any time under **Settings · Integrations**. Details are in the [feed reference](https://docs.sanda-os.com.au/odata/reference#authentication).
## Refreshing and what it costs
Every refresh is a live read of your warehouse, through the same read-only role that answers questions. It is billed to your workspace like any other query, and it is subject to the same compute budget. A workbook set to refresh every five minutes is a query every five minutes, indefinitely, so choose a schedule somebody will actually look at. See [usage](https://docs.sanda-os.com.au/billing/usage).
A page is 1,000 rows. When a table has more, sanda hands the client a link to the next page, and Excel and Power BI follow it without being asked, so a 40,000-row table simply takes a moment.
## What the feed does not do
The feed is read only and deliberately small.
- **It cannot write.** Anything other than a read is refused.
- **`or` and `not` in a filter are refused.** sanda combines conditions with `and` only, and says so when a filter asks for more.
- **`$count`, `$expand`, `$apply` and `$search` are refused by name.** A filter that was silently ignored would hand you a bigger number than you asked for, so nothing is dropped quietly.
- **A metric cannot be filtered on.** It is worked out from the rows a query reads. Filter on a dimension.
- **It pages up to 1,000,000 rows into a table and no further.** After that, narrow the read with a filter, or build a view.
The [feed reference](https://docs.sanda-os.com.au/odata/reference) has the details.
## Next steps
:::links
- [Connect Excel](https://docs.sanda-os.com.au/odata/excel): Data, Get Data, From OData Feed, step by step.
- [Connect Power BI](https://docs.sanda-os.com.au/odata/power-bi): Power BI Desktop, a Power Query snippet and scheduled refresh in the service.
- [OData feed reference](https://docs.sanda-os.com.au/odata/reference): Addresses, tables, query options, paging, limits and errors.
:::
---
# Connect Excel
> Load your semantic fluid into Excel as live tables with Data, Get Data, From OData Feed. Use a Basic sign-in with an integration token, then refresh.
Excel reads sanda's feed with its built-in OData connector. You paste one address and a token, tick the tables you want and load them. Refreshing re-reads your warehouse.
The menu names below are from Excel for Microsoft 365 on Windows and may differ in other versions. The address and the token are the same everywhere.
## Before you start
- **An integration token.** An owner or admin creates one under **Settings · Integrations**. A feed needs none of the optional switches. Copy the token when it is shown, because sanda cannot show it again. If you are not an owner or admin, ask one to create it for you. See [agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens#create-a-token).
- **Something on the map.** The feed offers the tables on your [semantic fluid](https://docs.sanda-os.com.au/semantic-fluid). If the map is empty, there is nothing to load.
## Connect
:::steps
1. **Open the OData feed connector.** In Excel, go to **Data · Get Data · From Other Sources · From OData Feed**.
2. **Paste sanda's address.** Paste this exactly, including the trailing slash, and press **OK**:
```text
https://console.sanda-os.com.au/api/odata/
```
3. **Choose Basic.** Excel asks how to sign in. Choose **Basic**.
4. **Enter the token.** Paste the token into **Password**. sanda ignores the user name, so leave it empty. If your version of Excel insists on a value, type any text. If Excel asks which level to apply the sign-in to, leave it on the sanda address. Press **Connect**.
5. **Choose tables.** The Navigator lists everything on your map. Each table appears twice where it has metrics: its rows (`invoices`) and its summary (`invoices_summary`). Tick one and press **Load**, or press **Transform Data** to shape it first.
:::
The table lands on a worksheet as an Excel table. Numbers arrive as numbers and dates as dates, because Excel reads the column types from the feed.
### Which table to pick
- **The summary** (`..._summary`) holds your metrics grouped by your dimensions. It is the one to use for numbers: `net revenue` there is the definition your business agreed on, and it is small and quick to refresh.
- **The rows** hold every column of the table. Use them for detail, and narrow them first (below), because a large table takes a while.
Ignore the `_row` column. It is sanda's row number for that read, and it is not stable from one refresh to the next.
## Narrow what you load
Choosing fewer columns and fewer rows makes a refresh cheaper as well as smaller, because sanda does the narrowing in the warehouse and not after the rows arrive.
In the Power Query editor (**Transform Data**), remove the columns you do not need and filter the rows. Power Query sends what it can to sanda as `$select` and `$filter`. A few rules follow from what sanda accepts:
- **Filters combine with "and".** A filter that needs "or" or "not" comes back as an error. Split it into separate steps, or filter after loading.
- **Filter dates with "on or after" and "before".** sanda compares dates as a window that includes its first day and excludes its end, so a month filter never loses its last day. A comparison that means "after" or "on or before" is refused with a message saying which to use.
- **Filter on a dimension, not a metric.** A metric is worked out from the rows a query reads, so it cannot decide which rows are read.
The complete list of what sanda accepts is in the [feed reference](https://docs.sanda-os.com.au/odata/reference#query-options).
## Refresh
- **Once:** choose **Data · Refresh All**.
- **On a schedule:** choose **Data · Queries & Connections**, right-click the query, choose **Properties**, and set **Refresh every** and **Refresh data when opening the file**.
Every refresh is a live read of your warehouse. It is billed to your workspace like any other query, so a workbook that refreshes every five minutes is a query every five minutes, indefinitely. Pick a schedule somebody will actually look at. See [usage](https://docs.sanda-os.com.au/billing/usage).
When a table has more than 1,000 rows, Excel follows sanda's links to the next pages on its own. A 40,000-row table takes a little longer, and nothing more is needed. sanda pages up to 1,000,000 rows into a table. Past that, filter the table or build a view under **Data · Modelling**. See [views](https://docs.sanda-os.com.au/modelling/views).
## Change or clear the saved token
Excel keeps the credential in its data source settings on your computer. To swap in a new token after you [rotate one](https://docs.sanda-os.com.au/mcp/agent-tokens#rotate-a-token), or to be asked again:
:::steps
1. **Open the settings.** Go to **Data · Get Data · Data Source Settings**.
2. **Choose the sanda address** in the list.
3. **Edit or clear.** Press **Edit Permissions** and then **Edit** beside the credentials to enter a new token, or press **Clear Permissions** and Excel will ask for a credential again on the next refresh.
:::
When a token is revoked or expires, the next refresh fails with a sign-in error. sanda answers a missing or wrong token with a 401.
## If something goes wrong
Excel shows sanda's own message when it can. These are the common ones.
| What Excel shows | What it means | What to do |
|---|---|---|
| A sign-in dialog or a credentials error | The token is wrong, revoked or expired | Create a new token and [update the saved one](https://docs.sanda-os.com.au/odata/excel#change-or-clear-the-saved-token) |
| "That token cannot run queries, so it cannot read a feed" | The token lacks the permission to ask questions, which every token normally has | Create a new token |
| "This workspace has nothing on its semantic map yet" | No tables are on the map | Import a table under **Intelligence · Semantic fluid** |
| "This feed has no table called ..." | The name is not one sanda offers | Open the Navigator and choose from the list. The message names the tables it has |
| "sanda can only combine filters with “and”" | A filter used “or” | Rewrite it without “or” |
| "... looks like it holds a secret, so sanda won't select it" | The table has a column whose name looks like a password or key, and sanda never reads those | Rename the column if the name is wrong, or build a view that leaves it out |
| "Reading ... took longer than sanda will wait" | The read ran past 30 seconds | Filter it, or remove columns |
| "sanda could not read ... just now" | A temporary fault reading the warehouse. Nothing was lost | Refresh again |
| "This workspace is suspended" | The workspace is suspended, for example because a trial ended | An owner can check **Settings · Plan & budget**, or ask sanda support |
More help is in [troubleshooting](https://docs.sanda-os.com.au/help/troubleshooting).
## Next steps
:::links
- [Connect Power BI](https://docs.sanda-os.com.au/odata/power-bi): The same feed in Power BI Desktop and the Power BI service.
- [OData feed reference](https://docs.sanda-os.com.au/odata/reference): Every table, option, limit and error.
- [Agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens): Create, rotate and revoke the token your workbook holds.
:::
---
# Connect Power BI
> Read your semantic fluid in Power BI Desktop with the OData feed connector, shape it in Power Query, and keep it fresh with scheduled refresh in the service.
Power BI reads sanda's feed with its built-in OData connector, the same one Excel uses. You connect once in Power BI Desktop with a token, and after you publish, the Power BI service refreshes the dataset on a schedule.
## Before you start
- **An integration token.** An owner or admin creates one under **Settings · Integrations**. A feed needs none of the optional switches. Copy it when it is shown, because sanda cannot show it again. See [agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens#create-a-token).
- **Something on the map.** The feed offers the tables on your [semantic fluid](https://docs.sanda-os.com.au/semantic-fluid).
The feed is on every edition.
## Connect in Power BI Desktop
:::steps
1. **Open the OData feed connector.** On the **Home** ribbon, choose **Get data**, then **OData feed**. If it is not in the short list, choose **More**, search for OData, and select **OData feed**.
2. **Paste sanda's address.** Paste it exactly, including the trailing slash, and press **OK**:
```text
https://console.sanda-os.com.au/api/odata/
```
3. **Choose Basic.** When Power BI asks how to sign in, choose **Basic**. Paste the token into **Password**. sanda ignores the user name, so leave it empty, or type any text if Power BI insists on a value.
4. **Apply it to the sanda address.** If Power BI asks which level to apply the sign-in to, leave it on the address you typed. Press **Connect**.
5. **Choose tables.** The Navigator lists everything on your map. A table with metrics appears twice: its rows (`invoices`) and its summary (`invoices_summary`). Tick what you need, then choose **Load**, or **Transform Data** to shape it first.
:::
Use the summary for numbers. `net revenue` in a summary is the definition your business signed off, composed by the same engine that answers it in sanda. Use the rows table for detail, and ignore its `_row` column: it is sanda's row number for that read and it is not stable from one refresh to the next.
A table's description in the fluid is sent as its OData description, for clients that show one.
## Shape the data in Power Query
Choosing **Transform Data** opens Power Query. What you do there is sent to sanda where it can be: removing columns becomes `$select`, and filtering rows becomes `$filter`. Narrowing at the source makes a refresh cheaper as well as smaller.
A query that loads one table is a few lines of Power Query M:
```powerquery title="Load a summary table"
let
Source = OData.Feed("https://console.sanda-os.com.au/api/odata/"),
invoices_summary = Source{[Name = "invoices_summary", Signature = "table"]}[Data]
in
invoices_summary
```
The credential is not part of the query. It is stored in Power BI's data source settings, so the token never appears in your file.
To keep this year's rows and only the columns you need:
```powerquery title="Filter and choose columns"
let
Source = OData.Feed("https://console.sanda-os.com.au/api/odata/"),
invoices_summary = Source{[Name = "invoices_summary", Signature = "table"]}[Data],
ThisYear = Table.SelectRows(invoices_summary, each [issue_date] >= #datetimezone(2026, 1, 1, 0, 0, 0, 0, 0)),
Columns = Table.SelectColumns(ThisYear, {"region", "issue_date", "net_revenue"})
in
Columns
```
The table and column names in these snippets are examples. Yours come from your own fluid, so copy them from the Navigator.
A few rules follow from what sanda accepts:
- **Filters combine with "and".** A filter that needs "or" or "not" is refused with a message saying so, and Power BI shows it in the error pane. Filter in separate steps, or after loading.
- **Filter dates with "on or after" and "before".** sanda compares dates as a window that includes its first day and excludes its end, so a month filter never loses its last day. A comparison that means "after" or "on or before" is refused.
- **Filter on a dimension, not a metric.** A metric is worked out from the rows a query reads.
The complete list is in the [feed reference](https://docs.sanda-os.com.au/odata/reference#query-options).
## Refresh
In Power BI Desktop, choose **Refresh** on the **Home** ribbon. Every refresh is a live read of your warehouse through the read-only role, billed to your workspace like any other query. sanda sends tables in pages of 1,000 rows, and Power BI follows the links to the next pages on its own.
## Publish and schedule a refresh in the service
A dataset in the Power BI service refreshes without your desktop, so it needs the token entered again there. sanda's feed is on the public internet, so the service reads it directly and no on-premises data gateway is involved.
:::steps
1. **Publish the report** from Power BI Desktop to a workspace in the Power BI service.
2. **Open the dataset's settings.** In the service, find the dataset in its workspace and open its **Settings**.
3. **Edit the credentials.** Under **Data source credentials**, choose **Edit credentials** for the sanda data source.
4. **Choose Basic.** Set the authentication method to **Basic**, leave the user name empty or type any text, and paste the token into the password box. Choose a privacy level, then **Sign in**.
5. **Turn on scheduled refresh.** Under **Scheduled refresh**, switch it on, choose how often it runs and save.
:::
How often the service lets you refresh depends on your Power BI licence. Whatever the schedule, each run is a query against your warehouse, so choose a frequency the report actually needs.
When you [rotate the token](https://docs.sanda-os.com.au/mcp/agent-tokens#rotate-a-token), edit the credentials here as well as in Power BI Desktop. A dataset whose token was revoked or has expired fails its next refresh with a sign-in error.
### Power Query Online and dataflows
A dataflow uses Power Query Online, which has an OData connector. Give it the same address and the same Basic credential, with the token as the password.
## If something goes wrong
Power BI shows sanda's own message when it can.
| What Power BI shows | What it means | What to do |
|---|---|---|
| A sign-in prompt, or a credentials error on refresh | The token is wrong, revoked or expired | Create a new token and update the credentials, in Desktop and in the service |
| "This workspace has nothing on its semantic map yet" | No tables are on the map | Import a table under **Intelligence · Semantic fluid** |
| "sanda can only combine filters with “and”" | A filter used “or” or “not” | Rewrite it without them |
| "... is a metric, worked out from the rows sanda reads" | A filter named a metric | Filter on a dimension instead |
| "... looks like it holds a secret, so sanda won't select it" | A column whose name looks like a password or key is in the table, and sanda never reads those | Rename the column if the name is wrong, or build a view that leaves it out. See [views](https://docs.sanda-os.com.au/modelling/views) |
| "Reading ... took longer than sanda will wait" | The read ran past 30 seconds | Filter the table, or remove columns |
More help is in [troubleshooting](https://docs.sanda-os.com.au/help/troubleshooting).
## Next steps
:::links
- [OData feed reference](https://docs.sanda-os.com.au/odata/reference): Every table, option, limit and error.
- [Connect Excel](https://docs.sanda-os.com.au/odata/excel): The same feed in a workbook.
- [Agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens): Create, rotate and revoke the token your dataset holds.
:::
---
# OData feed reference
> The sanda OData feed in full: addresses, tables, data types, query options, the filter grammar, paging, limits and every error it returns.
sanda serves your semantic fluid as an OData v4 service. This page is the exact contract: what to send, what comes back, and what sanda refuses. For a walk through, see [Connect Excel](https://docs.sanda-os.com.au/odata/excel) or [Connect Power BI](https://docs.sanda-os.com.au/odata/power-bi).
The feed is read only and is on every edition.
## Addresses
The service root is:
```text
https://console.sanda-os.com.au/api/odata/
```
Keep the trailing slash. Some clients resolve their own links against this string, and without it the service document is the last path segment rather than the root.
| Address | Returns |
|---|---|
| `/api/odata/` | The **service document**: every table this workspace offers |
| `/api/odata/$metadata` | The **CSDL document**: every table's columns and their types, as XML |
| `/api/odata/` | The rows of one table |
`$metadata` is also accepted percent-encoded as `%24metadata`.
Only `GET` and `HEAD` are accepted. Anything else is refused with 405 and `Allow: GET, HEAD`. Every response carries `OData-Version: 4.0` and `Cache-Control: no-store`. Rows come back as `application/json;odata.metadata=minimal;charset=utf-8`, and `$metadata` as `application/xml;charset=utf-8`.
A browser request that carries an `Origin` other than sanda's own is refused with 403, so a web page on another site cannot read the feed.
## Authentication
A feed takes a credential that was typed in. It never uses a signed-in browser session.
| Credential | How to send it | Use it for |
|---|---|---|
| Integration token as a Basic credential | `Authorization: Basic `, with the token as the password | Excel, Power BI, and anything that cannot set a bearer header |
| Integration token as a bearer | `Authorization: Bearer bat_...` | Scripts and clients that can set a header |
| OAuth access token as a bearer | `Authorization: Bearer ` | A client that already signed a person in with OAuth |
Notes:
- **Either half of a Basic credential is read.** A token begins `bat_`, and sanda picks it out of the pair, so a token pasted into the user name box works too.
- **An OAuth access token is refused as a Basic credential.** It expires in an hour and is refreshed by a client that knows how, which a workbook does not.
- **The token needs `query:run`.** Every integration token has it. Nothing else is asked for, and the feed cannot write, define or run SQL of its own.
- **A missing or wrong credential is a 401** with `WWW-Authenticate: Basic realm="sanda", charset="UTF-8", Bearer realm="sanda"`. That header tells a client to ask for a credential rather than treat the response as a failure.
Get a token under **Settings · Integrations**. See [agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens).
## Tables
For each table on the semantic map, the feed offers a **rows** table named for the table, and, when the table has at least one metric, a **summary** table.
| Table | Name | Properties |
|---|---|---|
| Rows | `` | Every column of the table the fluid can see, up to 200. A column that has a dimension takes the dimension's name |
| Summary | `_summary` | The table's dimensions, then its metrics. One row for each combination of dimension values |
Names in the fluid are lowercase prose. The feed folds each into an identifier: lowercase, every run of characters other than letters and digits becomes one underscore, underscores at either end are dropped, and a name that starts with a digit gets an `n` in front. `order details` is `order_details`, and `on-time delivery %` is `on_time_delivery`. If two names fold to the same identifier, the second gets `_2`, then `_3`, so both stay reachable.
Every table also has a `_row` property, typed `Edm.Int64`. It is the row's position in that read (a `$skip` of 1,000 makes the first row of the next page `1000`). OData needs every row to have a key, and sanda has no honest business key to offer, because a declared primary key is missing on plenty of tables and a key that is not unique makes a client silently drop rows. `_row` is not a business key and it does not survive a refresh. You cannot filter or sort by it, and it is ignored in `$select`.
Summary tables are grouped by every dimension they carry, so `$select` on a summary is also a coarser grouping. Select only `region` and `net_revenue`, and you get one row for each region.
A table is only in the feed when it is on the map. If the map is empty, the service document lists no tables and reading any table returns 404.
### Service document
```json
{
"@odata.context": "https://console.sanda-os.com.au/api/odata/$metadata",
"value": [
{ "name": "invoices", "kind": "EntitySet", "url": "invoices" },
{ "name": "invoices_summary", "kind": "EntitySet", "url": "invoices_summary" }
]
}
```
### Metadata
`$metadata` is a CSDL 4.0 document in the namespace `sanda`, with the container `fluid`. Each table is an entity set, and its entity type is named `_row`.
```text title="Abridged"
```
- **Every property is nullable** except `_row`, because a column you left out with `$select`, a join that matched nothing and an empty cell look the same to a client.
- **Decimals carry `Scale="variable"`.** CSDL defaults an unstated scale to zero, which would have a client round every price and total.
- **A table's description** rides as the annotation `Org.OData.Core.V1.Description`, for clients that show a description beside a table. A summary table's description is generated: "invoices: its metrics, grouped by its dimensions."
## Data types
Types are declared in `$metadata` and values are typed to match, so a number arrives as a number and Excel can add it up.
| Warehouse type | OData type | In JSON |
|---|---|---|
| `boolean` | `Edm.Boolean` | `true` or `false` |
| `smallint` | `Edm.Int16` | A number |
| `integer` | `Edm.Int32` | A number |
| `bigint` | `Edm.Int64` | A number, or a string when it has more than 15 significant digits |
| `numeric`, `decimal` | `Edm.Decimal` | A number, or a string when it has more than 15 significant digits |
| `money` | `Edm.String` | The amount as the warehouse formats it, such as `$1,234.50`. Cast it to `numeric` in the model for a number |
| `real`, `double precision` | `Edm.Double` | A number |
| `date` | `Edm.Date` | `2026-06-30` |
| `timestamp`, `timestamp with time zone` | `Edm.DateTimeOffset` | `2026-06-30T04:05:06.000Z`, always UTC |
| `time`, `time with time zone` | `Edm.TimeOfDay` | `14:30:00`. A time zone offset is dropped, because OData has no time of day with an offset |
| `interval` | `Edm.String` | An ISO 8601 duration, such as `P0Y1M3DT4H0M0S`. OData's duration type cannot hold months or years, so an interval is sent as text |
| `uuid` | `Edm.Guid` | A string |
| `text`, `character varying`, `jsonb`, arrays, ranges and enums | `Edm.String` | A string. JSON and arrays are sent as JSON text. Nothing is truncated |
| A metric in a summary | `Edm.Decimal` | A number |
Two things follow. A value too large to be exact as a JSON number arrives as a string rather than as a number that is close. And a client that would rather have every 64-bit integer and decimal as a string can say so by sending `IEEE754Compatible=true` in its `Accept` header:
```text
Accept: application/json;IEEE754Compatible=true
```
## Query options
Options are applied in the warehouse and not after the rows arrive, so a `$filter` makes a refresh cheaper as well as smaller.
| Option | Supported | Behaviour |
|---|---|---|
| `$select` | Yes | A comma-separated list of property names, case insensitive. Fewer columns is a cheaper query. `_row` and `*` are ignored, and a `$select` that names nothing else is refused |
| `$filter` | Yes | See [the filter grammar](https://docs.sanda-os.com.au/odata/reference#the-filter-grammar) |
| `$orderby` | Yes | Comma-separated `name`, `name asc` or `name desc`. You can order only by properties you also select. `_row` is refused |
| `$top` | Yes | A whole number of rows, 0 or more. It bounds the whole read, not one page. `$top=0` returns an empty page |
| `$skip` | Yes | A whole number of rows to skip, from 0 to 1,000,000 |
| `$format` | `json` only | `json` or `application/json`. Any other value is refused |
| `$count` | `$count=false` only | `$count=false` is accepted, and any other value is refused |
| `$expand`, `$apply`, `$search` | No | Refused by name, with a message saying what to do instead |
sanda does not implement any other system query option and does not read one. The four it refuses by name are refused because ignoring them would give you a bigger answer than you asked for, and nothing is dropped quietly:
- `$count` would need a second scan of the table, billed to the workspace, to fill in a number nothing on the sheet needs. Follow the pages instead.
- `$expand`: the tables in the feed have no links between them. sanda joins on the map instead, so ask for a summary, or model a view under **Data · Modelling**.
- `$apply`: sanda does its grouping in the fluid. Every table with metrics already has a summary that is grouped.
- `$search`: use `$filter` with `contains`, or sanda search in the console.
### The filter grammar
A `$filter` is one or more conditions joined by `and`. Parentheses are accepted and change nothing, because every condition is combined with `and`.
```text
region eq 'AU' and total ge 100 and contains(notes,'urgent')
```
**Comparisons** have the form `property operator value`:
| Operator | Means | Applies to |
|---|---|---|
| `eq` | Equals | Text, number, boolean, date |
| `ne` | Does not equal | Text, number, boolean, date |
| `gt` | Greater than | Number |
| `ge` | Greater than or equal, or on or after | Number, date |
| `lt` | Less than, or before | Number, date |
| `le` | Less than or equal | Number |
**Text functions** take a text property and a quoted value:
| Function | Example |
|---|---|
| `contains(property,'value')` | `contains(notes,'urgent')` |
| `startswith(property,'value')` | `startswith(region,'New')` |
| `endswith(property,'value')` | `endswith(region,'Wales')` |
**Null tests** are `property eq null` (the value is empty) and `property ne null` (it has a value). Only `eq` and `ne` may be used with `null`.
**Values:**
- Text is in single quotes, and a quote inside text is doubled: `'O''Brien'`.
- Numbers are bare: `100`, `-2.5`.
- Dates and instants are bare, in ISO 8601: `2026-01-01` or `2026-01-01T00:00:00Z`.
- Booleans are bare: `true` or `false`.
Property names are case insensitive and are the names in `$metadata`. What kind of property it is decides which operators it accepts: a **date** is a property whose dimension is a time dimension or whose column is a date or timestamp, a **number** is a numeric column, a **boolean** is a boolean column, and everything else is **text**.
What sanda refuses, always with a sentence that says what to write instead:
- **`or` and `not`.** A query carries conditions that are all combined with `and`, so a disjunction has nowhere to go, and turning one into an `and` would answer a narrower question than the one asked. Ask two questions, or use `ne`.
- **`gt` and `le` on a date.** sanda compares dates as a window that includes its first day and excludes its end, which is why a month filter never loses its last day. Use `ge` for on or after and `lt` for before.
- **A metric.** A metric in a summary is worked out from the rows a query reads, so it cannot decide which rows are read. Filter on a dimension.
- **A text function on a property that is not text.**
- **More than 20 conditions.**
- **A property that is not in the table.** The message suggests the nearest names.
A month, filtered correctly:
```text
$filter=issue_date ge 2026-06-01 and issue_date lt 2026-07-01
```
## Paging
A page is at most **1,000 rows**. When more rows remain, the response carries `@odata.nextLink`, an address that keeps every option you sent and moves `$skip` on. Its query string is percent-encoded (`%24skip`), which every client decodes. Excel and Power BI follow it without being asked, so a large table just takes a little longer.
```json
{
"@odata.context": "https://console.sanda-os.com.au/api/odata/$metadata#invoices_summary(region,net_revenue)",
"value": [
{ "_row": 0, "region": "AU", "net_revenue": 128450.5 },
{ "_row": 1, "region": "NZ", "net_revenue": 41210 }
],
"@odata.nextLink": "https://console.sanda-os.com.au/api/odata/invoices_summary?%24select=region%2Cnet_revenue&%24skip=1000"
}
```
The rows and numbers above are an illustration. `@odata.context` names the columns you selected in brackets, and leaves them out when you selected all of them.
- **Every page is ordered.** An `OFFSET` over an unordered result is a different arbitrary slice on every page, and the symptom is a workbook missing rows. If you send no `$orderby`, sanda sorts ascending by the first four dimension or column properties it returns. Two rows that tie on every one of them may swap places, which a client cannot tell apart anyway.
- **`$top` bounds the whole read.** A `$top` of 50 returns 50 rows and no `nextLink`, however large the table is. A `$top` of 2,500 returns three pages, and the `nextLink` carries the rows still to come.
- **`$skip` goes up to 1,000,000.** Past that, every page costs the warehouse the whole scan again, so sanda refuses with `becca.too_deep`. Narrow the feed with `$filter`, read a summary instead, or build a view under **Data · Modelling**.
## Limits
| Limit | Value |
|---|---|
| Rows in one page | 1,000 |
| Deepest `$skip` | 1,000,000 |
| Time one read may take | 30 seconds |
| Columns in a rows table | 200 |
| Conditions in one `$filter` | 20 |
A read runs through the warehouse's read-only role and is billed to the workspace like any other query. See [usage](https://docs.sanda-os.com.au/billing/usage).
The feed composes no SQL of its own. Every read goes through the composer that answers questions in sanda's workbench, so anything that surface refuses, the feed refuses for the same reason: a column whose name looks like it holds a secret, two tables that would fan out if joined, a table a join repeats that has no key columns. Reading a table that has a column named like a secret is refused whole. Leave the column out with `$select`, rename it if the name is wrong, or build a view without it.
## Errors
A read sanda will not compose is a 4xx that carries sanda's own sentence. A 200 with no rows would look to Excel like an empty table, and nothing would say why, so a refusal is never a 200.
```json
{
"error": {
"code": "becca.refused",
"message": "sanda can only combine filters with “and”. Ask the two questions separately, or narrow with eq on a list."
}
}
```
`code` is a stable word a script can branch on, and `message` is the sentence a person sees in Excel's error pane.
| Status | `code` | When |
|---|---|---|
| 400 | `becca.refused` | A `$select`, `$filter`, `$orderby`, `$top` or `$skip` sanda cannot use, or a question the composer will not answer |
| 400 | `becca.unsupported` | `$count`, `$expand`, `$apply` or `$search`, or a `$format` that is not JSON |
| 400 | `becca.too_deep` | A `$skip` beyond 1,000,000 |
| 401 | `becca.unauthorized` | No credential, or one sanda does not recognise. Carries `WWW-Authenticate` |
| 402 | `becca.suspended` | The workspace is suspended |
| 402 | `becca.budget_suspended` | The warehouse is suspended for its budget |
| 403 | `becca.scope` | The token cannot ask questions |
| 403 | `becca.origin` | A browser sent an `Origin` sanda does not answer |
| 404 | `becca.no_such_set` | No table by that name. The message lists the tables there are |
| 405 | `becca.method` | Not a `GET` or `HEAD` |
| 502 | `becca.warehouse` | The read timed out, or the warehouse failed. A timeout says to narrow it with `$filter` or `$select` |
| 503 | `becca.no_warehouse` | The workspace has no warehouse to read yet |
## Try it from a terminal
`-u ":$TOKEN"` is the Basic form with an empty user name.
```bash
TOKEN=bat_your_token_here
# every table this workspace offers
curl -s https://console.sanda-os.com.au/api/odata/ -u ":$TOKEN"
# the columns and their types
curl -s 'https://console.sanda-os.com.au/api/odata/$metadata' -u ":$TOKEN"
# ten rows
curl -s 'https://console.sanda-os.com.au/api/odata/invoices?$top=10' -u ":$TOKEN"
# this year, by region
curl -s 'https://console.sanda-os.com.au/api/odata/invoices_summary?$select=region,net_revenue&$filter=issue_date%20ge%202026-01-01' \
-u ":$TOKEN"
# the same with a bearer header
curl -s https://console.sanda-os.com.au/api/odata/ -H "Authorization: Bearer $TOKEN"
```
Replace `invoices`, `invoices_summary` and the column names with your own, which the service document and `$metadata` list. Put a space in a URL as `%20`.
## Next steps
:::links
- [Connect Excel](https://docs.sanda-os.com.au/odata/excel): Data, Get Data, From OData Feed.
- [Connect Power BI](https://docs.sanda-os.com.au/odata/power-bi): Desktop, Power Query and scheduled refresh.
- [Filters](https://docs.sanda-os.com.au/reference/filters): The operators and named periods sanda uses in its own filters.
:::
---
# Reference
> Exact answers, generated from the product wherever possible: limits, scopes, sync modes, filters, formulas, SQL and every sanda term.
The pages in this tab are for looking things up. Where a figure or a list is something sanda enforces (a plan limit, a scope, a sync mode, a filter operator, a formula function, an MCP tool), the table is generated from the same definitions the product runs on, so it matches what the console and the server actually do.
## Look it up
:::links
- [Limits](https://docs.sanda-os.com.au/reference/limits): Every limit sanda enforces, per edition, and what happens when you reach one.
- [Scopes](https://docs.sanda-os.com.au/reference/scopes): The permissions an agent token or a connected app can hold, and the tools each one allows.
- [MCP tool reference](https://docs.sanda-os.com.au/mcp/tools): Every tool sanda's MCP server offers, with its parameters, scope and edition.
- [Sync modes](https://docs.sanda-os.com.au/reference/sync-modes): The five ways sanda reads a source and writes it to your warehouse.
- [Filters and date ranges](https://docs.sanda-os.com.au/reference/filters): The operators and named periods behind every filter.
- [Sheet formulas](https://docs.sanda-os.com.au/reference/sheet-formulas): The operators, functions, errors and formats a report sheet understands.
- [SQL reference](https://docs.sanda-os.com.au/reference/sql): The dialect, schemas and naming, and what sanda sql allows.
- [Glossary](https://docs.sanda-os.com.au/reference/glossary): Every sanda term, alphabetically.
- [Keyboard shortcuts](https://docs.sanda-os.com.au/reference/keyboard-shortcuts): Every shortcut in the console.
:::
## For machines
Every page on this site is also published as Markdown at the same address with `.md` on the end, for example [this page as Markdown](https://docs.sanda-os.com.au/reference.md). An index of every page, for AI assistants and other tools, is at [llms.txt](https://docs.sanda-os.com.au/llms.txt), and the whole site as one file is at [llms-full.txt](https://docs.sanda-os.com.au/llms-full.txt).
---
# Glossary
> Every sanda term in alphabetical order, each with a short definition and a link to the page that covers it.
Terms are listed alphabetically. Names inside the semantic fluid, such as a table, metric or dimension, are always lowercase. For a guided introduction to the main ideas, read [core concepts](https://docs.sanda-os.com.au/get-started/concepts).
### Admin
A member role that can do everything an owner can except the owner's decisions: closing, reopening, deleting or handing over the workspace, changing or removing an owner, switching sanda search on or off, and requiring two-step sign-in. Admins provision warehouses, connect systems, manage the team, set budgets, and create agent tokens. See [members and roles](https://docs.sanda-os.com.au/workspace/members-and-roles).
### Agent token
A secret that lets a program read your workspace without a person signing in, such as Claude Code, an agent of your own, Excel or Power BI. Owners and admins create one under **Settings · Integrations**, where it is called an integration token. sanda shows it once, its optional permissions start off, and it can expire or be revoked. See [agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens).
### Alias
What your team calls a table or column. An alias maps a hard-to-read column name to a business term, and can carry synonyms and value labels, such as a stored value `AUTHORISED` shown as `Approved`. See [naming](https://docs.sanda-os.com.au/semantic-fluid/naming).
### Approval
On the **Agents** page, an owner or admin can turn on **Ask before an agent acts**. An agent's risky actions, such as committing SQL, dropping a derived view or removing a search service, then wait for approval instead of running. Anything not decided within 24 hours expires without running. See [MCP permissions](https://docs.sanda-os.com.au/mcp/permissions).
### Ask first
The default cherry permission mode. Every change cherry proposes waits as a card with **Confirm** and **Decline**, and nothing runs until you press one. Compare [autonomous](https://docs.sanda-os.com.au/reference/glossary#autonomous) and [held action](https://docs.sanda-os.com.au/reference/glossary#held-action). See [actions and approvals](https://docs.sanda-os.com.au/cherry/actions-and-approvals).
### Autonomous
A cherry permission mode. cherry defines semantics, builds views, schedules tasks, starts syncs and loads rows as soon as it decides to. It still asks before emailing a report, dropping a view, retiring a table, or deleting a definition or a delivery. Set it under **Settings · cherry**. See [actions and approvals](https://docs.sanda-os.com.au/cherry/actions-and-approvals).
### sanda search
Search over the text inside your own tables, by meaning as well as by words. You define a [search service](https://docs.sanda-os.com.au/reference/glossary#search-service), sanda builds an index in your warehouse, and a query returns the records that match. An owner must switch it on first, and it is included from Standard. The console calls its page **Vector search**. See [search](https://docs.sanda-os.com.au/search).
### sanda sql
The name for SQL that runs against your warehouse, whether from the [SQL shell](https://docs.sanda-os.com.au/reference/glossary#sql-shell) or written by cherry. It is PostgreSQL-compatible. See [SQL reference](https://docs.sanda-os.com.au/reference/sql).
### Block
One piece of a report: a figure (kpi), a bar chart, a line chart, a table, a written analysis, or a header. A block stores the question it asks, not the rows, and asks again each time the report opens. See [blocks](https://docs.sanda-os.com.au/reports/blocks).
### Budget
A monthly ceiling on metered spend that an owner or admin sets under **Settings · Plan & budget**, in **Budget & alerts**. Reminder emails go out as the month fills, and at 100% the warehouse is [suspended](https://docs.sanda-os.com.au/reference/glossary#suspended) until an owner or admin resumes it. A trial can set one too, and until it does, its only ceiling is its free credit. See [budgets and limits](https://docs.sanda-os.com.au/billing/budgets-and-limits).
### Category
One word for a way of slicing, across every column that carries it, such as `region` or `product line`. See [dimensions and filters](https://docs.sanda-os.com.au/semantic-fluid/dimensions-and-filters).
### cherry
sanda's AI data agent. It answers questions, defines metrics and relationships, builds views and reports, starts syncs, and walks a new workspace through setup. It lives in a pane beside your work, and on its own page. See [cherry](https://docs.sanda-os.com.au/cherry).
### cherry chat
The full-screen page for cherry, in the left navigation. It is the same conversation as the cherry pane. See [a tour of the console](https://docs.sanda-os.com.au/get-started/console-tour).
### Closed workspace
A workspace an owner has closed. It is read-only for 90 days, so every page still opens and an export can still be taken. After that, the warehouse and its contents are destroyed. An owner can reopen it at any point before then. See [close or delete](https://docs.sanda-os.com.au/workspace/close-or-delete).
### Compute
The processing your warehouse does when queries run. It scales automatically, drops to zero when idle, and is billed by the hour it works. A question wakes it for a minute, so several questions inside that minute count once. See [usage](https://docs.sanda-os.com.au/billing/usage).
### Connected app
An AI assistant, most often Claude, that a person has connected by approving sanda's consent screen. It acts as that person, with the permissions they ticked. Owners and admins see every connected app under **Settings · Integrations**, beside the agent tokens, and can disconnect any of them. See [connect Claude](https://docs.sanda-os.com.au/mcp/connect-claude).
### Connection
A link between one source system and one warehouse. It records the connector, the streams to sync and the schedule. You add one from **Data · Connections**. See [connections](https://docs.sanda-os.com.au/connections).
### Connector
A supported kind of source system, such as Xero, Shopify or PostgreSQL. The connector catalogue lists every one. See [connectors](https://docs.sanda-os.com.au/connectors).
### Console
The signed-in web app where you work in sanda, at console.sanda-os.com.au. See [a tour of the console](https://docs.sanda-os.com.au/get-started/console-tour).
### Cursor
The column an incremental stream uses to tell which rows are new or changed since the last run, such as an updated-at timestamp. See [sync modes](https://docs.sanda-os.com.au/reference/sync-modes).
### Daily AI limit
The most AI a workspace can use in a day. It starts at A$5, and owners and admins can raise it to A$50 under **Budget & alerts**. Every AI call counts, including sanda's own work such as learning and search indexing. At the limit, AI waits until midnight UTC and nothing else stops. See [models and usage](https://docs.sanda-os.com.au/cherry/models-and-usage).
### Data region
Where a workspace's data is held: its warehouses, and the loading, staging and modelling of what you sync. You choose it when you sign up, and the workspace stays in it, so it cannot be changed later. Workspaces can be created in Australia (Sydney). AI features, and sanda search if an owner switches it on, send some data outside the region, as [how sanda works](https://docs.sanda-os.com.au/get-started/how-becca-works#where-your-data-lives) explains. See [workspace settings](https://docs.sanda-os.com.au/workspace#workspace).
### Data rules
Rules that limit which rows a reader sees on the reports portal, such as only their own region. They apply to readers, whose only door to the data is the portal. See [the reports portal](https://docs.sanda-os.com.au/reports/portal).
### Dataset
A group of tables in the semantic fluid. By default it is every stream from one connector, and it tints those tables on the map. See [tables](https://docs.sanda-os.com.au/semantic-fluid/tables).
### Delivery
A schedule that sends a report by email, or posts a summary to Slack, or posts each run as JSON to a webhook. You manage them on the **Report schedules** page. See [report delivery](https://docs.sanda-os.com.au/reports/delivery).
### Dimension
In the semantic fluid, a way to slice a number, such as `customer`, `region` or `month`. A time dimension has a grain such as day and rolls up to month, quarter and year. See [dimensions and filters](https://docs.sanda-os.com.au/semantic-fluid/dimensions-and-filters).
### Edition
Basic, Standard or Enterprise. An edition sets the platform fee, how many warehouses you can hold, how large each can grow, and whether sanda search and included support are on. A trial has every feature. See [editions](https://docs.sanda-os.com.au/billing/editions).
### Explorer
A read-only tree of every schema, table, view and procedure in a warehouse, with columns and the first rows of each. You reach it from **Data · Explorer**. See [the explorer](https://docs.sanda-os.com.au/warehouse/explorer).
### Fact
A kind of table. A fact table records something that happened and carries measures, such as invoices, payments or orders. The other kind is a dimension table, which records something that exists, such as customers or products. See [tables](https://docs.sanda-os.com.au/semantic-fluid/tables).
### Filter
A named condition that says which rows count, such as `approved invoices`. You write it once and reuse it. See [dimensions and filters](https://docs.sanda-os.com.au/semantic-fluid/dimensions-and-filters).
### Frozen workspace
A trial whose free credit is used up, or whose 7 days are over. Queries, reports, AI, syncs and scheduled runs wait, and your data is kept. Moving to an edition lifts the freeze, and the workspace carries on where it stopped. **Resume warehouse** does not lift it. See [the trial](https://docs.sanda-os.com.au/billing/trial).
### Full refresh
A sync mode family that reads the whole table on every run. It comes as **Overwrite**, which replaces what landed last time, **Overwrite + Deduped**, which also keeps one row per primary key, and **Append**, which keeps a snapshot of each run. It needs no cursor. See [sync modes](https://docs.sanda-os.com.au/reference/sync-modes).
### Glue
How two source systems join to each other, for example matching a store code in one system to a site id in another. It is one of the definitions on the map's rail. See [relationships](https://docs.sanda-os.com.au/semantic-fluid/relationships).
### Healing
When a source changes, such as a renamed column, sanda notices the drift, remaps the definitions that used it, and logs it. The Semantic fluid page lists these under "The fluid at work", each marked healed or open. See [semantic fluid](https://docs.sanda-os.com.au/semantic-fluid).
### Held action
A change cherry has proposed that is waiting for you. In [ask first](https://docs.sanda-os.com.au/reference/glossary#ask-first) mode it shows as a card with **Confirm** and **Decline**. When several are waiting, **Confirm all** runs them in the order cherry proposed them. See [actions and approvals](https://docs.sanda-os.com.au/cherry/actions-and-approvals).
### Incremental
A sync mode family that reads only the rows whose cursor has moved since the last run. It comes as **Append**, which adds them, and **Append + Deduped**, which keeps the latest version of each primary key. See [sync modes](https://docs.sanda-os.com.au/reference/sync-modes).
### Intake table
A table opened so that a connected assistant holding the intake permission can add rows to it, matching its columns, and do nothing else to it. It suits a log somebody keeps by talking. You open one when you upload a CSV, and close it in the **Intake tables** panel under **Settings · Integrations**. See [upload a CSV](https://docs.sanda-os.com.au/connections/upload-a-csv).
### Integration token
The name **Settings · Integrations** uses for an [agent token](https://docs.sanda-os.com.au/reference/glossary#agent-token).
### Learn from my data
Asking cherry to read your warehouse and propose the tables, relationships, metrics and dimensions it holds. It ends by telling you what stood out and what it could not settle, and opens the suggestions for you to review. See [learn from my data](https://docs.sanda-os.com.au/semantic-fluid/learn).
### Learning dataset
A sample dataset you can add from the Warehouse page: a specialty-foods importer with ten years of trading, already modelled. It is free and read-only, and it lets you ask questions before your own data lands. See [warehouse](https://docs.sanda-os.com.au/warehouse).
### Map
The canvas view of the semantic fluid. Tables are cards, relationships are lines, and a rail down the left opens each kind of definition: new table, datasets, relationships, aliases, categories, metrics, filters and glue. See [the map](https://docs.sanda-os.com.au/semantic-fluid/the-map).
### Materialized view
A view whose rows are stored and refreshed on demand or on a schedule, so queries read the stored result. You build one in **Modelling**, and can give it a unique key so a refresh does not block readers. See [views](https://docs.sanda-os.com.au/modelling/views).
### MCP
The Model Context Protocol, an open standard for letting AI assistants use tools. sanda is an MCP server, so an assistant such as Claude can search your definitions and query your data through the semantic fluid, with only the permissions you grant. It is included from Standard. See [MCP](https://docs.sanda-os.com.au/mcp).
### Member
A person with a seat in a workspace. Seats are not priced. A member's [role](https://docs.sanda-os.com.au/reference/glossary#role) decides what they can do. See [members and roles](https://docs.sanda-os.com.au/workspace/members-and-roles).
### Memory
What cherry keeps across conversations: facts about your business that you have told it, and how you like to work. Every line is read into every conversation, so remove a wrong one. You manage it under **Settings · cherry**. See [memory and history](https://docs.sanda-os.com.au/cherry/memory-and-history).
### Metering
How sanda measures what you use: storage in gigabyte-months held, compute in hours, AI by the token, and managed sync by the run. Each edition adds a flat platform fee. The console shows usage as it accrues. See [usage](https://docs.sanda-os.com.au/billing/usage).
### Metric
A number defined once, with a name, a definition and optional conditions, such as `net revenue`. Every answer that uses it uses the same definition, and one metric can build on another. See [metrics](https://docs.sanda-os.com.au/semantic-fluid/metrics).
### Modelling
The area of the console where you build views, materialized views and procedures in your warehouse, and the tasks that keep them fresh. What you build lives in the `derived` schema. See [modelling](https://docs.sanda-os.com.au/modelling).
### OData feed
A read-only feed of your semantic fluid that Excel, Power BI and Power Query read with nothing installed. It uses an agent token, and it works on every edition. See [OData](https://docs.sanda-os.com.au/odata).
### Optimise
A page reached from a warehouse's card that finds ways to speed up queries and reclaim storage. Review the evidence and the SQL before applying a change, because each statement uses billed compute. See [warehouse](https://docs.sanda-os.com.au/warehouse).
### Owner
The role you have when you sign up. An owner can do everything, including closing or deleting the workspace and handing ownership to an admin. See [members and roles](https://docs.sanda-os.com.au/workspace/members-and-roles).
### Pipeline
The console page that shows one timeline of what runs and when: syncs, task runs and report deliveries. You reach it from **Monitor · Pipeline**. See [pipeline](https://docs.sanda-os.com.au/modelling/pipeline).
### Platform fee
The flat monthly fee for a workspace, set by its edition. A trial pays none. It is separate from what you use. See [editions](https://docs.sanda-os.com.au/billing/editions).
### Primary key
The column, or columns together, that identify one row of a table. The sync modes that deduplicate need one. When you add a table to the map, sanda guesses its key and checks the guess against a sample of the rows. See [sync modes](https://docs.sanda-os.com.au/reference/sync-modes).
### Procedure
A stored routine, written in SQL or PL/pgSQL, that you build in Modelling and can run on demand or as a step in a task. See [modelling](https://docs.sanda-os.com.au/modelling).
### Provisioning
Creating your warehouse. sanda does it in under a minute, from **Data · Warehouse**. See [provision a warehouse](https://docs.sanda-os.com.au/warehouse/provision).
### Public link
A link that lets anyone who holds it read one report, with no account. The link is the whole credential, so treat it like a password. You can set an expiry and revoke it. See [sharing](https://docs.sanda-os.com.au/reports/sharing).
### Reader
A member role for people you build reports for. A reader signs in to the reports portal, reads what you publish, and can ask cherry about it. They have no console, and data rules can limit their rows. See [the reports portal](https://docs.sanda-os.com.au/reports/portal).
### Read-only
A member role that can open every page and ask every question but changes nothing. See [members and roles](https://docs.sanda-os.com.au/workspace/members-and-roles).
### Relationship
How two tables join. You draw it between two columns on the map, and it records which side holds a single row. Any other table that carries the same key becomes joinable too. See [relationships](https://docs.sanda-os.com.au/semantic-fluid/relationships).
### Report
A canvas of blocks that you arrange on a page. A report stores the questions its blocks ask, so it shows current numbers each time it opens. See [reports](https://docs.sanda-os.com.au/reports).
### Reports portal
A place for the people you build reports for. They sign in, read the reports you publish to them, and can ask cherry about the numbers. They never see the console. See [the reports portal](https://docs.sanda-os.com.au/reports/portal).
### Role
What a member can do in a workspace: [owner](https://docs.sanda-os.com.au/reference/glossary#owner), [admin](https://docs.sanda-os.com.au/reference/glossary#admin), [read-only](https://docs.sanda-os.com.au/reference/glossary#read-only) or [reader](https://docs.sanda-os.com.au/reference/glossary#reader). See [members and roles](https://docs.sanda-os.com.au/workspace/members-and-roles).
### Schedule
When something runs: a connection's sync, a task in Modelling, or the delivery of a report. You pick a time of day in your own timezone, and runs fall on a 15 minute grid. See [schedules](https://docs.sanda-os.com.au/connections/schedules).
### Schemas
The groups of tables inside your warehouse. `raw` holds data as it landed. `derived` holds the views and procedures you build in Modelling. `mapping` holds what the semantic fluid publishes for reading, one view per table on the map. `search` holds sanda search indexes. See [how sanda works](https://docs.sanda-os.com.au/get-started/how-becca-works).
### Scope
A permission an agent token or connected app holds, such as reading the fluid, running queries or starting a sync. Every token can ask questions, and every other scope is off until someone switches it on. See [scopes](https://docs.sanda-os.com.au/reference/scopes).
### Search service
The definition behind sanda search. It names one table on your map, a key column that identifies each row, and the text columns to search. sanda builds the index in your warehouse and can refresh it after the connection that feeds it lands rows. See [create a search service](https://docs.sanda-os.com.au/search/create-a-service).
### Semantic fluid
The shared business language on top of your warehouse: the tables you allow, how they join, and what your numbers mean. cherry, sanda search, MCP, reports and the Excel feed all read through it, so they agree. See [semantic fluid](https://docs.sanda-os.com.au/semantic-fluid).
### Sheet
The instructions that shape a table block's rows once they arrive: formula columns, formats, totals, a sort, and hidden columns. A sheet stores instructions, never rows. See [sheets](https://docs.sanda-os.com.au/reports/sheets).
### Short name
The workspace's short name (its slug), shown in **Settings · Workspace** and typed to confirm closing or deleting the workspace. See [close or delete](https://docs.sanda-os.com.au/workspace/close-or-delete).
### Sign-in link
The single-use link sanda emails you to sign in. It works once and expires after 30 minutes, and asking for a new one cancels the earlier ones. There are no passwords. See [sign-in](https://docs.sanda-os.com.au/workspace/sign-in).
### SQL shell
A page for running one SQL statement at a time directly on a warehouse. Only owners and admins can use it. It is read-only unless you switch to read-write, results are capped and timed, every statement is recorded, and there is no undo. See [SQL](https://docs.sanda-os.com.au/warehouse/sql).
### Stream
One table or object that a connection syncs from its source, such as invoices or contacts. See [choose streams](https://docs.sanda-os.com.au/connections/choose-streams).
### Suggestion
What learning proposes and cherry offers for you to accept: a table, relationship, metric or dimension. Nothing reaches an answer until you accept it. You can accept it as it is, correct it first, or reject it. Also called a proposal. See [review suggestions](https://docs.sanda-os.com.au/semantic-fluid/review).
### Support ticket
A message you send from **System · Support**. It reaches sanda's team with your workspace's record attached, and appears under **Your tickets** with a status. See [getting help](https://docs.sanda-os.com.au/help).
### Suspended
The state of a warehouse whose month's metered spend has reached your budget. Queries, reports, chat, syncs and scheduled runs wait, and your data is kept. An owner or admin resumes it with **Resume warehouse** once the budget is raised or a new month begins. A trial that runs out of credit or reaches its end date is [frozen](https://docs.sanda-os.com.au/reference/glossary#frozen-workspace) instead. See [usage and suspension](https://docs.sanda-os.com.au/warehouse/usage-and-suspension).
### Sync
One run of a connection: it reads the source and lands the rows in your warehouse. It runs on the connection's schedule, or when you press **Sync now**. See [monitor syncs](https://docs.sanda-os.com.au/connections/monitor-syncs).
### Sync mode
How a stream is read and written on each run. There are five: three full refresh modes and two incremental modes. Each stream has its own. See [sync modes](https://docs.sanda-os.com.au/reference/sync-modes).
### Table
In the semantic fluid, a table or view from your warehouse that you have put on the map. It is either a [fact](https://docs.sanda-os.com.au/reference/glossary#fact) or a dimension. Adding a table publishes it as a view in the `mapping` schema. See [tables](https://docs.sanda-os.com.au/semantic-fluid/tables).
### Task
An ordered list of steps on one warehouse, such as refreshing a materialized view or calling a procedure. It runs on a schedule, after a named connection lands rows, or by hand, and it pauses itself after repeated failures. You manage tasks on the **Tasks** tab in Modelling. See [modelling](https://docs.sanda-os.com.au/modelling).
### Thread
One conversation with cherry. Threads are yours, and you can rename, archive and search them. See [memory and history](https://docs.sanda-os.com.au/cherry/memory-and-history).
### Trial
A trial has every feature and lasts 7 days, after which the workspace is [frozen](https://docs.sanda-os.com.au/reference/glossary#frozen-workspace) until it moves to an edition. It comes with A$10 of [free credit](https://docs.sanda-os.com.au/reference/glossary#trial-credit) and needs no card. See [the trial](https://docs.sanda-os.com.au/billing/trial).
### Trial credit
The free credit a trial comes with, A$10. Compute, storage, AI and sync runs draw it down at the published rates, and nothing is billed. When it is used up, the workspace is [frozen](https://docs.sanda-os.com.au/reference/glossary#frozen-workspace). The top bar shows the balance. See [the trial](https://docs.sanda-os.com.au/billing/trial).
### Two-step sign-in
A code from an authenticator app, asked for after your email link or your Google or Apple sign-in. Anyone can turn it on for themselves, and an owner can require it of everyone. 10 recovery codes cover a lost phone. See [two-step sign-in](https://docs.sanda-os.com.au/workspace/two-step).
### Vector search
The label for sanda search in the console's left navigation, under **Intelligence**. See [sanda search](https://docs.sanda-os.com.au/reference/glossary#sanda-search).
### View
A saved query in your warehouse, built in Modelling. A view can be put on the map, and cherry and reports then read it like any other table. See [views](https://docs.sanda-os.com.au/modelling/views).
### Warehouse
Your sanda warehouse: a dedicated PostgreSQL database that belongs to your workspace, held in its [data region](https://docs.sanda-os.com.au/reference/glossary#data-region). Every connection and CSV lands in it. See [warehouse](https://docs.sanda-os.com.au/warehouse).
### Workspace
Your business's home in sanda. It holds your warehouse, connections, semantic fluid, reports, cherry conversations, settings and team, and it is what you are billed for. An email address belongs to one workspace. See [workspace](https://docs.sanda-os.com.au/workspace).
---
# Limits
> Every limit sanda enforces, per edition, what happens when you reach one, and which you can raise.
A limit is the most of something your workspace can have. It is not a price. At a limit sanda pauses loading or slows queries, and it never bills past one. Prices are on [How billing works](https://docs.sanda-os.com.au/billing).
| Limit | Basic | Standard | Enterprise |
| --- | --- | --- | --- |
| Warehouses per workspace | 1 | 3 | 50 |
| Storage per warehouse | 10 GB | 100 GB | 16 TB |
| Concurrent connections per warehouse | 60 | 60 | 300 |
| Longest single query | 60 seconds | 60 seconds | 5 minutes |
| Search services per workspace | Not included | 5 | No limit |
| Rows per indexed table | Not included | 2,000,000 | No limit |
| Pushes per workspace | Not included | 1 | No limit |
| Compute per warehouse | Up to 4 compute units | Up to 8 compute units | Up to 16 compute units |
| Daily AI limit | A$5 to start, up to A$50 | A$5 to start, up to A$50 | A$5 to start, up to A$50 |
| Schedule grid | Every 15 minutes | Every 15 minutes | Every 15 minutes |
A trial has Standard's limits, with a smaller allowance for sanda search. See [Free trial](https://docs.sanda-os.com.au/billing/trial).
## How limits work
**Edition limits are ceilings.** They are the most a warehouse can be set to. You can set any warehouse's limits lower, which is a way to cap a month, and never higher than your edition allows. If you move to a lower edition, sanda caps what you had set at the new numbers.
**sanda measures and enforces them itself.** It reads each warehouse's size and the work it does regularly, warns at 80% of a limit, and enforces at the limit. Reads, reports and chat keep working throughout.
**You are told.** When a warehouse passes 80% of a limit, and when it reaches one, sanda emails your owners and admins, unless you have turned those alerts off. The warehouse card also says where it stands. See [Budgets and AI limits](https://docs.sanda-os.com.au/billing/budgets-and-limits).
To see or lower a warehouse's limits, go to **Data · Warehouse** and press **Limits** on its card. The form has **Storage limit (GB)**, **Compute limit (hours/day)**, **Concurrent queries** and **Statement ceiling (ms)**. Press **Apply limits** to save. Only owners and admins can.
## Warehouses per workspace
How many warehouses the workspace can hold. The sample learning dataset does not count.
- **At the limit,** sanda will not provision another. The message names your edition and how many it holds, and says to delete one you no longer need or to move up an edition.
- **To raise it,** change edition. Beyond the top edition's figure, talk to sanda support.
## Storage per warehouse
The most data one warehouse can hold, measured as the size of the warehouse's database. It includes tables you sync or upload, tables and views you build, and sanda search indexes.
- **At 80%,** the warehouse card reads "Past 80% of a limit".
- **At the limit,** loading pauses: syncs and CSV uploads stop landing rows. sanda will not start a new search service, because an index needs room to write. Queries are narrowed. Reads, reports and chat keep working. The card reads "At a limit", and loading resumes when the warehouse is back under the limit or the limit moves.
- **To raise it,** change edition. To get back under it, delete data you no longer need.
## Compute per warehouse
Compute is the same on every edition. The table shows the most any warehouse can use at once. It is billed on what runs, not on this ceiling.
You can also set a **Compute limit (hours/day)** on each warehouse, below the platform's ceiling. You are billed the hours used, including any that run once the warehouse is at its limit.
- **At the limit,** queries are narrowed, with fewer at once and less time for each, and loading pauses until use is back under. Reads, reports and chat keep working.
- **It does not change with edition.** Setting a lower limit is how you hold compute spend down.
## Concurrent connections per warehouse
How many queries can run against a warehouse at the same moment. It is the **Concurrent queries** field in the Limits form.
- **At the limit,** more than that cannot run at the same moment.
- **To raise it,** change edition. Enterprise allows more, as the table shows. You can set it lower.
## Longest single query
The longest a single query can run before sanda stops it. It is the **Statement ceiling (ms)** field in the Limits form, and it is in milliseconds. Rebuilds of views and scheduled runs have their own separate allowance.
- **At the limit,** the query is cancelled and returns a timeout error. Narrow it with a filter or a derived view.
- **To raise it,** change edition. Enterprise allows longer queries. You can set it lower.
## Search services per workspace
How many sanda search services the workspace can hold. Basic does not include sanda search, so it has none. Enterprise has no limit.
- **At the limit,** sanda will not create another. The message says how many the edition allows.
- **To raise it,** remove a service you no longer need, or change edition.
## Rows per indexed table
The largest table a single search service can index. sanda estimates the table's size when you create the service.
- **Over the limit,** sanda refuses to create the service and suggests narrowing the source with a derived view, or talking to sanda about a larger allowance.
- **To raise it,** change edition. Enterprise has no limit.
## Daily AI limit
The most AI spend a workspace can run up in a day. The table shows where it starts and the most owners and admins can set it to. The limit resets at midnight UTC.
- **At the limit,** AI waits for the reset. Syncs, reports and your data are unaffected.
- **To raise it,** an owner or admin can set their own limit up to the ceiling, in **Budget & alerts** under **Settings · Plan & budget**. For more, ask sanda support. See [Budgets and AI limits](https://docs.sanda-os.com.au/billing/budgets-and-limits).
## Schedule grid
Schedules start on a fixed grid of 15 minutes. It is the same on every edition and cannot be raised.
## What can be raised
| Limit | How to raise it |
|---|---|
| Warehouses per workspace | Change edition. Talk to sanda support beyond the top edition |
| Storage per warehouse | Change edition |
| Concurrent connections, longest single query | Change edition. Enterprise allows more of both |
| Search services, rows per indexed table | Change edition. Enterprise has no limit |
| Compute per warehouse | Not raised by edition. Every edition has the platform's ceiling |
| Daily AI limit | Owners and admins, up to the ceiling. Ask sanda support for more |
| Schedule grid | Fixed |
To change edition, see [Editions](https://docs.sanda-os.com.au/billing/editions).
:::links
- [Editions](https://docs.sanda-os.com.au/billing/editions): What each edition includes and how to ask to move.
- [Budgets and AI limits](https://docs.sanda-os.com.au/billing/budgets-and-limits): Your own ceilings on spend.
- [Usage and metering](https://docs.sanda-os.com.au/billing/usage): How storage and compute are measured.
:::
---
# Scopes
> Every permission an integration token or connected app can hold, what it includes, which are privileged, and which MCP tools each one allows.
A scope is one permission on an integration token or a connected app. sanda checks the scopes on every call to the MCP server, and a tool that a credential's scopes do not reach is left off its tool list.
There are twelve scopes. The list below gives each one's consent-screen wording, what it includes and the tools it allows. After it comes what to know before you grant one. Who can grant each scope, and how risky actions can wait for a person, is in [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions). Every tool is in the [MCP tool reference](https://docs.sanda-os.com.au/mcp/tools).
## Every scope
### fluid:read
Any member may grant it
Read what your workspace has modelled: its metrics, dimensions, named filters and the tables sanda may query.
Tools: [`search_fluid`](https://docs.sanda-os.com.au/mcp/tools#search-fluid), [`describe_table`](https://docs.sanda-os.com.au/mcp/tools#describe-table), [`search_docs`](https://docs.sanda-os.com.au/mcp/tools#search-docs).
### query:run
Any member may grant it
Ask questions of your data, composed from the definitions your workspace agreed on.
Includes `fluid:read`.
Tools: [`run_query`](https://docs.sanda-os.com.au/mcp/tools#run-query), [`search_documents`](https://docs.sanda-os.com.au/mcp/tools#search-documents).
### sql:run
Privileged: owners and admins grant it
Write and run its own read-only SQL against the tables sanda may query.
Includes `fluid:read`, `query:run`.
Tools: [`run_sql`](https://docs.sanda-os.com.au/mcp/tools#run-sql).
### fluid:write
Privileged: owners and admins grant it
Change what your workspace has modelled: put warehouse tables on the map, define metrics, dimensions, filters and relationships, and accept or remove proposals. Definitions it writes go live immediately.
Includes `fluid:read`.
Tools: [`list_warehouse_tables`](https://docs.sanda-os.com.au/mcp/tools#list-warehouse-tables), [`import_tables`](https://docs.sanda-os.com.au/mcp/tools#import-tables), [`set_table_kind`](https://docs.sanda-os.com.au/mcp/tools#set-table-kind), [`retire_tables`](https://docs.sanda-os.com.au/mcp/tools#retire-tables), [`define`](https://docs.sanda-os.com.au/mcp/tools#define), [`list_proposals`](https://docs.sanda-os.com.au/mcp/tools#list-proposals), [`review_proposal`](https://docs.sanda-os.com.au/mcp/tools#review-proposal), [`delete_definition`](https://docs.sanda-os.com.au/mcp/tools#delete-definition).
### workspace:read
Any member may grant it
See your connections and warehouses: what is connected, when it last synced, how full the warehouse is. Never a credential.
Tools: [`list_connections`](https://docs.sanda-os.com.au/mcp/tools#list-connections), [`connection_status`](https://docs.sanda-os.com.au/mcp/tools#connection-status), [`list_warehouses`](https://docs.sanda-os.com.au/mcp/tools#list-warehouses).
### workspace:manage
Privileged: owners and admins grant it
Start a sync on a connection.
Includes `workspace:read`.
Tools: [`sync_now`](https://docs.sanda-os.com.au/mcp/tools#sync-now).
### warehouse:write
Privileged: owners and admins grant it
Load tables of its own rows into your warehouse (as raw.csv__…), through the same loader and storage ceiling a sync uses. It cannot touch tables a sync landed.
Includes `workspace:read`.
Tools: [`load_rows`](https://docs.sanda-os.com.au/mcp/tools#load-rows), [`set_intake`](https://docs.sanda-os.com.au/mcp/tools#set-intake).
### warehouse:model
Privileged: owners and admins grant it
Define views, materialized views and procedures in your warehouse (as derived.…), rebuild and run them, and put a view on the semantic map. Every build runs on your compute and is billed to you.
Includes `workspace:read`.
Tools: [`list_sources`](https://docs.sanda-os.com.au/mcp/tools#list-sources), [`list_views`](https://docs.sanda-os.com.au/mcp/tools#list-views), [`create_view`](https://docs.sanda-os.com.au/mcp/tools#create-view), [`refresh_view`](https://docs.sanda-os.com.au/mcp/tools#refresh-view), [`drop_view`](https://docs.sanda-os.com.au/mcp/tools#drop-view), [`create_procedure`](https://docs.sanda-os.com.au/mcp/tools#create-procedure), [`call_procedure`](https://docs.sanda-os.com.au/mcp/tools#call-procedure), [`warehouse_advice`](https://docs.sanda-os.com.au/mcp/tools#warehouse-advice), [`optimise_warehouse`](https://docs.sanda-os.com.au/mcp/tools#optimise-warehouse), [`list_schedules`](https://docs.sanda-os.com.au/mcp/tools#list-schedules), [`create_schedule`](https://docs.sanda-os.com.au/mcp/tools#create-schedule), [`run_now`](https://docs.sanda-os.com.au/mcp/tools#run-now), [`pause_schedule`](https://docs.sanda-os.com.au/mcp/tools#pause-schedule), [`run_history`](https://docs.sanda-os.com.au/mcp/tools#run-history).
### search:manage
Privileged: owners and admins grant it
Define, rebuild and remove search services over the tables on your map. The text of the columns a service names is sent to an embedding model outside Australia to be indexed, and keeping one fresh costs tokens, storage and compute, billed to you.
Includes `workspace:read`.
Tools: [`list_search_services`](https://docs.sanda-os.com.au/mcp/tools#list-search-services), [`describe_search_service`](https://docs.sanda-os.com.au/mcp/tools#describe-search-service), [`estimate_search_build`](https://docs.sanda-os.com.au/mcp/tools#estimate-search-build), [`create_search_service`](https://docs.sanda-os.com.au/mcp/tools#create-search-service), [`update_search_service`](https://docs.sanda-os.com.au/mcp/tools#update-search-service), [`build_search_service`](https://docs.sanda-os.com.au/mcp/tools#build-search-service), [`run_search_build`](https://docs.sanda-os.com.au/mcp/tools#run-search-build), [`drop_search_service`](https://docs.sanda-os.com.au/mcp/tools#drop-search-service).
### sql:write
Privileged: owners and admins grant it
Write and run its own SQL on your warehouse and commit it: insert, update and delete rows and create tables in the derived schema, reading every landing table on the way. It runs as the same hand the SQL shell lends an owner, there is no undo, and every statement is recorded in your audit trail.
Includes `fluid:read`, `query:run`, `sql:run`, `workspace:read`.
Tools: [`execute_sql`](https://docs.sanda-os.com.au/mcp/tools#execute-sql).
### warehouse:append
Any member may grant it
Add rows to the tables an owner or admin has opened for intake, matching their columns, and nothing else: it cannot create, replace or delete a table, and it cannot change what is modelled. Any member may grant this, because which tables are open is decided by the people who run the workspace.
Includes `fluid:read`.
Tools: [`append_rows`](https://docs.sanda-os.com.au/mcp/tools#append-rows).
### reports:deliver
Privileged: owners and admins grant it
Email your reports on a schedule to the addresses it names, change or pause those deliveries, and send one now. Each delivery puts a copy of the report's numbers in someone's inbox, which cannot be recalled.
Tools: [`list_deliveries`](https://docs.sanda-os.com.au/mcp/tools#list-deliveries), [`create_delivery`](https://docs.sanda-os.com.au/mcp/tools#create-delivery), [`update_delivery`](https://docs.sanda-os.com.au/mcp/tools#update-delivery), [`send_delivery`](https://docs.sanda-os.com.au/mcp/tools#send-delivery), [`pause_delivery`](https://docs.sanda-os.com.au/mcp/tools#pause-delivery), [`delete_delivery`](https://docs.sanda-os.com.au/mcp/tools#delete-delivery).
## How scopes combine
A wider scope includes the narrower ones it depends on, so a credential never needs to hold both.
| Scope | Also includes |
|---|---|
| `fluid:read` | Nothing else |
| `query:run` | `fluid:read` |
| `sql:run` | `query:run`, `fluid:read` |
| `sql:write` | `sql:run`, `query:run`, `fluid:read`, `workspace:read` |
| `fluid:write` | `fluid:read` |
| `workspace:read` | Nothing else |
| `workspace:manage` | `workspace:read` |
| `warehouse:write` | `workspace:read` |
| `warehouse:model` | `workspace:read` |
| `search:manage` | `workspace:read` |
| `warehouse:append` | `fluid:read` |
| `reports:deliver` | Nothing else |
Nothing else nests. In particular:
- `sql:write` is never implied by `sql:run`, `fluid:write`, `warehouse:write` or `warehouse:model`. Reading through the read-only role and committing through the builder role are different kinds of trust.
- `warehouse:append` never implies `warehouse:write`, and it does not imply `query:run`.
- `reports:deliver` does not include `query:run`, because sending a report is not the same as asking its questions.
## Privileged scopes
Eight scopes change something or reach beyond the fluid, and only an **owner or admin** can grant them:
`sql:run`, `sql:write`, `fluid:write`, `workspace:manage`, `warehouse:write`, `warehouse:model`, `search:manage` and `reports:deliver`.
The other four (`fluid:read`, `query:run`, `workspace:read` and `warehouse:append`) can be granted by any member.
On the consent screen, a member sees the privileged scopes greyed out, and they are left out of the grant. Only an owner or admin can create an integration token at all. A privileged scope also lasts only as long as the person who granted it is still an owner or admin.
## What to know before you grant one
### Asking questions and running SQL
- **`fluid:read`** is what sanda offers a client that asks for no scope. It is ticked by default on the consent screen.
- **`query:run`** is held by every integration token, and the Excel and Power BI feed needs it. It includes `search_documents` on Standard and Enterprise, so searching an existing sanda search service needs nothing more. An MCP call returns at most 50 rows.
- **`sql:run`** allows one read-only `SELECT`, through the same read-only role as every question, returning at most 50 rows and stopping after 20 seconds. The assistant must say why it could not use `run_query`. A column whose name looks like a password or an identity number is refused.
- **`sql:write`** allows one statement per call, run as the warehouse's builder role. The builder reads landing tables, the derived schema and the `mapping` tables, and writes only in the derived schema. 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. Every statement is recorded with its reason, and a write-mode statement can be held for an owner or admin to approve. It includes `workspace:read` because a statement has to name a warehouse.
### Seeing and driving the workspace
- **`workspace:read`** never returns a credential or a connection string. Five other scopes include it.
- **`workspace:manage`** starts a sync. A sync reads from the source system and costs compute like any other.
### Changing the semantic fluid
- **`fluid:write`** makes definitions **live at once**, not proposals, which is why only an owner or admin can grant it. Every call runs the same handler as the console's own button, with the same validation and audit record. Approvals do not hold these tools. If you want a person to make every change, leave this scope off.
### Loading and shaping warehouse data
- **`warehouse:write`** lands tables as `raw.csv__`, up to 10,000 rows and 250 columns per call, and it cannot touch a table a sync landed. It also opens a table for intake.
- **`warehouse:append`** is the one write any member may grant. It starts unticked on the consent screen. It allows appending up to 10,000 rows per call to a table that is open, and nothing else. An owner or admin opens a table for intake when they upload a CSV, or with `load_rows` or `set_intake`, and closes it in the **Intake tables** panel of **Settings · Integrations**. It includes `fluid:read` so that the assistant can describe the table it adds to.
- **`warehouse:model`** builds in the derived schema, which the builder role can write and nothing else. Every build runs on your compute and is billed to you, and `drop_view` can be held for approval.
### Search and reports
- **`search:manage`** is part of sanda search, so it applies on Standard and Enterprise. A workspace owner must switch sanda search on in the console first, and an agent holding this scope cannot do that. `drop_search_service` can be held for approval.
- **`reports:deliver`** is email only, to at most 25 recipients within the recipient domains your workspace allows, on a schedule that falls on the quarter hour. `create_delivery` and `update_delivery` can be held for approval when a recipient is outside the email domains your members use.
`action_status` is offered to a credential holding `sql:write`, `warehouse:model`, `search:manage` or `reports:deliver`, because those are the scopes whose tools can be held.
## Where scopes are set
| Where | How |
|---|---|
| A connected app | The person ticks scopes on the consent screen. See [connect Claude](https://docs.sanda-os.com.au/mcp/connect-claude#the-consent-screen) |
| An integration token | An owner or admin turns on switches when creating it. **May run its own SQL** is `sql:run`, and so on. Every token also holds `query:run`. See [agent tokens](https://docs.sanda-os.com.au/mcp/agent-tokens#the-switches) |
| A client that asks | A client names scopes in the `scope` parameter of its authorization request. Scopes sanda does not have are dropped. See [connect other MCP clients](https://docs.sanda-os.com.au/mcp/connect-other-clients#send-the-person-to-the-consent-screen) |
An OAuth client that gets a tool refused with `insufficient_scope` has learned which scope it lacks, and can ask the person to approve it. An integration token cannot ask: an owner or admin turns the switch on, or creates a new token.
:::note
A workspace that has been closed is read-only. Every credential is limited to `query:run` and `workspace:read`, whatever else it was granted.
:::
---
# Sync modes
> The five ways sanda can read a source and write it to your warehouse, with what each needs and how each treats edits, deletes and history.
A sync mode is set per stream. It says how sanda reads the table at the source and how it writes the table in your warehouse. For advice on choosing, see [Sync modes](https://docs.sanda-os.com.au/connections/sync-modes) in the connections guide.
## The modes at a glance
| Mode | Needs a cursor | Needs a primary key | What it does |
| --- | --- | --- | --- |
| **Full refresh · Overwrite** (default) | No | No | Reads the whole table on every run and replaces what landed last time. |
| **Full refresh · Overwrite + Deduped** | No | Yes | Reads the whole table on every run, replaces what landed and keeps one row per primary key. |
| **Full refresh · Append** | No | No | Reads the whole table on every run and adds it beside the earlier copies: a history of snapshots. |
| **Incremental · Append** | Yes | No | Reads only the rows whose cursor has moved since the last run, and adds them. |
| **Incremental · Append + Deduped** | Yes | Yes | Reads only the rows whose cursor has moved, and keeps the latest version of each primary key. |
A **cursor** is a column that moves forward whenever a row is created or changed, such as `updated_at`. A **primary key** is the column, or columns, that identify one row. Both are chosen in the [stream picker](https://docs.sanda-os.com.au/connections/choose-streams) unless the source has already decided them.
## Full refresh · Overwrite
**Shown in stream lists as** `full refresh`. **Needs** nothing. **This is the default.**
Reads the whole table on every run and replaces what landed last time.
- **Edits at the source:** the table shows the new value after the next run.
- **Deletes at the source:** the row is gone after the next run, because the whole table is replaced.
- **History:** none. Only the result of the latest run exists.
It is the default for any stream you have not chosen a mode for, because every table supports it and it needs nothing answered. Where a table does not offer it, the picker opens on the first mode the table does offer.
## Full refresh · Overwrite + Deduped
**Shown in stream lists as** `full refresh · dedup`. **Needs** a primary key.
Reads the whole table on every run, replaces what landed, and keeps one row per primary key.
- **Edits at the source:** the table shows the new value after the next run.
- **Deletes at the source:** the row is gone after the next run.
- **History:** none.
Use it when a source can return the same key more than once and you want a single row for each.
## Full refresh · Append
**Shown in stream lists as** `full refresh · append`. **Needs** nothing.
Reads the whole table on every run and adds it beside the earlier copies: a history of snapshots.
- **Edits at the source:** each run's copy holds the value the row had at that run.
- **Deletes at the source:** the earlier copies still hold the row. It is missing from the copies that follow.
- **History:** one full copy per run. The table grows by the size of the source on every run, and the storage counts toward your warehouse limit.
Each landed row carries a loading column, whose name starts with an underscore, that records when it was extracted. It is what tells one run's copy from the next.
## Incremental · Append
**Shown in stream lists as** `incremental`. **Needs** a cursor.
Reads only the rows whose cursor has moved since the last run, and adds them. The first run has nothing to compare with, so it reads the whole table.
- **Edits at the source:** the changed row lands again as a new row, and the earlier version stays.
- **Deletes at the source:** a cursor cannot see a row that is no longer there, so the row stays in the table. The exception is a source read by its database's change log, which reports a deletion as a flag on the row.
- **History:** every version of every row that a run has seen.
## Incremental · Append + Deduped
**Shown in stream lists as** `incremental · dedup`. **Needs** a cursor and a primary key.
Reads only the rows whose cursor has moved, and keeps the latest version of each primary key.
- **Edits at the source:** the table holds the latest version.
- **Deletes at the source:** the row stays, for the same reason as **Incremental · Append**.
- **History:** the table holds one row per key, not every version.
## What a mode needs
| Need | Modes that ask for it | Where it comes from |
|---|---|---|
| A cursor | **Incremental · Append**, **Incremental · Append + Deduped** | You pick a column. The picker marks the source's suggestion. A source can also decide the cursor itself, and then the cell reads **source-defined** |
| A primary key | **Full refresh · Overwrite + Deduped**, **Incremental · Append + Deduped** | You pick a column. A source can declare the key itself, and then the cell shows it |
The picker refuses to save while a stream that is on lacks what its mode needs, and says how many streams are missing a cursor or a key.
## Modes a source offers
Each table offers only the modes the source supports for it. A table that supports only one shows it as fixed text. When a source reports nothing about a table, all five are offered.
A mode applies from the next sync after you save it. Changing a mode does not run anything by itself.
## Older connections
Before sanda offered all five modes, a stream was either "full refresh" or "incremental", and sanda chose how to write it. A stream saved that way keeps doing what it did: a full refresh overwrote, and an incremental deduplicated when the table had a key (its own or the source's) and appended otherwise. Open **Edit streams** to move it to one of the five.
## What is the same in every mode
- The table lands as `raw.__`, whichever mode reads it.
- Every landed table carries a few loading columns whose names start with an underscore. They stay out of the semantic fluid.
- A run that replaces a table is followed by a short window, until sanda has attached the new copy, in which a question over the table returns a message that it has no rows attached right now. See [Troubleshoot connections](https://docs.sanda-os.com.au/connections/troubleshooting#a-table-has-no-rows-attached-right-now).
- Each successful run is metered as one run, however many rows it moves.
:::links
- [Sync modes](https://docs.sanda-os.com.au/connections/sync-modes): How to choose a mode for each table.
- [Choose what to sync](https://docs.sanda-os.com.au/connections/choose-streams): The picker, cursors and primary keys.
:::
---
# Filters and date ranges
> The operators and named date periods behind every filter in sanda, how a condition is read, how date windows begin and end, and which time zone they use.
Wherever you narrow a question in sanda, you are writing a condition: a field, a comparison and one or more values. This page is the reference for how those conditions read, the operators available, the named date periods, and how a date window is worked out. It ends with the filters on a search index, which use a smaller vocabulary of their own.
## Where filters appear
| Where | What it is |
|---|---|
| **query semantic fluid**, on the semantic fluid page | The **only rows where** strip under your selection, with **add a condition** |
| A report block | The conditions in the block's builder |
| A whole report | The **filters** button on the report canvas, which opens **filters · every block** |
| A push | **Which rows** in the push editor, which narrows the records a push sends values for. See [create a push](https://docs.sanda-os.com.au/push/create-a-push#which-rows) |
| cherry and connected assistants | The `conditions` of a query, with a named period in `relative` |
| A search index | Filters on the columns you kept beside the text. See [search filters](#search-filters) |
A **condition** narrows one question and is thrown away with it. A **named filter** on your [semantic fluid](https://docs.sanda-os.com.au/semantic-fluid) is different: a condition your team wrote down and signed off, so that "approved" means one thing in every question. You use a named filter by name. A condition is something you are doing right now.
## How a condition works
A condition has three parts.
- **The field.** A dimension by its name, or any column of a table on your semantic map. The picker searches both, so you do not have to turn a column into a dimension first.
- **The operator.** What to compare. The operators offered depend on the field's type.
- **The values.** How many depends on the operator.
sanda reads a field's type from the dimension's kind or the column's data type: **text**, **number**, **true or false**, or **date and time**. A date field is offered the date operators, so you are never asked to match a timestamp as if it were text.
Every condition on a question must hold at once. There is no "or" between conditions. To match either of two values, give one condition both values.
A condition takes up to 20 values, each up to 200 characters. A question takes up to 20 conditions, and a report holds up to 20 filters.
## Operators
| Operator | Reads as | Field types | Values |
| --- | --- | --- | --- |
| `equals` | is | text, number, true or false, date and time | One or more |
| `not_equals` | is not | text, number, true or false, date and time | One or more |
| `contains` | contains | text | One or more |
| `not_contains` | does not contain | text | One or more |
| `starts_with` | starts with | text | One or more |
| `ends_with` | ends with | text | One or more |
| `gt` | is more than | number | One |
| `gte` | is at least | number | One |
| `lt` | is less than | number | One |
| `lte` | is at most | number | One |
| `between` | is between | number | Two (from and to) |
| `in_range` | is within | date and time | Two (from and to) |
| `before` | is before | date and time | One |
| `on_or_after` | is on or after | date and time | One |
| `set` | has a value | text, number, true or false, date and time | None |
| `not_set` | is empty | text, number, true or false, date and time | None |
A few behaviours worth knowing:
- **is** with one value matches it. With several values it means "is one of them". In the console, separate several with commas.
- **is not** and **does not contain** keep rows where the field is empty. A row whose status was never set is not a row whose status is "paid", so excluding "paid" does not silently drop the blanks.
- **contains**, **starts with** and **ends with** ignore capitals. With several values they match any one of them. A `%` or `_` in your value is matched as itself.
- **is between** includes both ends.
- **is** on a date matches one exact moment. To match a whole day, use **is within** with that day as both ends.
- **has a value** and **is empty** take no value.
## Date ranges
### Both ends, and which one is excluded
A date window is **half-open**: it includes its start and excludes its end. July is from 1 July up to, but not including, 1 August. That means the whole of 31 July is in and the first moment of 1 August is out, whether the column holds a date, a timestamp or a timestamp with a time zone.
The alternative, an inclusive pair of dates, is a common date bug in reporting. A window ending "31 July" against a timestamp means "up to midnight at the start of the 31st", and a day of trading disappears without a word. sanda avoids it by design.
You do not need to think about this when you pick dates yourself. The **to** date you enter is the last day that is in the window, and the label beside the boxes says "both days included". sanda stores the next midnight behind the scenes.
### Named periods
Pick a period by name instead of typing dates. They appear as buttons under **is within**.
| Period | Reads as | Kind |
| --- | --- | --- |
| `today` | today | A single day |
| `yesterday` | yesterday | A single day |
| `tomorrow` | tomorrow | A single day |
| `last_7_days` | last 7 days | Rolling, to the last full day or month |
| `last_30_days` | last 30 days | Rolling, to the last full day or month |
| `last_90_days` | last 90 days | Rolling, to the last full day or month |
| `last_3_months` | last 3 months | Rolling, to the last full day or month |
| `last_6_months` | last 6 months | Rolling, to the last full day or month |
| `last_12_months` | last 12 months | Rolling, to the last full day or month |
| `this_week` | this week | The current period |
| `this_month` | this month | The current period |
| `this_quarter` | this quarter | The current period |
| `this_year` | this year | The current period |
| `last_week` | last week | The last full period |
| `last_month` | last month | The last full period |
| `last_quarter` | last quarter | The last full period |
| `last_year` | last year | The last full period |
| `next_7_days` | next 7 days | Ahead of today |
| `next_30_days` | next 30 days | Ahead of today |
The **Kind** column groups the periods: a single day, rolling, the current period, the last full period, and ahead of today. The conventions below say exactly where each period starts and ends:
- **`today`, `yesterday` and `tomorrow` are single days.**
- **The day-based rolling periods count whole days back from the start of today.** `last_7_days`, `last_30_days` and `last_90_days` end yesterday. `last_7_days` is the seven complete days ending yesterday. Today is a part-day, and including it would make a daily total look as if it fell off a cliff every morning.
- **The months periods are whole calendar months, ending with last month.** `last_6_months` asked in September is March to August: six months, each of them finished. `last_90_days` is the rolling alternative when you want a quarter's trading counted back from yesterday.
- **`this_month` is the whole calendar month**, not month to date. For history the two are the same. For anything dated forward, such as a due date, month to date would hide the rows you opened the question to find.
- **Weeks start on Monday.**
- **Quarters and years are calendar quarters and years**, January to March and so on. There is no financial year period. For 1 July to 30 June, press **pick the dates myself** and enter them.
- **`next_7_days` and `next_30_days` start tomorrow.**
Here is what each period means on Wednesday 30 September 2026:
| Period | Covers |
|---|---|
| `today` | 30 September |
| `yesterday` | 29 September |
| `tomorrow` | 1 October |
| `last_7_days` | 23 to 29 September |
| `last_30_days` | 31 August to 29 September |
| `last_90_days` | 2 July to 29 September |
| `this_week` | Monday 28 September to Sunday 4 October |
| `last_week` | 21 to 27 September |
| `this_month` | 1 to 30 September |
| `last_month` | 1 to 31 August |
| `last_3_months` | 1 June to 31 August |
| `last_6_months` | 1 March to 31 August |
| `last_12_months` | 1 September 2025 to 31 August 2026 |
| `this_quarter` | 1 July to 30 September |
| `last_quarter` | 1 April to 30 June |
| `this_year` | 1 January to 31 December 2026 |
| `last_year` | 1 January to 31 December 2025 |
| `next_7_days` | 1 to 7 October |
| `next_30_days` | 1 to 30 October |
The console prints the resolved days under the period buttons, so you can check a window before it goes into a board pack.
### Time zones
Which time zone a named period uses depends on where you set it.
- **In the console.** When you press a period in the semantic fluid workbench or a block's conditions, sanda works out the dates at that moment, in your browser's time zone, at your local midnight. The dates it worked out are what is sent.
- **In a report's filters.** A period set in **filters · every block** is kept as a period, not as dates, so the window moves with the calendar. sanda works it out on its own servers, in UTC, each time a block runs.
- **In a push.** A period under **Which rows** is kept as a period too, so a push that sends "last 30 days" every day sends a window that moves with it. sanda works it out in UTC each time the push runs.
- **Through cherry and connected assistants.** There is no browser, so a named period is worked out in UTC, and the result carries the dates it used. Ask cherry which dates it used if you need to see them.
UTC midnight is 10am in Sydney during standard time and 11am during daylight saving. So a window worked out in UTC starts and ends at that hour, and before it each morning, UTC is still on yesterday's date. "Yesterday", asked at 8am in Sydney, is the day before the Sydney yesterday. When the exact days matter, such as a month-end close, give explicit dates.
:::caution A block's own period is fixed at the moment you pick it
A period chosen in a single block's own conditions keeps the dates it meant when you picked it. A period in **filters · every block** stays a period and moves with the calendar. To have a whole report follow the calendar, set the window in its filters.
:::
### In reports
A condition on the **filters · every block** pane applies to every block. It replaces a block's own condition on the same field, so "last 12 months" there widens a block that said "last month". You can also type a sentence into **say it**, such as "only paid invoices" or "last 12 months", and sanda turns it into one condition for you. That uses AI, so it counts towards your [daily AI limit](https://docs.sanda-os.com.au/cherry/models-and-usage).
## Search filters
A [search index](https://docs.sanda-os.com.au/search) has filters of its own, on the columns you kept beside the text when you created it. They are a smaller vocabulary than conditions, and every filter must match.
| Column holds | Comparisons in the console | Name used by assistants |
|---|---|---|
| Text | **is**, **is not**, **contains** | `eq`, `neq`, `contains` |
| A number, or a date and time | **is**, **is not**, **greater than**, **at least**, **less than**, **at most** | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` |
| True or false | **is**, **is not** | `eq`, `neq` |
- A search takes up to eight filters, each value up to 200 characters.
- On text, **is** matches the whole value exactly, capitals included, and **contains** ignores capitals. A text column cannot be compared with greater than or less than.
- Dates and times in the console are entered in your own time zone.
- Connected assistants can also use `in`, a list of up to 50 values.
See [search your records](https://docs.sanda-os.com.au/search/use-search).
:::links
- [Search your records](https://docs.sanda-os.com.au/search/use-search): Filters on a search index.
- [Build reports with cherry](https://docs.sanda-os.com.au/reports/build-with-cherry): Setting a window on a whole report.
- [Ask questions](https://docs.sanda-os.com.au/cherry/ask): Naming a period in a question to cherry.
:::
---
# Sheet formulas
> Every operator, function, error and number format a report sheet formula understands, with worked examples on a small table.
Formulas add a column of your own to a [table block](https://docs.sanda-os.com.au/reports/sheets) on a report. The language is Excel's, without the parts that need a grid: the operators are Excel's, the function names are Excel's and each means what it means in Excel. That is what lets an [exported workbook](https://docs.sanda-os.com.au/reports/sheets#take-it-to-excel) carry the same formula and keep calculating.
## The sample table
The examples on this page run against this table, which is what a table block might return. The `net revenue` column is a sum, and `ikura` has no revenue.
| product | net revenue | cost |
|---|---|---|
| chai | 1,200 | 700 |
| aniseed syrup | 800 | 900 |
| chang | 2,000 | 500 |
| ikura | (empty) | 0 |
Where an example gives one result, it is the result on the `chai` row unless the text says otherwise.
## Syntax
A formula is an expression. A leading `=` is allowed and ignored, and spaces are ignored everywhere except inside text.
| Piece | Written as | Notes |
|---|---|---|
| Column | `[net revenue]` | The column's name in square brackets, spaces and all |
| Number | `12`, `3.5`, `.5`, `1e3` | No thousands separators. Write a negative with a minus sign: `-4`. |
| Text | `"chai"` | In double quotes. Write a double quote inside text by doubling it: `"say ""hi"""`. |
| Truth value | `TRUE`, `FALSE` | Shown as TRUE and FALSE |
| Function | `ROUND([cost], 1)` | A name, then arguments in brackets separated by commas |
| Grouping | `([a] + [b]) * 2` | Brackets change the order of work |
Function names are not case-sensitive: `sum`, `Sum` and `SUM` are the same function. A formula can be up to 500 characters. The editor says so under the box and will not save a longer one.
## References
A **column reference** in square brackets is the column's name as its header shows it. It is matched without regard to case, so `[Net Revenue]` finds `net revenue`.
A reference means one of two things depending on where it sits:
- **On its own or inside an operator or ordinary function**, it is **this row's value**. `[net revenue] - [cost]` is 500 on the `chai` row.
- **As a bare argument of `SUM`, `AVERAGE`, `MIN`, `MAX`, `COUNT` or `COUNTA`**, it is **the whole column**. `[net revenue] / SUM([net revenue])` is each row's share of the total: 0.3 on the `chai` row.
Only a *bare* `[column]` inside an aggregate reads the whole column. Any other argument is worked out for the current row. `SUM([net revenue] * 2)` is therefore not the total of a doubled column; on the `chai` row it is 2,400, the doubled value of that one row. To total a computed value, give it a column of its own and then sum that: add `revenue x2` as a formula, then use `SUM([revenue x2])`.
Some more rules:
- A formula can read the columns before it, including formula columns you added earlier. It cannot read itself or a column added after it.
- Whole-column functions cover the rows the block returned, no more. If the table says **of more**, the block's row limit cut the result short and a total covers only the rows you can see.
- A formula keeps reading a column you hide after writing it. The formula editor only accepts columns that are showing.
- A reference to a column that does not exist makes the whole formula unreadable: its column shows `#NAME?`, and the sentence under the table says which column is missing.
### Empty cells and text that looks like numbers
Formulas treat the data the way Excel does:
- An **empty cell is 0 in arithmetic**, so `[net revenue] - [cost]` on the `ikura` row is 0, not an error. An empty cell compared with text is the empty text.
- **Text that reads as a number is a number.** The warehouse often sends numbers as text (`2,000` or `0.00`). `"12" + 1` is 13, and `[net revenue] = 0` is true for a value that arrives as `"0.00"`. Two texts still compare as text, so the codes `"007"` and `"7"` are different.
- **Other text in arithmetic is an error.** `[product] * 2` is `#VALUE!`.
- **TRUE is 1 and FALSE is 0** in arithmetic.
## Operators
From the one that binds tightest to the one that binds loosest. Operators at the same level work from left to right, and brackets override the order.
| Operator | What it does | Example | Result |
|---|---|---|---|
| `%` after a value | Divides by 100 | `50%` | 0.5 |
| `-` or `+` before a value | Sign | `-[cost]` | -700 |
| `^` | Power | `2^3` | 8 |
| `*` and `/` | Multiply and divide | `[cost] * 10%` | 70 |
| `+` and `-` | Add and subtract | `[net revenue] - [cost]` | 500 |
| `&` | Joins text | `[product] & " (" & [cost] & ")"` | chai (700) |
| `=` `<>` `<` `>` `<=` `>=` | Compare, giving TRUE or FALSE | `[net revenue] > 1000` | TRUE |
As in Excel, a sign binds tighter than a power, so `-2^2` is 4 and not -4.
### Comparing
- Two numbers compare as numbers, even when one or both arrived as text.
- A number and an empty cell compare as if the empty cell were 0, so `IF([net revenue] = 0, ...)` is true on a row with no revenue. That is the guard people write against dividing by zero, and it works.
- Two texts compare **without regard to case**: `[product] = "CHAI"` is TRUE on the `chai` row, and `<` and `>` compare alphabetically.
## Functions
| Function | Arguments | Reads a whole column |
| --- | --- | --- |
| `SUM` | One or more | Yes |
| `AVERAGE` | One or more | Yes |
| `MIN` | One or more | Yes |
| `MAX` | One or more | Yes |
| `COUNT` | One or more | Yes |
| `COUNTA` | One or more | Yes |
| `ROUND` | 1 to 2 | No |
| `ROUNDUP` | 1 to 2 | No |
| `ROUNDDOWN` | 1 to 2 | No |
| `ABS` | 1 | No |
| `INT` | 1 | No |
| `MOD` | 2 | No |
| `POWER` | 2 | No |
| `SQRT` | 1 | No |
| `IF` | 2 to 3 | No |
| `IFERROR` | 2 | No |
| `AND` | One or more | No |
| `OR` | One or more | No |
| `NOT` | 1 | No |
| `ISBLANK` | 1 | No |
| `ISNUMBER` | 1 | No |
| `LEN` | 1 | No |
| `UPPER` | 1 | No |
| `LOWER` | 1 | No |
| `TRIM` | 1 | No |
| `CONCAT` | One or more | No |
| `TEXT` | 1 to 2 | No |
| `VALUE` | 1 | No |
| `LEFT` | 1 to 2 | No |
| `RIGHT` | 1 to 2 | No |
Column summaries: `sum`, `avg`, `min`, `max`, `count`. Formats: `auto`, `integer`, `number`, `percent`, `currency`, `compact`, `text`.
The functions marked as reading a whole column are the six aggregates. Functions that take any number of arguments (`SUM`, `AVERAGE`, `MIN`, `MAX`, `COUNT`, `COUNTA`, `AND`, `OR` and `CONCAT`) take at least one. The sections below say what each does.
### Whole-column functions
| Function | Returns | Example | Result |
|---|---|---|---|
| `SUM(x, ...)` | The total of the numbers | `SUM([net revenue])` | 4,000 |
| `AVERAGE(x, ...)` | The mean of the numbers. `#DIV/0!` when there are none. | `AVERAGE([net revenue])` | 1,333.33 |
| `MIN(x, ...)` | The smallest number, or empty when there are none | `MIN([net revenue])` | 800 |
| `MAX(x, ...)` | The largest number, or empty when there are none | `MAX([net revenue])` | 2,000 |
| `COUNT(x, ...)` | How many numbers | `COUNT([product])` | 0 |
| `COUNTA(x, ...)` | How many values are not empty | `COUNTA([product])` | 4 |
These read numbers only and skip anything else, so `AVERAGE([net revenue])` divides 4,000 by 3: the empty `ikura` cell is left out, not counted as 0. Give an aggregate two columns and it reads both in full: `SUM([net revenue], [cost])` is 6,100. A value that is not a bare column, such as `SUM([cost], 10)`, is worked out for the row and counted once.
### Rounding and arithmetic
| Function | Returns | Example | Result |
|---|---|---|---|
| `ROUND(x, [places])` | `x` rounded, halves away from zero. `places` defaults to 0 and may be negative. | `ROUND(1.005, 2)` | 1.01 |
| `ROUNDUP(x, [places])` | `x` rounded away from zero | `ROUNDUP([net revenue] / 7, 1)` | 171.5 |
| `ROUNDDOWN(x, [places])` | `x` rounded towards zero | `ROUNDDOWN(-2.349, 2)` | -2.34 |
| `ABS(x)` | The size of `x`, without its sign | `ABS([cost] - [net revenue])` | 500 |
| `INT(x)` | `x` rounded down to a whole number | `INT(-1.5)` | -2 |
| `MOD(x, d)` | The remainder of `x` divided by `d`, with the sign of `d`. `#DIV/0!` when `d` is 0. | `MOD(-7, 3)` | 2 |
| `POWER(x, y)` | `x` to the power `y` | `POWER(2, 10)` | 1,024 |
| `SQRT(x)` | The square root. `#NUM!` for a negative. | `SQRT(16)` | 4 |
`ROUND` works on the number as written, so it agrees with Excel where plain arithmetic would not: `ROUND(1.005, 2)` is 1.01, `ROUND(-2.5, 0)` is -3 and `ROUND(1234.5, -2)` is 1,200. `ROUND(x)` with no second argument rounds to a whole number.
### Logic
| Function | Returns | Example | Result |
|---|---|---|---|
| `IF(test, then, [else])` | `then` when `test` is true, otherwise `else`, or FALSE when `else` is left out | `IF([net revenue] > [cost], "profit", "loss")` | profit |
| `IFERROR(x, fallback)` | `x`, unless working it out gives a formula error, then `fallback` | `IFERROR([cost] / [net revenue], "n/a")` | 0.5833 (and n/a on `ikura`) |
| `AND(x, ...)` | TRUE when every argument is true | `AND([net revenue] > 1000, [cost] < 800)` | TRUE |
| `OR(x, ...)` | TRUE when any argument is true | `OR([net revenue] > 1500, [cost] > 800)` | FALSE |
| `NOT(x)` | TRUE when `x` is false | `NOT([net revenue] > 1000)` | FALSE |
| `ISBLANK(x)` | TRUE when `x` is empty | `ISBLANK([net revenue])` | TRUE on `ikura` |
| `ISNUMBER(x)` | TRUE when `x` is a number, or text that reads as one | `ISNUMBER("12")` | TRUE |
A test counts as true when it is TRUE, a number other than 0, or text that is not the number 0. It is false when it is FALSE, 0 or empty.
`IF` only works out the branch it returns, so `IF([net revenue] = 0, 0, [cost] / [net revenue])` never divides by zero. `IFERROR` rescues `#DIV/0!`, `#VALUE!` and `#NUM!`. It cannot rescue `#NAME?`, because a formula that cannot be read is never run.
### Text
| Function | Returns | Example | Result |
|---|---|---|---|
| `LEN(x)` | The number of characters | `LEN([product])` | 4 |
| `UPPER(x)` | Upper case | `UPPER([product])` | CHAI |
| `LOWER(x)` | Lower case | `LOWER("Chai")` | chai |
| `TRIM(x)` | Removes leading and trailing spaces and squeezes runs of spaces to one | `TRIM(" a b ")` | a b |
| `CONCAT(x, ...)` | The arguments joined as text | `CONCAT([product], ": ", [cost])` | chai: 700 |
| `LEFT(x, [n])` | The first `n` characters. `n` defaults to 1. | `LEFT([product], 3)` | cha |
| `RIGHT(x, [n])` | The last `n` characters. `n` defaults to 1. | `RIGHT([product], 2)` | ai |
| `TEXT(x, format)` | A number written as text with thousands separators | `TEXT([net revenue], "0.00")` | 1,200.00 |
| `VALUE(x)` | Text read as a number. `#VALUE!` when it is not one. | `VALUE("1,200")` | 1,200 |
`TEXT` reads only how many zeros follow the decimal point in its format. `"0.00"` means two decimals and `"0"` means none. It always adds thousands separators, and it does not understand percent, currency or date formats. Text that is not a number comes back unchanged. To show a value as a percent in a column, set the column's [format](#number-formats) instead.
## Errors
A formula that cannot be worked out on a row shows an error word in that cell, and the rest of the table carries on. Errors are Excel's words.
| Cell shows | Meaning | Typical causes |
|---|---|---|
| `#DIV/0!` | Divided by zero | `[cost] / [net revenue]` on a row with no revenue. `MOD(x, 0)`. `AVERAGE` of no numbers. |
| `#VALUE!` | A text value where a number was needed | `[product] * 2`. `VALUE("abc")`. |
| `#NUM!` | Not a real number | `SQRT` of a negative. A result too large to hold. |
| `#NAME?` | The formula cannot be read at all | An unknown column or function, a missing closing bracket or quote, or the wrong number of arguments |
A `#NAME?` fills the whole column, marks the column's header with a red `!`, and the first problem is written under the table after the column's name. Some of the sentences you may see:
| Under the table | What to fix |
|---|---|
| there is no column called “units” | Spell the column as its header shows it |
| FOO is not a function this sheet knows | Check the function's name against the list above |
| SUM is missing its closing ) | Close the bracket |
| a column reference needs a closing ] | Close the square bracket |
| a text value needs a closing quote | Close the double quote |
| the formula ends too soon | Something is missing after an operator |
`IF` and `IFERROR` are how you handle the first three. A `#NAME?` is always a mistake in the formula itself.
## Number formats
A format changes how a value is **shown** and never the value itself, so a formula that reads a formatted column sees the underlying number. Use `ROUND` to change the value. A column's format is set from its menu on the table.
| Format | Value | Shown as |
|---|---|---|
| **automatic** | 1234.567 | 1,234.57 |
| **whole number** | 1234.567 | 1,235 |
| **number** | 1234.567 | 1,234.57 |
| **percent** | 0.256 | 25.6% |
| **currency** | 1234.5 | $1,234.50 |
| **compact (1.2M)** | 12500 | 12.5K |
| **compact (1.2M)** | 1250000 | 1.3M |
| **text** | 1234.567 | 1234.567 |
Detail worth knowing:
- **percent** multiplies by 100, so it expects a ratio. 0.256 shows as 25.6%, and 25.6 shows as 2,560.0%.
- **compact** leaves numbers under 10,000 in full (9,500 stays 9,500) and shortens larger ones to K, M or B.
- **automatic** leaves text alone and keeps a value such as `00123` as text, since a code with leading zeros is not a number.
- Numeric formats right-align the column.
- TRUE and FALSE always show as TRUE and FALSE.
## In the exported workbook
When you [download a table as xlsx](https://docs.sanda-os.com.au/reports/sheets#take-it-to-excel), each formula column is written as a formula in Excel's own grid. `[net revenue] - [cost]` on the first data row of the sample table becomes `=(B2-C2)`, and `[net revenue] / SUM([net revenue])` becomes `=(B2/SUM(B$2:B$5))`, with the whole-column reference turned into a fixed range over every data row. Excel recalculates it, so a formula column keeps working after you change a number in the file. A column you hid on the report is written into the workbook as a hidden column, so a formula that reads it keeps its cell reference and keeps calculating.
## Limits
| Limit | Value |
|---|---|
| Formula columns on one table block, hidden ones included | 12 |
| Length of one formula | 500 characters |
| Length of a column's name | 60 characters |
| Rows a formula can see | The rows the block returned, at most 200 |
What a formula cannot do: refer to cells such as `A1`, look things up (`VLOOKUP`), add conditionally (`SUMIF`), work with dates, or keep a running total. A formula sees only the rows of its own table. If you need a shaped number in more than one report, define it as a [metric](https://docs.sanda-os.com.au/semantic-fluid/metrics) so every report gets it.
## Worked examples
**Margin, and margin as a percent of revenue.** Add two columns. The second reads the first.
```text
margin [net revenue] - [cost]
margin % IF([net revenue] = 0, 0, [margin] / [net revenue])
```
Set `margin %` to the **percent** format. On the `chai` row it shows 41.7%, and on `ikura`, which has no revenue, 0.0%.
**Each row's share of the total.**
```text
share [net revenue] / SUM([net revenue])
```
With the **percent** format the sample table reads 30.0%, 20.0%, 50.0% and 0.0%.
**Flag the rows that need a look.**
```text
status IF([cost] > [net revenue], "loss", "ok")
```
Text results are left-aligned like any other text column.
**A tidy label.**
```text
label UPPER(LEFT([product], 3)) & " · " & TEXT([cost], "0")
```
On the `chai` row this reads `CHA · 700`.
**A ratio that never shows an error.**
```text
cost ratio IFERROR([cost] / [net revenue], 0)
```
On `ikura` this is 0 instead of `#DIV/0!`.
---
# SQL reference
> The SQL dialect, schemas and naming, the columns sanda adds, what sanda sql allows and refuses, limits, and worked example queries.
Your warehouse speaks PostgreSQL, so everything here is ordinary PostgreSQL. This page is the lookup for the parts that are sanda's: which schemas exist and what they are called, the columns sanda adds, exactly what each place you can type SQL accepts and refuses, the limits, and examples that run against a sanda warehouse as it is laid out.
For a walk-through of the shell itself, see [sanda sql](https://docs.sanda-os.com.au/warehouse/sql).
## Dialect
The dialect is PostgreSQL. Common table expressions, window functions, `filter (where ...)`, `distinct on`, lateral joins, arrays, JSON operators and the built-in date and text functions all work as in PostgreSQL, subject to what the role you run as may do.
Identifiers fold to lower case unless you double-quote them. A landed column with capital letters has to be quoted, for example `"CustomerId"`. Names sanda creates are all lower case.
## Where SQL runs
There are five places to run or store SQL, and they are not the same. What differs is who may use them, which login they run as, and how strictly the text is checked.
| Where | Who | Runs as | What it accepts |
|---|---|---|---|
| [sanda sql](https://docs.sanda-os.com.au/warehouse/sql) | Owners and admins | The builder role | One statement of any kind the role may run. Read-only unless you switch to read-write. |
| A view or materialized view definition in [Modelling](https://docs.sanda-os.com.au/modelling/views), and its preview | Owners and admins | The builder role | One `select` under the strict rules below. |
| A procedure body in [Modelling](https://docs.sanda-os.com.au/modelling/procedures) | Owners and admins | The builder role | SQL or `plpgsql`, not scanned. The role's permissions are the boundary. |
| cherry and connected assistants, when they write SQL with `run_sql` | Anyone using cherry, and credentials with the `sql:run` scope | A login that can read only the tables the semantic fluid publishes in `mapping` | One read-only `select`, under the strict rules below and a few more. They reach for it only when a question cannot be composed from the fluid's metrics and dimensions, and they must give a reason. |
| An assistant over MCP using `execute_sql` | A credential with the `sql:write` scope | The builder role | The same as sanda sql, with a reason recorded for every statement. |
Reports, the semantic workbench and the OData feed do not run SQL that you write. sanda composes their queries from the definitions on the fluid.
## Schemas and naming
### Schemas
| Schema | What it holds | You can |
|---|---|---|
| `raw` | Landing tables: every stream a connection syncs, and every CSV upload. | Read, in sanda sql and in view definitions. |
| `derived` | Your views, materialized views and procedures, and tables your own SQL creates. | Read and, in read-write mode, write. |
| `mapping` | Views the semantic fluid publishes for cherry and reports, plus any tables built there (for example the sanda sample dataset copied into a warehouse). | Read the tables. The published views are not readable by the builder role. |
| `search` | sanda search's indexes, one table per service. | Not from the shell. |
| `meta` | sanda's own bookkeeping. | Not from the shell. |
The managed sync also keeps a working area in the same database. It is not listed in the Explorer and you cannot read it.
### Names
| What | Name | Example |
|---|---|---|
| A table landed by a connection | `raw.__` | `raw.xero__invoices` |
| A table from a CSV upload | `raw.csv__` | `raw.csv__sales` |
| Your view, materialized view or procedure | `derived.` | `derived.monthly_revenue` |
| A table published on the fluid | `mapping.` | `mapping.invoices` |
**``** is the connection's name in lower case, with every run of characters other than `a` to `z` and `0` to `9` replaced by one underscore, leading and trailing underscores removed, and cut to 40 characters. If nothing is left, sanda uses the connector's type. Two underscores separate it from the stream.
**`` for a CSV** is the name you give the upload, in lower case, without a `.csv`, `.tsv` or `.txt` ending, with the same replacement rule, cut to 40 characters, and prefixed `t_` if it starts with a digit. The CSV's columns become lower snake case with no leading or trailing underscore, at most 58 characters, with `c_` added in front of a name that starts with a digit, `_2`, `_3` added to repeats, and `column_` for an empty heading. Types are worked out from the whole file: `bigint` for whole numbers, `numeric` for decimals, `boolean` for `true` and `false`, `date` for `2026-09-30`, `timestamptz` for ISO timestamps, and `text` for anything else. Empty cells are nulls.
**`` in `derived`** is lower case letters, digits and underscores, starts with a letter, is at most 63 characters, does not start with `pg_`, and cannot match a table in `raw` or a view in `mapping`.
Always write the schema, as in `raw.csv__sales`.
## Columns sanda adds
Some columns in your tables are not from your source. Their names start with an underscore, and sanda treats every one as bookkeeping.
| Column | Where | What it holds |
|---|---|---|
| `_becca_loaded_at` | Every `raw.csv__*` table | `timestamptz`, defaulting to the moment the row was loaded. |
| Underscore-prefixed sync columns | Every table a connection lands | Loading details such as a record identifier, the time the row was extracted and a metadata record. Their names start with an underscore and one of two reserved prefixes. |
A CSV heading never keeps a leading underscore, so a heading called `_becca_loaded_at` lands as `becca_loaded_at` and cannot collide.
Because they are bookkeeping, these columns are left out wherever a person or cherry would see business data:
- the [Explorer](https://docs.sanda-os.com.au/warehouse/explorer) counts them but does not list or preview them;
- the views the fluid publishes in `mapping` do not name them, so cherry and reports cannot read them;
- the fluid's map and the context cherry is given do not include them.
They are real columns, so `select *` in sanda sql returns them, and you can use them. The prefixes are reserved and are matched as prefixes, so a column of your own called `_notes` is untouched.
## What sanda sql allows and refuses
### The builder role
Every statement in sanda sql runs as the warehouse's builder role. The role, not a list of banned words, is what limits a statement.
| The role can | The role cannot |
|---|---|
| Read `raw.*`, `derived.*` and the tables in `mapping.*` | Read a view the semantic fluid publishes in `mapping` |
| Read the database catalogue: `information_schema` and `pg_catalog` | Read `search.*` or `meta.*`, or the managed sync's working area |
| Create, change and drop objects in `derived` (read-write mode) | Write anywhere else, or reach another database or role |
Anything outside those permissions fails with a permission error that says what the role may do.
### Statements
- **One statement per press.** sanda reads the text the way PostgreSQL does. A semicolon inside a string, a double-quoted name, dollar-quoted text or a comment is text, and a semicolon between two statements is a boundary. A single trailing semicolon, with comments after it, is fine.
- **Comments and dollar quoting are allowed.** `--` and `/* ... */` comments (including nested block comments), `E'...'` strings and `$tag$...$tag$` quoting all work in sanda sql. They are refused in view definitions.
- **At most 20,000 characters.**
- **Read-only or read-write.** In read-only mode the statement runs in a read-only transaction and PostgreSQL refuses anything that would change data, including a writable `with` and a function with side effects. In read-write mode it runs as written and commits at once.
- **Statements that start with `select`, `with`, `values` or `table`** are read through a cursor, so the row limit is enforced at the source. Anything else runs directly. If it returns rows, as `explain` does, they are shown under the same cap. Otherwise the result is its command tag and the number of rows it touched.
### Rows, values and secrets
- At most **500** rows come back. The **rows** menu chooses 50, 200 or 500.
- A value is cut at **300** characters, with an ellipsis. Dates arrive as ISO strings and JSON as JSON text.
- A result column whose **name** looks like a secret is returned with empty values, and the result lists it as withheld. The test reads the column's name in the result as words, split at underscores, hyphens, digits and changes of case (so `userPassword2` is `user`, `password` and `2`), and ignores case. These words count wherever they appear, even run together with others: `password`, `passwd`, `passphrase`, `passcode`, `passkey`, `token`, `credential`, `apikey`, `privatekey`, `secretkey`, `socialsecurity`, `taxfile` and `cardnumber`. `secret` counts as a word or at the end of one, so `clientsecret` is withheld and `secretary` is not. These count only as a whole word: `pass`, `ssn`, `tfn`, `iban`, `swift`, `cvv`, `pan` and `routing`. Pairs of words count too: `api_key`, `private_key`, `secret_key`, `social_security`, `tax_file` and `card_number`, however they are separated. So `pass_hash` and `auth_token` are withheld, and `passenger_count` and `compass_heading` are not. Giving a column a different name in your `select` shows it, so use that only where the match is a false one.
- The Explorer's preview applies the same test to column names.
### Time
The shell uses your warehouse's statement ceiling, which is set under **Limits** and is at most 60 seconds on Basic, 60 seconds on Standard and 5 minutes on Enterprise. It never waits longer than 55 seconds, so a longer ceiling is lowered to 55 seconds. A statement that runs past it is stopped.
## Rules for view definitions and assistant queries
A view or materialized view definition, its preview, and every query cherry or an assistant writes with `run_sql` must pass one strict check first. sanda does this as a courtesy, so you get a sentence instead of a database error, and the login's permissions still stand behind it.
| Rule | Detail |
|---|---|
| One statement | Begins with `select` or `with`. A trailing semicolon is removed. A semicolon anywhere else is refused, even inside a string. |
| No comments | `--`, `/*` and `*/` are refused anywhere in the text, even inside a string. |
| Ordinary quoting only | Dollar-quoted text, `E'...'` strings and `U&'...'` or `U&"..."` are refused. Write an ordinary string and double the quote to include one. |
| At most 20,000 characters | Longer text is refused. |
| Reads named tables | Use schema-qualified names of `raw`, `derived` or tables in `mapping`. A definition that reads a view the fluid publishes is rejected after it is created, and rolled back. |
| Forbidden words | See below. |
The check reads the statement as tokens. A forbidden word inside a string literal or inside a double-quoted name is fine: `where status = 'CREATED'` and `select "comment"` both pass.
**Refused as bare words:**
- statements and clauses: `insert`, `update`, `delete`, `drop`, `alter`, `create`, `grant`, `revoke`, `truncate`, `copy`, `merge`, `call`, `do`, `vacuum`, `analyze`, `reindex`, `cluster`, `lock`, `listen`, `notify`, `prepare`, `execute`, `comment`, `refresh`, `set`, `reset`, `begin`, `commit`, `rollback`, `savepoint`, `security` and `into`;
- the catalogue: `pg_catalog`, `information_schema`, `pg_class`, `pg_attribute`, `pg_namespace`, `pg_tables`, `pg_views`, `pg_roles`, `pg_user`, `pg_shadow`, `pg_authid`, `pg_settings`, `pg_database`, `pg_proc`, `pg_stat_activity` and `pg_stat_statements`. These are refused even inside double quotes;
- families of server, file, lock and replication functions, matched by prefix: names starting `pg_sleep`, `pg_terminate_backend`, `pg_cancel_backend`, `set_config`, `dblink`, `pg_notify`, `pg_reload_conf`, `pg_rotate_logfile`, `pg_switch_wal`, `pg_backend_pid`, `pg_export_snapshot`, `pg_stat_file`, `pg_advisory_`, `pg_try_advisory_`, `pg_read_`, `pg_ls_`, `lo_`, `pg_logical_`, `pg_replication_`, `pg_create_`, `pg_drop_`, `txid_` and `pg_current_`, and the XML export functions `query_to_xml`, `table_to_xml`, `cursor_to_xml`, `schema_to_xml` and `database_to_xml`.
Because the word list matches whole words, a column named `comment` has to be written `"comment"`, and a bare column called `lo_score` is refused until it is quoted.
Queries written by cherry and assistants face a few more rules. A statement is refused if any column or alias in it looks like a secret, if it uses a whole row as a value, or if it renames columns by position. Those queries read only the fluid's published tables.
## Limits
Plan limits are on the [Limits](https://docs.sanda-os.com.au/reference/limits) page. These are the limits of the SQL itself.
| Limit | Value |
|---|---|
| Statement in sanda sql, or in a view definition, or a procedure body | 20,000 characters |
| Rows returned by sanda sql | 500 (menu: 50, 200 or 500) |
| Longest value shown in a result | 300 characters |
| Longest statement in sanda sql | The warehouse's ceiling, or 55 seconds, whichever is less |
| Rows in a view preview, and in the Explorer's data preview | 50 |
| Columns in the Explorer's data preview | 60 |
| A view's preview | Stops after 20 seconds |
| Build or procedure call started from the page | 55 seconds |
| Build or procedure call in a task | Up to 14 minutes for the whole run |
| Object names | 63 characters |
| Columns in a drawn view | 200, with at most 8 joins |
| Key columns on a materialized view | 8 |
| Procedure arguments | 16, of the types `text`, `integer`, `bigint`, `numeric`, `boolean`, `date`, `timestamptz`, `timestamp`, `interval`, `jsonb` and `uuid` |
| Steps in a task | 20 |
| Concurrent queries in a warehouse | 60 on Basic, 60 on Standard, 300 on Enterprise, at most |
| Warehouse statement ceiling | 60 seconds on Basic, 60 seconds on Standard, 5 minutes on Enterprise, at most |
## Error messages
What you may read in sanda sql, and what to do.
| Message | What it means | What to do |
|---|---|---|
| Type a statement first. | The editor is empty. | Type one. |
| That statement is longer than 20,000 characters. | Over the length limit. | Split the work, or move it to a [procedure](https://docs.sanda-os.com.au/modelling/procedures). |
| One statement at a time... | The text has a second statement after a semicolon. | Run them one after another. |
| The shell is in read-only mode, and that statement would change something. | A change was attempted in read-only mode. | Switch to **Read-write** if you mean it. |
| Permission denied... The builder role reads `raw.*`, `derived.*` and mapping's tables, and writes only `derived.*`. | The statement touched something the role may not. | Read from a table the role can reach, or write in `derived`. |
| Something depends on it... | A `drop` or change would break another object. | Drop or redefine the dependents first. |
| That ran past its time ceiling and was stopped. | The statement outlasted the ceiling. | Narrow it, add a filter, or materialize the result in a view. |
| PostgreSQL's own message, ending "(at character N)" | A syntax or name error. `N` is the position in the text. | Fix the text at that position. |
| This workspace's warehouse is suspended... | The workspace's budget was reached. | See [Usage, budgets and suspension](https://docs.sanda-os.com.au/warehouse/usage-and-suspension). |
## Examples
These examples use a CSV upload called `sales`, with the headings `Sale date`, `Region`, `Product`, `Units` and `Revenue`, and dates written `2026-09-30`. sanda lands it as `raw.csv__sales` with the columns `sale_date` (a `date`), `region`, `product`, `units` and `revenue`. Replace the names with your own from the [Explorer](https://docs.sanda-os.com.au/warehouse/explorer).
### Find your way around
```sql title="Every table and view the builder role can read"
select table_schema, table_name, table_type
from information_schema.tables
where table_schema in ('raw', 'derived', 'mapping')
order by table_schema, table_name;
```
```sql title="The columns of one table"
select ordinal_position, column_name, data_type, is_nullable
from information_schema.columns
where table_schema = 'raw'
and table_name = 'csv__sales'
order by ordinal_position;
```
```sql title="The bookkeeping columns on a landed table"
select column_name, data_type
from information_schema.columns
where table_schema = 'raw'
and table_name = 'xero__invoices'
and left(column_name, 1) = '_'
order by ordinal_position;
```
```sql title="How big are the objects you have built"
select c.relname as name,
c.relkind,
pg_size_pretty(pg_total_relation_size(c.oid)) as size
from pg_class c
where c.relnamespace = 'derived'::regnamespace
and c.relkind in ('r', 'm')
order by pg_total_relation_size(c.oid) desc;
```
### Look at the data
```sql title="A quick look"
select * from raw.csv__sales limit 20;
```
```sql title="An exact row count"
select count(*) as row_count from raw.csv__sales;
```
```sql title="When a CSV table was last loaded"
select max(_becca_loaded_at) as last_loaded from raw.csv__sales;
```
```sql title="Duplicates on what should be a unique key"
select sale_date, region, product, count(*) as copies
from raw.csv__sales
group by 1, 2, 3
having count(*) > 1
order by copies desc;
```
### Answer a question
```sql title="Revenue by month"
select date_trunc('month', sale_date)::date as month,
sum(revenue) as revenue
from raw.csv__sales
group by 1
order by 1;
```
```sql title="Month-on-month change"
with monthly as (
select date_trunc('month', sale_date)::date as month,
sum(revenue) as revenue
from raw.csv__sales
group by 1
)
select month,
revenue,
revenue - lag(revenue) over (order by month) as change,
round(
100.0 * (revenue - lag(revenue) over (order by month))
/ nullif(lag(revenue) over (order by month), 0),
1
) as change_pct
from monthly
order by month;
```
```sql title="Top ten products in the last 90 days"
select product, sum(units) as units, sum(revenue) as revenue
from raw.csv__sales
where sale_date >= current_date - interval '90 days'
group by product
order by revenue desc
limit 10;
```
```sql title="See the plan before you run something heavy"
explain
select region, sum(revenue)
from raw.csv__sales
group by region;
```
### Write in derived (read-write mode)
```sql title="Keep a rollup as a table"
create table derived.monthly_revenue as
select date_trunc('month', sale_date)::date as month,
region,
sum(revenue) as revenue
from raw.csv__sales
group by 1, 2;
```
```sql title="Remove it again"
drop table derived.monthly_revenue;
```
A table made this way has no version history and no schedule. For something sanda versions and rebuilds, define a view in [Modelling](https://docs.sanda-os.com.au/modelling/views). Its definition must satisfy the strict rules above: one `select`, no semicolon, no comments.
```sql title="A definition that passes the strict rules"
select date_trunc('month', s.sale_date)::date as month,
s.region,
sum(s.revenue) as revenue
from raw.csv__sales s
group by 1, 2
```
:::links
- [sanda sql](https://docs.sanda-os.com.au/warehouse/sql): The shell, step by step.
- [Views and modelled tables](https://docs.sanda-os.com.au/modelling/views): Turn a query into a view sanda keeps.
- [Procedures](https://docs.sanda-os.com.au/modelling/procedures): Multi-step SQL with arguments.
- [Limits](https://docs.sanda-os.com.au/reference/limits): Every plan limit by edition.
:::
---
# Keyboard shortcuts
> Every keyboard shortcut in the console, covering the cherry pane, message boxes, the suggestion queue, the map, the report canvas and the SQL shell.
The console has a small set of shortcuts, and this page lists all of them. Use ⌘ on a Mac and Ctrl on Windows and Linux. Wherever a table says ⌘, Ctrl works the same way, except for clicking on the semantic map.
## Everywhere
| Keys | What it does |
|---|---|
| ⌘ J | Opens and closes the cherry pane, from any page. On **Overview** and **cherry chat**, where cherry is the page, it moves the cursor to the message box instead. |
| Esc | Closes the window, menu or panel you have open: dialogs, the AI usage card in the top bar, the cherry model menu, and the navigation drawer on a phone. |
| Tab and Shift Tab | Move between controls. In an open dialog or the phone navigation drawer, focus stays inside until you close it. |
| ← → | Move between tabs when a tab is focused. Wraps from the last tab to the first. |
| Home and End | Jump to the first or last tab when a tab is focused. |
## cherry
These apply to the message box in the cherry pane, on **cherry chat**, on the **Overview** page, and to the box beside a report on the reports portal.
| Keys | What it does |
|---|---|
| Enter | Sends your message. |
| Shift Enter | Starts a new line without sending. |
| Esc | Closes the cherry pane beside a report on the reports portal. |
## The suggestion queue
Learning proposes tables, relationships, metrics and dimensions, and they wait in the queue headed "N suggestions waiting on you" on the semantic map. These keys work while the queue is open and your cursor is not in a text field. See [review suggestions](https://docs.sanda-os.com.au/semantic-fluid/review).
| Keys | What it does |
|---|---|
| ↓ or J | Moves to the next suggestion. |
| ↑ or K | Moves to the previous suggestion. |
| Enter | Accepts the highlighted suggestion. |
| Delete or Backspace | Rejects the highlighted suggestion. |
## The semantic map
These work on the map when your cursor is not in a text field. See [the map](https://docs.sanda-os.com.au/semantic-fluid/the-map).
| Keys | What it does |
|---|---|
| Click a table card | Selects it. |
| Shift-click a table card, or ⌘-click on a Mac | Adds the table to the selection, or takes it out. Ctrl-click does not do this. |
| Delete or Backspace | With tables selected, asks whether to remove them from the map. Nothing is deleted: they drop off the map, and their relationships and metrics are retired. Press **remove** to confirm. |
| Esc | Steps back one layer at a time: the confirmation, then an open dialog or panel, then a selected relationship, then the table selection. |
## The report canvas
These work on a report when your cursor is not in a text field. See [the canvas](https://docs.sanda-os.com.au/reports/canvas).
| Keys | What it does |
|---|---|
| ⌘ Z | Undoes your last change. It goes back within the current session only. |
| ⌘ Shift Z | Redoes it. |
| ⌘ P | Exports the report as a PDF, by opening the print dialog with the page laid out flat. |
| Hold Space and drag | Pans the canvas. |
| Scroll | Pans the canvas. |
| ⌘ and scroll | Zooms the canvas. |
| Delete or Backspace | Deletes the selected block. ⌘ Z brings it back. |
| Esc | Deselects the block and closes any open panel or menu: colours, history, the more menu, and the block builder. |
Inside a report's text fields, these keys apply:
| Where | Keys | What it does |
|---|---|---|
| The report's name | Enter | Saves the name. |
| A block's title | Enter or Esc | Saves the title, or cancels the edit. |
| A new block you drew on the page | Enter | Sends what you typed to cherry to build the block. |
| A new block you drew on the page | Shift Enter | Starts a new line. |
| A new block you drew on the page | Esc | Closes the prompt, or cancels the new block. |
| A block's comment box | Enter | Adds the comment. |
| A block's comment box | Shift Enter | Starts a new line. |
| A sheet's formula column | Enter | Saves the formula, once it is valid. |
| A sheet's open menu or editor | Esc | Closes it. |
## The SQL shell
| Keys | What it does |
|---|---|
| ⌘ Enter | Runs the statement in the editor. The shell runs one statement at a time. |
## Related pages
:::links
- [A tour of the console](https://docs.sanda-os.com.au/get-started/console-tour): The cherry pane and the rest of the console.
- [Sheet formulas](https://docs.sanda-os.com.au/reference/sheet-formulas): The formulas a report sheet accepts.
:::
---
# Release notes
> What changed in sanda each month, in plain words: new features, behaviour changes, visible fixes and pricing changes.
Each month has its own page, newest first. Within a month, changes are listed by date range with the latest at the top, and grouped by product area. They are what a customer can notice: features, changes in behaviour and fixes to bugs you could see. Fixes start with "Fixed:".
Prices and limits change over time, so a release note that mentions a price change never quotes the figure. Read current prices and limits on [editions](https://docs.sanda-os.com.au/billing/editions).
:::links
- [October 2026](https://docs.sanda-os.com.au/releases/2026-10): Fixes from a security review: two-step sign-in, the SQL shell, connected apps, private reports and cherry's workspace memory.
- [September 2026](https://docs.sanda-os.com.au/releases/2026-09): Reports become canvases you can share and publish. sanda search, modelling, the OData feed, cherry across the console, three editions with a trial credit, and the reports portal.
- [August 2026](https://docs.sanda-os.com.au/releases/2026-08): The first month. Sign-in, warehouses and connections, the semantic fluid as a map, the learning dataset and chat over your own definitions.
:::
---
# October 2026
> becca is now sanda, numbers from different tables in one answer, Push to Salesforce, a data region at signup, and fixes to sign-in, the SQL shell and reports.
## 6 October
**sanda**
- becca is now sanda. The name, the logo (three salmon circles) and the colours have changed across the website, the console, the reports portal, emails and these docs. Nothing else has: your workspace, warehouse, connections and credentials stay exactly as they were.
- sanda has new addresses, on sanda-os.com.au: the website is sanda-os.com.au, the console is console.sanda-os.com.au, and the MCP endpoint and OData feeds are under `https://console.sanda-os.com.au/api/`. The reports portal and these docs moved the same way. Every old becca-os.com address, and the same names on sanda-os.com, takes you to its new home, so bookmarks and links in old emails still work. A Claude connection or OData feed you set up on console.becca-os.com keeps working where it is; point it at the new address when you next touch it. You sign in again once, on the new console. Email moved too: sign-in links and reports now come from noreply@sanda-os.com.au, so if your IT team allows senders by name, add that one. Write to us at hello@sanda-os.com.au. See [connect Claude](https://docs.sanda-os.com.au/mcp/connect-claude) and [OData](https://docs.sanda-os.com.au/odata).
- Text is darker and easier to read everywhere, small print and labels most of all, and the dark panels are now a warm charcoal.
- sanda is now run by Sanda Technologies Pty Ltd (ABN 63 702 995 015, ACN 702 995 015). The terms and the privacy policy name the company from 6 October 2026, and the footer shows both numbers. The next time you open the console it asks you to read and accept the privacy policy again. Account holders are told by email before the updated terms apply to a workspace that signed up earlier. See [privacy](https://docs.sanda-os.com.au/workspace/privacy).
- Report colours have a new default, **salmon**, to match. A workspace that never picked its colours now draws reports in salmon. If you chose **cherry** before, it is still there and still yours. See [workspace settings](https://docs.sanda-os.com.au/workspace).
- An authenticator app set up from today lists your two-step code under sanda. One you set up earlier keeps the label it had, and its codes still work.
**Semantic fluid**
- Numbers measured on different tables sit in one answer everywhere sanda reads the semantic fluid, the OData feed and pushes included. Put `revenue`, summed over order lines, beside `order count`, counted over orders, and each order is counted once however many lines it has. A table counted this way needs its key columns set. See [query the fluid](https://docs.sanda-os.com.au/semantic-fluid/query-the-fluid#numbers-from-different-tables).
- A metric built from metrics on other tables runs wherever a relationship joins the tables. Each part is worked out on its own table, so `{revenue} / nullif({order count}, 0)` is revenue per order. See [metrics](https://docs.sanda-os.com.au/semantic-fluid/metrics#build-on-other-metrics).
- A metric's own condition applies to that number alone. Two metrics whose conditions disagree sit side by side instead of being refused, and a metric that counts only paid invoices no longer narrows the invoice count beside it. A metric built from others keeps each part's own condition. See [metrics](https://docs.sanda-os.com.au/semantic-fluid/metrics#filters-on-a-metric).
- A row whose relationship finds no match is kept, with `null` on the other side, rather than left out, so a breakdown still adds up to its total.
- Every question now goes straight to your warehouse. One asked again within a few seconds is answered from the first answer, and a report with many blocks fills in from the top. See [blocks](https://docs.sanda-os.com.au/reports/blocks#what-every-data-block-stores).
## 1 to 3 October
**Push**
- Push is in the console, under **Activate · Push**, once it is switched on for your console. Until then it isn't listed. Connect a Salesforce org by signing in to Salesforce as the user sanda writes as, then create a push: the object, how each record is found, and the sanda value for each field. Each run sends only the records whose values changed, and a run where nothing changed makes no Salesforce call. See [Push](https://docs.sanda-os.com.au/push).
- A new push opens with **Preview the next run**: how many records a run would send, the first twenty, and the rows that can't be sent as they stand. Saving sends nothing, so you can check the preview before the first run. Anyone who can open the console can preview, read-only members included.
- A push's **Runs** says what each run did. Open one to see the records it couldn't deliver, with Salesforce's own reason and sanda's hint beside it. See [runs and refusals](https://docs.sanda-os.com.au/push/runs).
- Push to Salesforce is included on Standard and Enterprise, and in a trial. A workspace holds up to 1 on Standard and any number on Enterprise. The editions table, **Settings · Plan & budget** and the pricing page list it. See [editions](https://docs.sanda-os.com.au/billing/editions).
**Workspace and security**
- Settings is now six tabs across the top of the page instead of one long page: **Workspace**, **Plan & budget**, **Team**, **cherry**, **Security** and **Integrations**. Report colours have moved into **Workspace**, and closing or deleting the workspace is at the foot of that tab. The tab you are on is kept in the address, so a link or a reload opens the same one. See [workspace settings](https://docs.sanda-os.com.au/workspace).
- Signup asks where your data is held. Under **Data region**, you choose the region a new workspace lives in: its warehouse is created there, and loading and modelling run there. Workspaces can be created in Australia (Sydney). See [quickstart](https://docs.sanda-os.com.au/get-started/quickstart).
- A workspace stays in the region it was created in. **Settings · Workspace** now shows its **Data region** rather than offering a choice. See [workspace settings](https://docs.sanda-os.com.au/workspace#workspace).
- The global data plane, which was announced and never opened, is no longer listed on the pricing page or in Settings.
- Codes tried when turning off two-step sign-in now count toward the same hourly limit as codes tried at sign-in. See [two-step verification](https://docs.sanda-os.com.au/workspace/two-step).
- Connecting an assistant to a workspace that requires two-step sign-in now asks you to have it set up, even when you are signed in to a different workspace. See [Connect Claude](https://docs.sanda-os.com.au/mcp/connect-claude).
- A portal handoff link opened from another website no longer signs anyone in. See [the reports portal](https://docs.sanda-os.com.au/reports/portal).
- A closed workspace narrows every credential to the reading it already had. A credential that could only add rows or send reports can no longer do either. See [permissions and approvals](https://docs.sanda-os.com.au/mcp/permissions).
- Fixed: the SQL shell could be made to run a second statement after the first, so a read-only statement could commit a change. It now runs exactly one statement, as it says.
- Fixed: duplicating a private report, or saving it as a template, made a copy everyone in the workspace could open. The copy is now private. See [share a report](https://docs.sanda-os.com.au/reports/sharing).
- Fixed: the Report schedules page listed the schedules of private reports to people who could not open them.
- Fixed: a report's shared conversation could be reached from cherry's thread list by the person who typed first in it, after they lost access to the report.
**cherry**
- Only owners and admins add to or remove the workspace's memory. Everyone else keeps personal notes. See [memory and conversations](https://docs.sanda-os.com.au/cherry/memory-and-history).
- cherry no longer runs SQL of its own when the **sql** switch is off under **Settings · cherry**, even when a model asks to.
---
# September 2026
> Reports become canvases, sanda search, the modelling layer, the OData feed, cherry across the console, three editions and the reports portal.
## 28 to 30 September
**Documentation**
- docs.sanda-os.com.au is open: guides for every part of sanda, a page for each connector, and the MCP and OData references.
- Every console page has a **Docs** link in the top bar that opens the guide to that page. The product page links each chapter to its guide, the pricing page links the billing guide, and the site footer has a Docs column.
**Plans and billing**
- Owners and admins move between Basic and Standard themselves: **Move to Standard** or **Move to Basic** in **Settings · Plan & budget**, then confirm. The new edition's features and limits apply at once. A move up bills Standard's fee for the month you moved in, and a move down bills Basic's from the next month. A move to Basic waits until the workspace fits it, and the message says what to delete or trim. Leaving a trial, Enterprise and agreed fees are still asked for. See [editions](https://docs.sanda-os.com.au/billing/editions).
- A change of edition now applies the new edition's query and connection limits to every warehouse straight away, not at the warehouse's next measurement.
- sanda search holds an index to your edition's row limit when an agent points it at a different table, and counts a view that has no row estimate instead of passing it on a sample.
- Enterprise adds up to 10 times faster analytical queries from an in-memory columnar engine, a 99.99% uptime SLA that includes maintenance, read replicas for reports and dashboards, and continuous backup with point-in-time recovery. See [editions](https://docs.sanda-os.com.au/billing/editions).
- MCP and agents are included on Basic, so every edition can connect Claude and other assistants. sanda search stays on Standard and Enterprise.
- Standard holds up to 3 warehouses of 100 GB each and 5 search services. A warehouse's compute can burst to 4 vCPU on Basic and 8 on Standard. Enterprise runs on a plane of its own with its own storage and compute rates. See [editions](https://docs.sanda-os.com.au/billing/editions).
- A trial now freezes when it reaches its end date, as the terms and the pricing page say, as well as when its credit is spent. The banner, the top bar pill (**Trial · ended**), the email and every refusal name which of the two it was. A paid edition lifts either.
- Messages that send you to the budget now name its real place, **Settings · Plan & budget · Budget & alerts**, and the budget emails link straight to it. The **Remind me at** hint says the suspension notice goes whenever alert emails are on.
- All AI is billed under a daily limit. Every AI call sanda makes on your workspace's data counts against it, including sanda's own learning work and calls from console buttons and from MCP. Owners and admins set the limit in the **Daily AI limit** field under **Settings · Plan & budget**, up to a ceiling. sanda support can set a higher one on request. The top bar meter shows today's spend, and its card explains what counts and shows the month.
- A trial is A$10 of free credit. Everything an invoice would meter since signup draws it down at the invoice's own prices, and nothing is billed. When the credit is spent, the workspace freezes: every warehouse is suspended and sanda makes no AI calls for it. Your data, connections and reports are kept. See [trial](https://docs.sanda-os.com.au/billing/trial).
- The AI meter updates while a reply is running, and a reply's cost chip shows the billed amount rather than the supplier's, learning passes included.
**Workspace and security**
- An expired sign-in link says so and asks for a new one. If you asked for the link from the same browser, your address is already filled in, so one press of **Email me a sign-in link** sends it.
- The console's left rail names your role as **Settings · Team** does ("Read-only", not "viewer"), and the **Admin** hint lists everything that stays with the owner.
- Region copy says what is true today: every workspace runs in Australia (Sydney).
- Owners can delete a workspace immediately. **Delete workspace now** in **Settings · Close or delete** asks for the workspace's short name, then closes the workspace and deletes its warehouse databases, its sync connections and its data. Anyone whose only workspace it was is removed too, so their address can sign up again. You land on signup, or on sign-in if your address belongs to another workspace.
- Fixed: deleting a workspace failed if it had ever accepted the terms or been invoiced. Deleting now works, and sanda rehearses every delete before it removes anything.
- Signup is one short form: name, work email, company, an optional role, the terms box and **Start free trial**. It no longer asks about your systems. The console asks when it matters.
- The privacy policy and terms are rebuilt from an audit of what sanda does, including exactly what is sent to AI models. A closed workspace stays read-only for 90 days before deletion, up from 30, and the governing law moves from New South Wales to Victoria.
- **Settings · Team** can invite a **Reader**, who sees the reports portal and nothing else.
**cherry**
- Setup is warehouse first. A checklist reads the workspace as five steps in order: a warehouse, a source, learn from my data, review the suggestions and a first report. A CSV loaded into the warehouse counts as a source, and the source step is done once rows have landed, not when a connection exists. A first sync that has not started, is running, failed or finished empty says so, with a link to the connection.
- Setup runs one step per press. Nothing runs by itself: a first sync lands tables and stops, and learn counts as done only after a pass has run. cherry does not chain steps or move you to another page beside a checklist that already offers the button. It no longer opens with a recap or a correction of an earlier reply.
- The first report step asks for a name and a purpose, then builds a whole report: KPIs, trends, breakdowns and an analysis.
- **Learn from my data** in the checklist takes you to the semantic fluid first, with cherry beside it, so you watch the pass land on the map. Once suggestions are accepted, cherry offers to tidy the map, and the map highlights its arrange button with a tip when relationships appear.
- The conversation docks beside the page it opens instead of disappearing mid-reply. When the checklist scrolls out of view, a compact pinned copy sits at the top of the thread. You can unpin it.
- Fixed: the text cursor in the Overview's question box was clipped by the box's rounded corner.
- Fixed: after **Learn from my data**, every question over the learned tables was refused with "permission denied for schema raw". Learn now publishes each table it proposes before writing the proposal, and a map already written on landing tables moves onto its published views the next time it is read.
**Connections**
- **Replace** on a loaded CSV works when something is built on the table. sanda swaps the rows in place, so your views keep working, and rebuilds the semantic map's own view when the new file drops or retypes a column it reads. If one of your own views reads such a column, the load is refused and names the view. See [load a CSV](https://docs.sanda-os.com.au/connections/upload-a-csv).
- **Next sync** is written in the schedule's own time zone, which the line under it names, so it agrees with the schedule wherever you open the page.
- Only owners and admins can add a connection, as only they can register, run or delete one.
- Zuora and BigCommerce are offered by arrangement: their connectors are no longer maintained. Connector notes no longer mention controls sanda does not have.
- A running sync says how far it has got and how fast. The **Status** tab shows rows this run, rows per second, data this run and running time. A queued run reads "Queued for 3s · waiting for the sync engine", and the page checks as soon as it opens. When a run settles, a note says how it ended, such as "Sync finished: 1,990,700 rows in 6m 33s". Every successful run in the Timeline shows its average rows per second. A syncing row in the Connections list shows rows so far and rows per second, and cherry and MCP report the same progress.
- A sync reads as a run: queued, launching the connector, syncing each stream, landed. Fixed: a running sync showed 0 rows and 0 B for minutes before jumping to its final count.
**MCP and OData**
- The consent screen says what the grant you are giving can do. It says "can write nothing" only when no ticked permission writes, and it names each write permission you tick. It points to **Settings · Integrations** to disconnect.
- The workspace brief and context resources need the **fluid:read** permission, like the tools that read the fluid. A connection without it is listed no resources.
- Approval requests are emailed to every owner and admin, since either can decide them, and follow the workspace's alert email switch.
- The OData feed declares `time` columns as a time of day, and `interval` and `money` columns as text, so they arrive with their values instead of empty. See [OData reference](https://docs.sanda-os.com.au/odata/reference).
**Semantic fluid**
- Fixed: editing a metric in the metrics panel, or renaming one that others refer to, reset its **Shown as** to a plain number. Every field the panel does not show now keeps its value.
- The list view's forms fill in from what is stored. The pencil on a row opens its current values, and typing a name that already exists does the same, so re-saving a table no longer blanks its grain, key, event time and notes or resets its row count. The bin on a table retires it, as removing it from the map does, instead of deleting it.
- A new metric's or category's name keeps a hyphen you type: `on-time delivery %` stays `on-time delivery %`.
- A metric built from metrics on other tables is refused by name in the workbench and `run_query`, instead of running against one table. The metrics panel's examples and **Insert** chips offer only metrics on the same table.
- Learn from one connection reads that connection's own tables, so a small connection beside a large one is no longer told nothing has landed.
- Fixed: the Ask the fluid card slid over the top bar as you scrolled, and its lists handed the scroll wheel to the page at their ends.
**sanda search**
- The consent screen says plainly what is sent to be embedded: the text of the columns a service indexes, and the text of every search. Owners who agreed to the earlier wording will be asked to read the new one.
- The personal details warning looks only at the columns that are sent, and suggests keeping such a column as a filter.
- A table only suggested for the semantic map, or retired from it, can no longer be a service's source. The picker offers the tables the map draws.
**Warehouse and modelling**
- Columns such as `passenger_count` or `compass_heading` are no longer withheld as secrets: sanda matches the words in a column's name, not letters inside them.
- Dropping a view that another view reads is refused before anything changes, so the view stays on the semantic fluid.
- The visual builder can draw on tables in `mapping`.
- The compute limit hint says what happens at the limit: sanda narrows queries and pauses loading, and the hours used are billed.
- Pipeline copy says task and step, as the console does, instead of graph.
**Reports portal**
- The reports portal launches for the people a workspace builds reports for. They sign in, see the reports published to them, open one as a page and ask cherry about the numbers. They never see the console. See [reports portal](https://docs.sanda-os.com.au/reports/portal).
- An owner or admin publishes from a **Publish** pane on the report canvas, to everyone or to chosen people, with a group and an optional featured flag. Later canvas edits stay on the canvas until someone presses **Update**, and the pane says when a report has changed since it was published and how many people have read it.
- A reader can be given data rules: a field and the values that reader may see. Rules apply to every portal query, both report blocks and cherry's answers, and cannot be replaced by a report's own filters. A block that cannot reach a rule's field is refused, never shown whole. Portal cherry can search the fluid, describe tables and run queries, and nothing else. An owner or admin can view the portal as any member.
- The **Reports** page in the console has two tabs, **Reports** and **Portal**. The **Portal** tab sets the portal's name and welcome text, whether it is open, whether cherry is on, what a reader with no rules sees, the order of published reports and each reader's rules.
- The portal home opens with a greeting, your workspace's welcome text and a box for cherry with first questions as chips, with the report library underneath. Asking takes you to a full page with your conversations down the left, and a **Conversations** link in the top bar returns to the list. On a phone the list is a drawer. A published report fills the page and sits centred, with a zoom bar for zooming out, zooming in, returning to the width and fitting the whole report on screen.
**Reports**
- A kpi's comparison with the previous period shows on shared links and the reports portal, as it does on the canvas.
- A metric's **Shown as** and **Unit** decide how its figures read on kpi, bar and line blocks, everywhere a report goes, the email included.
- An Excel download keeps hidden sheet columns as hidden columns, so formulas that read them stay live.
- A formula is refused, with the reason, past 500 characters, and the **formula** button says so when a sheet already holds 12.
- A logo over 146 KB is refused with its size. Changing a theme's preset keeps the logo.
- Read-only members are no longer offered Duplicate, new report from this, or save as template.
- cherry can change a report that already exists. It can find a report, read its filters and blocks, set report-wide filters, change blocks in place and remove blocks. "Last 7 days" becomes one filter that every block runs with, including blocks added later. Before saving, sanda checks each chart against the fluid with the filters applied. If any block could not run, nothing changes and that block is named. On an open report the change arrives live as one edit you can undo, and any other report is saved as a new version.
- The report toolbar sits in its own row above the board instead of floating over blocks, and every control has the same size and border.
## 24 and 25 September
**Workspace and security**
- Emailed sign-in links now need a click. Opening a link shows a button on the sign-in page, and you are signed in when you press it. This stops mail scanners spending your link and closes a cross-site sign-in trick.
- A session ends after 14 days unused. Settings lists your sessions, with **Sign out everywhere else**.
- Two-step sign-in works with an authenticator app and comes with recovery codes. An owner can require it for the whole workspace. See [workspace security](https://docs.sanda-os.com.au/workspace/security).
- Workspace credentials are one list. Owners and admins can revoke any credential, and an owner can hand the workspace to an admin.
- An approval queue for risky agent actions is available and is off by default.
- Invitations no longer reveal whether an address already has a workspace. Pages shared by link return rows only, never SQL or database errors. Some actions that any member could take are now for owners and admins: the SQL preview in the view builder, and creating a schedule that emails a private report to any address.
- Fixed: a single question could spend about twice the daily AI limit, because the limit was compared in the wrong currency.
- Fixed: after signing in, sanda now goes to the page you were heading for, so approving Claude's connection survives the sign-in.
- Pages load faster. The first download is about a third of its former size.
**Reports**
- A delivered report email is drawn like the canvas: a masthead, then each block in its place, in your report's colours. KPI cards show the change on the previous period, tables keep their sheet formatting, and on phones the blocks stack. Each email attaches the report as a PDF and a PNG. If the files would make the message too large, sanda leaves the PNG off first, then the PDF.
- Reports can also be delivered to Slack or to a webhook, and you can limit recipients to your own email domains. Delivery schedules keep their local time across daylight saving changes.
- Fixed: a delivery that failed to start stayed queued for ever, and blocks with the same id could reuse each other's results.
- Fixed in sheets: `=` now works on warehouse numbers, `ROUND` matches Excel, and a CSV export can no longer run a synced value as a formula.
- Fixed: filter chips showed the UTC date instead of yours. The hourly schedule now shows its next run.
**Semantic fluid**
- **Wipe** clears the semantic fluid (tables, datasets, metrics, relationships, suggestions and the map layout) so a workspace can start fresh. It is for owners and admins, behind a typed confirmation, in a danger zone at the bottom of the page. It is a hard delete, so re-adding a table does not bring back its old name.
- **Accept all** on suggestions is faster. It accepts six at a time, and a failure puts back only the rows that were refused.
**Modelling**
- The modelling assistant always drafts. It takes the most reasonable reading of your columns, states its assumptions, and asks any follow-up question alongside the code instead of in place of it. It can switch a new view between a view and a materialized view when you ask.
**Connections**
- Fixed: once a table was on the semantic fluid, a sync could fail on its next run with an error about dependent objects. sanda now lands each stream so that views on the fluid survive a full refresh and a schema change. **Check status** repairs any view still reading the sync engine's own tables, says what it found, and restarts the run when it removes the cause. A connection page shows which views still read those tables, and a repair that cannot run says why instead of failing silently.
- Connector forms show only the fields that need an answer up front. PostgreSQL asks for host, database, username and password, and the update method defaults to a user-defined cursor, because change data capture needs a replication slot and a publication in your database. See [connections](https://docs.sanda-os.com.au/connections).
- The stream picker offers the five sync modes and defaults new streams to Full refresh · Overwrite. It shows a cursor and primary key only where the mode uses them, will not save a stream that is missing a required one, and has a bar that sets the mode, cursor or key on every visible stream at once. Streams saved with the older values keep syncing exactly as before.
**cherry**
- cherry arrives as one data agent across the console. It is a pane docked on every page (⌘ J or the top bar toggle), a full page at **cherry chat**, and a thread that follows you around. In one thread you can ask a question of your data, define a relationship, build a view, create a report and be walked through setting up the workspace. See [cherry](https://docs.sanda-os.com.au/cherry).
- Threads can be pinned, renamed, archived, deleted and searched. Chat fills the window, shows your question straight away with a typing indicator, and draws tables and charts in replies.
- **Settings · cherry** chooses how cherry acts. In **Ask first**, each change appears as a card with its SQL and a 50 row preview for you to **Confirm** or **Decline**. In **Autonomous**, changes run at once. Dropping a view, retiring a table and deleting a definition always ask. The same section has a model library (up to five models on at a time, with a default), permissions per capability, and memory.
- A reply shows all of cherry's words, with the steps between them as one quiet line saying what they were, how many and how long. The Overview opens on cherry.
- cherry and MCP can schedule and send reports. Six MCP tools list, create, change, send, pause and delete deliveries, behind a new privileged permission. They are email only, for owners and admins, and follow the workspace's recipient domains and the schedule grid. A delivery given a timezone keeps its local time across daylight saving.
- **Ask cherry to learn** reads one source at a time with progress in the step label, runs one relationship pass over the whole map, keeps the full result as a card in the thread, then refreshes the map and opens the suggestions. It draws the joins your learning dataset declares, considers the whole map rather than only the tables it just proposed, profiles map tables it missed, and never proposes a join you rejected.
- Several changes of one kind show as one grouped card, with **Confirm all** and **Decline all**. The active thread survives closing the pane and reloading, and the pane header has a labelled **new chat** button.
- All cherry usage counts against your daily AI limit. The allowance card lists today's spend by model.
**Plans and billing**
- sanda now sells three editions: Basic, Standard and Enterprise. They bill storage and compute at the same rates and differ in fee, room and features. MCP and agents and sanda search are not part of Basic, and included support is part of Enterprise. Enterprise is **Talk to us**: its pricing card opens the enquiry form. A trial has every feature and is priced and limited as Standard. Prices and limits are on [editions](https://docs.sanda-os.com.au/billing/editions).
- Every feature stays in the navigation, tagged with the edition that has it. A page your edition does not include says so and links to **Settings · Plan & budget** and to the pricing page. Settings shows the fee as it will bill and all three editions, and an owner can ask to move, which files a support ticket. A warehouse above its edition's storage limit pauses loading until it is back under, and a workspace above its search service limit keeps its services but cannot add more.
- Faults and billing questions are supported at no charge on every edition. How-to help is included on Enterprise and billed by the hour below it.
- Each pricing card lists only what its edition includes.
- Fixed: compute readings taken close together were ignored, so an active workspace read "not metered" and its compute was missing from invoices. Every reading now gets a rate.
- Fixed: deleting a warehouse and provisioning a new one made the Overview read $0.00 for the month, and dropped the deleted warehouse's month from billing. That usage is kept, and the card is now **Account usage and spend**, which sums every warehouse including deleted ones.
- Tax invoices carry a validated ABN and have an A4 print layout. Draft and void invoices are no longer emailed.
## 21 to 23 September
**Reports**
- You can build a block by hand. A drawn box asks which door first. **pick from the fluid** opens a builder pane on the right with the fluid browser, aggregate and grain pickers and a condition builder, and the drawn box previews the result live. sanda suggests the mark and title from your selection: one measure becomes a KPI, a measure over time becomes a line, otherwise a bar, and no measure a table. **ask cherry** keeps the sentence box, with written analysis, a visual or "cherry decides" as chips. Edit a chart later from its toolbar's **edit**.
- Fixed: reports and KPIs could not be built in a workspace whose fluid had tables but no connection, such as uploaded or hand-added tables. It said there was no data behind it. The map now decides whether there is data.
- Date filters gain last 3 months and last 6 months, which resolve to whole calendar months ending with the previous month.
- You can deliver a report by email on a schedule. Choose the report, the recipients, a cadence and a closing note, then send now, pause or remove it from the Report schedules page. A KPI in the email shows the figure, the change on the previous period and its distance from any target.
- A KPI or line block can hold a target. A KPI states the gap in words, and a line draws a dashed rule. The target appears on shared pages and in delivered emails, in neutral terms: a distance and a direction, never a colour.
**Pipeline**
- **Monitor · Pipeline** puts every stage's timing on one canvas, in four columns: Sync, Transform, Fluid and Deliver. A wire is drawn only where a real link exists, such as a task that runs after a sync or a delivery that reads the fluid. Press a card to open an inspector with the same schedule picker everywhere: sync time and **Sync now** for a connection, schedule, run-after-sync, **Run now** and **Pause** for a task, and cadence and recipients for a delivery. A next 24 hours list shows every upcoming run. The Sync times and Report schedules entries leave the navigation, and tasks now sit inside Modelling.
**Semantic fluid**
- **Learn from my data** draws the joins your schema declares, then finds more by checking that the values on one side exist on the other. The table picker now infers joins from column names, as MCP imports already did, and no longer stores the first id-looking column as the key of every table, which was wrong for bridge tables.
- Fixed: **Learn from my data** returned nothing when a workspace held both the learning dataset and its own tables of the same shape. It now compares only against your active model.
**Plans and billing**
- A budget is a ceiling. When the month's metered spend reaches the budget you set under **Settings · Plan & budget**, sanda suspends every warehouse in the workspace. Nothing can be queried, loaded, rebuilt or indexed, by the console, by agents over MCP, by the OData feed or by syncs. Your data is kept. The suspension never lifts on its own: an owner or admin resumes it once spend is back under the budget or the budget has been raised or removed.
- Prices, invoices, budgets and allowances are in Australian dollars.
**Workspace and security**
- Everyone who signs in to the console reads and accepts the current privacy policy before anything else. It is shown once per person, and again whenever the policy changes. Machine credentials are not asked.
**MCP and agents**
- An owner or admin can open a table for intake, and an agent with the intake permission can then append rows to that table, but never create one. **Settings** has an **Intake tables** panel with a close button. Members, viewers included, can grant this permission, because its reach is only the tables the owner has opened.
- `define` can now set a table's grain, primary key, time column, description and kind over MCP, and rename it.
## 14 to 17 September
**Site**
- The public site is redesigned around the semantic fluid, sanda search and MCP. The homepage leads with the business problems they solve, and a new product page walks through sanda with interactive samples that are labelled as illustrations. Pricing, signup, sign-in, terms, privacy and the not-found page share the new look. Signup checks each step as you go and works with the keyboard.
**Console**
- The console gets the same design, with grouped navigation, a drawer on phones and section navigation in Settings. Semantic fluid, sanda search and Agents sit together under Intelligence. The report toolbar works on narrow screens.
- The **Search your data** link in the top bar is gone. sanda search stays under Intelligence.
**sanda search**
- sanda search is rebuilt around finding records. Pick an index, ask in the query box, add typed filters, read results as cards and open a full record. Index settings and build logs move to their own tabs, which keep your current query. Creating an index needs a current cost estimate, and changing a field that affects price discards it.
**Semantic fluid**
- Fixed: **Learn from my data** could propose nothing and reject all of its own suggestions as "must be fully qualified". Its results also no longer stretch the page header.
- Fixed: one column could get two dimensions, such as `customer_country` beside `customer country`. Names now treat underscores like spaces, a second dimension on a column that already has one is refused with the existing name, and existing duplicates are merged with the old names kept as synonyms.
**Connections**
- Fixed: a connection set to sync at 7:00 AM ran every morning while sanda kept saying "Last sync 11h ago". sanda now sees runs the sync engine starts on its own schedule.
- Fixed: a run could appear two or three times in a connection's Timeline under one job number, and a failed run showed no reason. Each run appears once, and a failure shows its reason, or says plainly that none was given.
**Workspace and security**
- From **Settings · Team**, owners and admins invite people as an admin or a read-only member, change roles and remove people. An invitation opens a join page in the workspace that sent it. One address belongs to one workspace, nobody edits their own role, an admin cannot change an owner, and the last owner stays. See [workspace](https://docs.sanda-os.com.au/workspace).
- Owners can close a workspace by typing its short name under **Settings · Close or delete**. The workspace goes read-only, for the console and for agents over MCP, and a banner shows the date it will be deleted. The owner can reopen it until then. The closed period is 30 days at this point and is extended on 29 September.
**Warehouse**
- The **SQL shell** lets owners and admins run one SQL statement at a time against a warehouse. It is read-only by default, enforced by a read-only transaction. In read-write mode each statement commits at once, and the page says so. Results are capped, statements stop at your account's time limit, every statement is recorded, and the page opens on a warning you must acknowledge.
**Reports**
- A table block is a sheet. You can add formula columns in Excel's own language (`[net revenue]` is this row, and the same reference inside `SUM` or `AVERAGE` is the whole column), set a format per column, and add a totals row, a sort and hidden columns. A sheet stores instructions, never rows, so a margin column defined in June is a margin column in July. Exports to Excel keep the formulas live. Cells cannot be overtyped, so every figure stays traceable to the semantic fluid.
- A report can be private, visible to its author, owners and admins and the people you name, with edit rights if you choose. It can also be shared by link with people who have no sanda account. A link can expire, can be revoked and counts its opens. It opens a page rather than a canvas, and a visitor can sort and export a table but cannot ask for anything the author did not put on the page. See [reports](https://docs.sanda-os.com.au/reports).
**OData**
- The OData feed lets Excel, Power BI and Power Query Online connect to the semantic fluid without installing anything. Columns arrive under your names, metrics arrive already calculated, and you refresh instead of exporting. Each refresh is a live warehouse read, billed like any other question. See [OData](https://docs.sanda-os.com.au/odata).
- The credential is now an integration token, and the Settings panel is **Integrations** rather than Agents, because Excel can hold one too. Tokens you already hold keep working. The panel lists the Excel steps: choose Basic, leave the user name empty and paste the token as the password.
**MCP and agents**
- Fixed: `define`, `set_table_kind` and `retire_tables` could act on the wrong copy of a table when the learning dataset was present, ending in "can't tell which table sales is measured on". They now resolve names through your active model, so what `describe_table` shows is what they can write to. Answers come back sorted, and table and column names match without regard to case.
- `execute_sql` lets an agent run a SQL statement in your warehouse under a new privileged permission. It reads landed tables and your own modelling layer and writes only to your own modelling layer, with the same row cap, time limit and audit record as the SQL shell, plus the reason the agent gave.
## 9 to 13 September
**sanda search**
- sanda search launches. Point a service at one table on the semantic map, choose a key column and the text columns worth searching, and sanda keeps an index that finds rows by meaning and by exact words. A query returns the keys of matching rows, and you can use those keys as a filter on a metric. The index refreshes after the sync that feeds it. See [sanda search](https://docs.sanda-os.com.au/search).
- Search is off until an owner accepts a data-processing permission, because the text of the columns you index is processed outside Australia to create search vectors. Withdrawing the permission pauses your services, cancels queued builds and drops queries to exact words.
- Indexing is metered like other AI use. sanda shows an estimate before you create a service, and pauses indexing before your daily AI limit is spent so you can still ask questions.
- Fixed: creating, estimating and editing a search service failed with an internal error.
- A queued build shows when it will start, and **Run now** indexes the next slice straight away.
- Results say why a row is in the answer: meaning, words, or meaning + words. This replaces a number that looked like a confidence score but was only a rank. When no row contains your words, the page says the results are the nearest by meaning alone.
- The build panel says what a build is doing. It reads "Indexing now" only while a slice runs, otherwise "Part way through, not running" or "Waiting to start", with progress such as "640 of ~3,000". A build that runs out of time names the step the warehouse was too slow on.
- Large tables finish. A table too big for one pass used to cycle between queued and running without end. sanda now keeps the embeddings it has already computed and carries on from where it stopped. Requests run in parallel and in larger batches, so big tables index several times faster.
- Fixed: a search build against an unreachable warehouse failed with an unhelpful message. Connecting to a warehouse now has a time limit, and the build names what failed.
**Modelling**
- Schedules, tasks and search builds run on a grid so that sanda's scheduler can rest between ticks. The grid is 30 minutes from 9 September and 15 minutes from 13 September, and it applies to connection sync schedules too. A time off the grid is not accepted, the time picker only offers legal times, and queued work says when it will start.
**Warehouse**
- The learning dataset is rebuilt every night over a rolling ten-year window that ends today, so its newest orders are never months old.
**MCP and agents**
- MCP tools let an agent create, query and manage search indexes and start a build. See [MCP](https://docs.sanda-os.com.au/mcp).
- **Intelligence · Agents** shows whether your MCP setup is working. **Harness** lists the four things that must be true before an agent can do anything, each linking to the page that fixes it. **Agents** lists each token and connected app with what it may do and when it was last used. **Activity** shows sessions, the call feed, refusal rates per tool, and what sanda refused outright.
**Workspace and security**
- Signup asks you to accept the terms and privacy policy with an unticked box, and signup is refused without it. sanda records which dated version you were shown, and when.
**Plans and billing**
- Fixed: the pricing page could quote a fee that differed from what invoices use. The page now shows the price sanda bills.
**Site**
- A new **Why sanda** page explains, first in plain terms and then in technical detail, how sanda compares with a conventional cloud data warehouse. A chapter rail keeps your place on a wide screen.
## 7 and 8 September
**Plans and billing**
- Storage and compute prices changed. Compute bills at the warehouse's minimum size for every minute it is awake, plus the work it does on top, instead of at its ceiling. A warehouse that idles no longer pays as though it were busy. Current prices are on [editions](https://docs.sanda-os.com.au/billing/editions).
- The estimator on the pricing page prices what the meter bills: the storage you hold and the compute hours your team's questions are expected to use. It shows one figure instead of a range, and the number of people asking questions drives the compute estimate.
**Warehouse**
- **Data · Explorer** opens the warehouse like a database console. One tree lists each warehouse, its schemas, tables and views with row estimates. Choose a table or view to see its columns, a preview of its first 50 rows and, for a view, its definition. The explorer is read-only and open to every member.
- **Optimise** reads what is slowing a warehouse down or costing it storage, and offers fixes you choose: indexes your relationships would use, unused or duplicate indexes, indexes left broken by a cancelled build, and vacuum and analyse. sanda shows each statement before it runs, and reading the advice uses none of your AI limit. It never runs a full vacuum by default, because that locks the table while it rewrites. Partitioning is advice only. See [warehouse](https://docs.sanda-os.com.au/warehouse).
**Modelling**
- A new **Modelling** section under Data builds your own layer on top of landed data: views, materialized views and procedures, with schedules that rebuild and run them. Draw a view in the drag-and-drop builder, using your warehouse's tables and joins suggested from the relationships you have drawn, or write the SQL. Each object shows what it affects: the fluid table it is published as, the objects that read it and the schedules that rebuild it. New views publish to the semantic fluid by default. See [modelling](https://docs.sanda-os.com.au/modelling).
- **Save as view** in Ask the fluid turns your current selection into a view or a materialized view and publishes it.
- Tables you have modelled lead in the semantic fluid, and chat is told to prefer them where they can answer the question.
- Every rebuild is measured and billed as compute. A scheduled run that fails emails owners, at most once a day for each task, and alert settings have a switch for it.
- Fixed: the view builder listed the tables of the wrong warehouse, or none, and said "Nothing has landed in this warehouse yet" when tables were there. It lists the tables of the warehouse you select, and a warehouse selector always shows.
**MCP and agents**
- Eleven MCP tools let an agent create and refresh views, create and call procedures, and create, run and pause schedules, behind a new permission. Three more list a warehouse's sources and read and apply its optimisation advice.
## 4 to 6 September
**MCP and agents**
- Agents can hold a token. Under Settings you mint a token that lets Claude Code, the Claude API's MCP connector or your own agents work with the semantic fluid over [MCP](https://docs.sanda-os.com.au/mcp). A token is shown once, and it stops working when the person who made it leaves the workspace.
- sanda is now its own sign-in server for MCP, so a person can add it as a custom connector in Claude and approve access on a consent screen. Connected apps are listed under Settings, each with a **Disconnect** button.
- Fixed: adding sanda to Claude failed with "Couldn't register with sanda's sign-in service".
- Fixed: Claude's consent screen offered a single permission. It now lists every permission, and the ones that reach beyond asking questions start unticked.
- Twelve more MCP tools let an agent change the semantic fluid (import tables, define metrics and relationships, review suggestions), see the workspace (connections, sync status, warehouses, never a credential or connection string) and start a sync. Each sits behind its own permission, and a tool you have not granted is not listed. Definitions an agent writes go live at once, so only an owner or admin can grant that permission.
- `load_rows` lets an agent land its own table of rows in your warehouse, by replacing or appending, with types inferred or declared, and optionally put it on the semantic map in the same call. It sits behind its own privileged permission and respects your storage limit.
**Semantic fluid**
- An **arrange** button lays every table out by how they join, with the most-joined table in the middle and dimensions at the edge. The fact or dimension badge on a card is now a one-click switch, and dragging near the edge of the map pans it.
- A metric you describe to sanda can be saved as a metric straight away, instead of only as a proposal you then review.
- Fixed: dragging one column onto another could draw extra relationships when several columns shared a key. sanda now warns when the key you type already sits on other columns, and before an unlink would take other relationships with it.
- Fixed: a table you removed from the map could not be added back. Tables in the picker are compared against your active model only, and groups collapse and show how many of their tables are on the map.
**Workspace and security**
- The guard in front of free-form SQL now refuses more ways of hiding a second statement, and every response carries browser security headers, so another site cannot frame the console.
- Signup no longer reveals whether an email address already has an account.
**Plans and billing**
- Usage is the bill. Storage is billed on the gigabyte-months you hold and compute on the hours you run. Nothing is bought in advance, so the storage plan ladder is gone from the pricing page and from Settings. Editions set limits (warehouses, storage and compute per warehouse, and features), not a different price for the same storage. Current prices are on [editions](https://docs.sanda-os.com.au/billing/editions).
- Compute is billed by the compute hour, which is one vCPU running queries for an hour. The Overview shows a month of usage as three charts that share a cursor (storage, compute and accrued cost) in place of two meters.
- Fixed: compute was not being metered on any warehouse, so the Overview read "not metered" and compute did not appear on invoices. Compute is now metered per warehouse.
- The sizing calculator becomes the pricing page. It has an enquiry form for Enterprise and for sizing questions. Fixed: **Email me this estimate** never sent an email. It does now.
## 1 to 3 September
**Reports**
- Reports become canvases. A report is now a pan-and-zoom page of blocks: KPI tiles, bar and line charts, tables and written analysis. Draw a box on empty space and describe what you want in it, or ask cherry for a whole pack ("Build a June board pack: P&L vs budget, cash, top debtors"). Blocks drag by their header, resize from a corner, snap to a grid, and can be redrawn as a KPI, bar, line or table. A block stores the question rather than the rows, so next month's report shows next month's numbers. See [reports](https://docs.sanda-os.com.au/reports).
- Blocks appear as they are placed, while cherry is still working, and you can draw several boxes at once. When a request could mean two things, cherry asks with buttons instead of guessing.
- A filters pane applies conditions to every block on a report. Add one by writing a sentence or with the condition builder. Named periods such as last 12 months stay unresolved, so they move with the calendar.
- A selected chart gets a grain picker (day, week, month, quarter or year). Bar charts show as many bars as the block's height allows and say what they left out.
- Fixed: a chart of sales by month showed one row per day.
- The canvas is drawn as a white page on a darker desk, and the view is held so the page can never scroll out of sight.
- **Report colours** in **Settings · Appearance** sets the accent and status colours every report uses, from eight presets or your own. A single report can override them from its toolbar.
- You can delete a report from the list or the canvas, duplicate it, and save it as a template. Templates sit in their own list with **new report from this**.
- Recent versions of a report are kept, and you can restore one from a history menu. Undo and redo work as you expect (⌘ Z).
- Reports print to PDF with **export pdf** in the **more** menu. A header block carries your logo, the report's name and a subtitle. You can leave comments on any block, refresh one block or all of them, and click a bar to filter the whole report by that category.
- A generated board pack lands in tidy bands across the page, and new blocks come in sizes that tile it: four KPI tiles fill a row.
**Connections**
- Each stream in a connection's list has its own live status (queued, syncing, in or failed) and the rows it has read so far. The columns stay lined up however long a table name is.
- Fixed: for a short time on 3 September sanda could not reach its sync engine, so syncs could not start. Syncing is restored.
**Semantic fluid**
- Fixed: choosing a metric and a dimension that are joined through a table you did not pick, such as revenue by an employee's job title by way of orders, failed with "no stated relationship". sanda now finds the shortest route through the relationships you have drawn and joins the tables along it.
- **Learn from my data** says why it dropped a proposal, and shows the model's own notes and unanswered questions, such as "which currency are these amounts in". It runs one pass per connection, so each source gets the whole table budget.
**Workspace and security**
- Password sign-in is removed, and you sign in with an emailed link. When sanda cannot send email, the sign-in page says so instead of promising an inbox.
- Fixed: Settings showed a blank page in some workspaces.
**Site**
- Every page on the site has its own title and description, the site publishes a sitemap, and an address that does not exist now returns a proper "not found" page.
---
# August 2026
> The first month of sanda: sign-in and the console, warehouses and connections, the semantic fluid as a map, and chat over your own definitions.
## 13 August
**cherry**
- Fixed: after the 8 August change, pressing send in chat could appear to do nothing. A reply now has a time limit and says so when it runs out.
## 7 and 8 August
**Semantic fluid**
- sanda's learning dataset gives every workspace a sample business to explore: millions of rows of trading history with the tables, relationships and metrics already modelled. You can ask questions of it before you connect anything of your own.
- **Ask the fluid** builds a query from the semantic catalogue. Pick metrics, dimensions, filters and columns, and sanda runs the query on every pick, shows the SQL it composed, and returns rows as a table or a chart. It opens as a full card, with the catalogue on the left and results on the right. A filters card holds named filters.
- Scrolling over the map no longer zooms it. Use the zoom buttons or fit to content.
**cherry**
- Chat now works as a loop. Instead of being handed the whole semantic fluid and asked to write SQL, it searches the fluid, looks up table definitions and builds queries from your metrics and dimensions through the same planner as Ask the fluid. It writes custom SQL, with an explanation, only when it has to. It remembers the conversation, so a follow-up can refer to an earlier answer. Row limits and the read-only rule are unchanged.
## 6 August
**Connections**
- Choosing streams is now part of setup. The wizard gains a **Streams** step, and nothing syncs until you have chosen. A connection with credentials but no streams reads "Needs streams".
- The schedule picker offers Manual, Hourly, Daily and Weekly cadences with day buttons, a time and a timezone. It appears in the wizard and on a connection's **Settings** tab, so a schedule can be changed after the connection exists.
- A connection page refreshes on its own while a sync runs, so you no longer press **Check status** to see it finish.
**Warehouse**
- The Overview gains a **Warehouse size** panel that shows storage and compute against their limits. Warehouses are measured whenever you open a page that shows capacity.
**Semantic fluid**
- The semantic fluid becomes a map. Tables are cards you drag around a canvas. Datasets group tables in colour, and glue joins two datasets, written by hand or matched by sanda. A Map and List toggle keeps the list view for detailed edits.
- **Add tables** lists the tables in your warehouse and imports the ones you tick, grouped by the connector they came from. Each table is a fact or a dimension, so sanda does not sum a price list. The badge on a card switches it.
- A table enters the fluid as the columns you choose. Each column carries the words your team uses for it and the categories you group by. You draw a relationship by dragging one column name onto another.
- Sync bookkeeping columns stay out of the fluid. Removing a table retires it rather than deleting it, so its history is kept.
- Relationships show their cardinality, one to one or one to many. Suggestions open in a review queue you can drive from the keyboard: ↑ and ↓ to move, Enter to accept, Delete to reject.
- Metrics can be edited. Renaming a metric updates every metric that referred to it, and the form lists which ones before you save. Every card on the map has its own search.
**Plans and billing**
- Every AI call is recorded. What you ask counts against a daily allowance for the workspace, and the workspace picks which model answers.
**Workspace and security**
- Only owners and admins can add, change, delete or sync a connection. Members can still see them.
- The audit log can no longer be changed or deleted.
- Before sanda serves a workspace's data, it now checks that isolation between workspaces is in force, and it refuses to serve if it is not.
- Fixed: sign-in emails were not being sent.
## 5 August
**Connections**
- A new Connections page has a searchable list, a three-step wizard (define the source, connect, configure) and a full page for each connection with its status, timeline and settings. Each connector has a setup guide beside its form.
- You can upload a CSV straight into a warehouse. Drag in a file, name the table, and choose whether to append or replace. sanda detects the delimiter and infers each column's type from every row.
- You can choose which tables a connection syncs. For incremental streams you pick the cursor and the primary key. Changes reach the running connection at once.
- Setup forms for PostgreSQL and other connectors that offer several options no longer fail with a validation error. Forms open on the simplest option, check the fields of the option you have open, and fold SSL, update method and tunnel settings under **Advanced**. PostgreSQL goes from eleven questions to four.
- sanda checks a database host against public DNS before it tries to connect, and tells you when a host can never work from the internet, such as `localhost` or a private address.
- Connection errors say what to do. A hostname that cannot be resolved, a refused connection and a rejected password each get their own plain-language explanation.
- Deleting a connection now removes it from the sync engine too, so nothing keeps syncing or holding your credentials.
- Fixed: a connection created without choosing streams failed every sync. Fixed: syncing a source that already carries another loader's bookkeeping columns failed with a duplicate column error.
**Warehouse**
- A workspace can have more than one warehouse, up to the limit of its edition. Each connection lands in the warehouse you choose. You can delete a warehouse, and retry one whose provisioning failed.
**Plans and billing**
- Warehouse pricing is published on the site and in the console, built on storage and compute. Prices changed during the month, so read current prices on [editions](https://docs.sanda-os.com.au/billing/editions).
**Site**
- Sydney is the default region, and the only one available. The console moves to its own address, separate from the public site.
## 3 and 4 August
**Site**
- The sanda site opens with a homepage and free trial signup. The trial runs for 7 days.
- A sizing calculator on the site estimates what a warehouse costs for your data size and team.
**Workspace and security**
- You sign in with a one-time link sent to your email. A link works once and expires in 30 minutes.
- You can also set a password from **Settings** and sign in with it. Password sign-in is removed again on 2 September (see [September 2026](https://docs.sanda-os.com.au/releases/2026-09)).
- Fixed: the sign-in link opened the homepage and never signed you in.
- Fixed: mail programs and link scanners that open a link before you do no longer use it up. Opening a link you have already used, while you are signed in, takes you to the console instead of an "expired" message.
- Console errors now say which request failed and with what status, in place of "Something went wrong."
**Warehouse**
- **Provision warehouse** in **Data · Warehouse** creates your own PostgreSQL database in Sydney, kept apart from every other workspace. See [warehouse](https://docs.sanda-os.com.au/warehouse).
**Semantic fluid**
- The semantic fluid arrives: tables, dimensions, metrics and mappings that give your data the meaning your business uses. See [semantic fluid](https://docs.sanda-os.com.au/semantic-fluid).
- **Learn from my data** profiles your warehouse and proposes definitions for you to review. Nothing goes live until you accept it.
**Connections**
- The Connections page builds each connector's setup form from the connector catalogue, so any listed connector can be configured without a hand-built form.