---
title: "Tools and live data"
description: "Let a generated UI read your data with @query and change it with @mutation, and keep the two kinds of tool apart."
---

> Documentation Index
> Fetch the complete documentation index at: https://gistui.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools and live data

A model can write an answer with the numbers baked in. It can also write an answer that **reads its data from your app**: a dashboard that refetches when a filter changes, a table that refreshes every minute. That is what `tools` and `mutations` are for.

## Two kinds of tool

| | `tools` | `mutations` |
|---|---|---|
| Called by | `@query("name", args)` | `@mutation("name", args)` through `@run` |
| Runs | As soon as the UI renders, and again when its arguments change | Only when a person presses a button |
| Must be | Read-only | Anything: this is where changes go |

The split is a safety rule, not a convention. A program is untrusted, and rendering it must never change anything. So only functions you list under `mutations` can be reached by a button, and nothing under `tools` should have a side effect.

## Give the renderer your functions

```tsx
<GistUI
  library={ui}
  stream={stream}
  tools={{
    get_sales: async ({ range }) => db.sales(range), // read-only
  }}
  mutations={{
    update_goal: async ({ goal }) => db.setGoal(goal), // runs on a button press
  }}
  onToolCall={(call) => {
    audit.log(call); // { name, args, kind, nodeId }
    return call.name !== "delete_everything"; // false blocks the call
  }}
/>
```

`tools` can also be an MCP-style client: any object with `callTool({ name, arguments })`.

## Tell the model they exist

```ts
const system = prompt({
  tools: [
    { name: "get_sales", args: "{range:str}" },
    { name: "update_goal", args: "{goal:num}" },
  ],
}).text;
```

The tool list goes at the end of the prompt, so the part before it stays cacheable.

## What the model writes

```gistui
$range = "30d"
root = Page(Header("Sales"), filter, Card(Chart(sales, type:line)), goal)
filter = Select("range", "Range", ["7d", "30d", "90d"], bind:$range)
sales = @query("get_sales", {range:$range}, default:[], every:60)
save = @mutation("update_goal", {goal:$goal})
goal = Row(Slider("goal", "Goal", min:0, max:100, bind:$goal), Button("Save", do:[@run(save), @run(sales)]))
```

- `sales` re-runs whenever `$range` changes. Results are cached per argument set.
- `default:[]` is what renders until the first result arrives.
- `every:60` refreshes every 60 seconds (the minimum interval is 5 seconds).
- A query never runs while its own statement is still streaming.
- `save.status` is `idle`, `pending`, `success` or `error`, so the program can show a spinner or a message.
- `@run(sales)` after a save re-fetches the query.

## Expressions over the data

A query result (or a pipe table) is a list of rows, and the language has a small set of built-ins to work with it:

```gistui
total = "Total: " + @fmt(@sum(sales.total), "$")
big = @each(@filter(sales, s => s.total > 1000), s => Tile(s.customer, value:@fmt(s.total, "$")))
```

`sales.total` plucks one field from every row. The full list is in [State and expressions](/docs/language/state-and-expressions/).

## Limits

The runtime bounds what a program can do, whatever it writes: at most 60 tool calls in any 10 seconds, at most 20 queries with a refresh timer, at most 5,000 items in one `@each`. The full table is in [the format's limits](/docs/protocol/format/#limits).

## Pausing

`paused` stops timed refreshes without unmounting the UI. `@gistui/chat` uses it so only the latest answer in a thread keeps refreshing.

Source: https://gistui.com/docs/guides/tools-and-data/index.mdx
