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

# Stored queries

> Write SQL when filters aren't enough — computed distances, bucketed totals, anything with arithmetic.

The generated search tool handles *filter, sort, limit*. Some questions need more than that:

* *"the five nearest stores"* — needs a distance calculated from the caller's location
* *"total spend by tier"* — needs grouping and a sum
* *"plans expiring in the next 30 days, cheapest first"* — needs date arithmetic

A **stored query** is SQL you write once, save on a table, and turn on. It becomes an agent tool like any other.

<Note>
  Writing and enabling stored queries needs the `custom_data:manage` permission. Reading and running them doesn't — an agent uses a query someone else validated.
</Note>

## Writing one

Open a table, go to **Queries**, and click **New query**.

```sql theme={null}
SELECT s.name,
       s.address,
       round((6371.0088 * 2 * asin(sqrt(
             power(sin(radians(a.lat - s.latitude) / 2), 2)
           + cos(radians(s.latitude)) * cos(radians(a.lat))
           * power(sin(radians(a.lng - s.longitude) / 2), 2)
       )))::numeric, 1) AS km
FROM stores s, args a
WHERE s.latitude IS NOT NULL
  AND s.longitude IS NOT NULL
ORDER BY km ASC
```

Three things to know before you write anything:

**Your table is a table.** Refer to it by name — `FROM stores s` — and its columns are real typed columns, not JSON. `latitude` is a number, not text.

**Parameters arrive in `args`.** Declare them below the editor and join `args` to read them. It's one row with one column per parameter, already the right type:

```sql theme={null}
FROM stores s, args a
WHERE s.region = a.region
```

**Don't write `LIMIT`.** The **Row limit** box applies it. That's your "nearest N".

## Parameters

Each parameter has a name and a type, and both matter: they become the generated tool's parameters, and they're the only place the type is stated. The SQL can't say that `lat` is a number, or that it exists.

The third column in each parameter row is a **test value**, used only by **Run**. It's never saved.

<Warning>
  A parameter with no test value runs as `NULL`, which is not the same call a real agent makes. The result panel tells you which ones ran as NULL — read it. A query that looks right with a missing parameter can behave completely differently when an agent actually supplies one.
</Warning>

## Run before you save

**Run** executes against your live rows and shows what comes back. Nothing runs on keystroke.

Three kinds of result:

* **Rows** — what an agent would get
* **"Ran without errors and matched no rows"** — valid query, nothing matched. Not a failure.
* **Did not validate** — with the reason, verbatim, and a note explaining the rule behind it

## What a query may do

Anything that reads **one table** and computes over it. Maths, `CASE`, aggregates, window functions, string and date functions, `GROUP BY`, `ORDER BY`.

What it may not do, and why:

| Not allowed                                           | Because                                                                             |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Reading another table — even your own                 | One table, no joins. It's what makes it impossible to reach anything you shouldn't. |
| Writing anything                                      | Queries read. For writes see [claiming rows](/custom-data/claiming-rows).           |
| Clock and session functions (`now()`, `current_user`) | A saved query must give the same answer for the same rows.                          |
| More than one statement                               | One question per query.                                                             |

If a query is refused, the message names the specific function or table that was rejected.

<Note>
  Need the current date? Pass it as a parameter. The workflow or agent calling the query knows what "today" is; the query deliberately doesn't.
</Note>

## Saving, and the four states

A query is always saved, even if it doesn't validate — a draft you're mid-way through isn't something to throw away. What changes is its state:

| State                  | Meaning                                                           |
| ---------------------- | ----------------------------------------------------------------- |
| **draft**              | Saved, not yet validated                                          |
| **validated**          | Checked and ready. Switch it on to generate the tool.             |
| **broken**             | Didn't validate. The reason is on the card. Can't be switched on. |
| **needs revalidating** | Was fine, but the table's schema changed underneath it            |

Only a query that is **validated and switched on** becomes an agent tool.

Re-saving a working query that now fails also switches it **off**, so a live agent never keeps a tool the platform has stopped vouching for.

## When a query goes stale

Editing the table's schema marks every query on it as needing revalidation, because their columns may no longer exist. The card tells you which column went missing:

> This reads `region`, which the schema no longer declares.

Open it, and save again to revalidate.

## Using one

Once switched on, a query appears on the **Agent tools** tab as `<query_name>_<table>`. Copy it into an agent like any other tool.

Workflows can call it too — add an action, pick **Custom Data → Run Saved Query**, choose the table and the query, and supply the arguments. Useful when you want the query's result to feed later steps rather than going straight to the model.

## A note on how arguments are handled

Values you pass are handed to the database as **data**, never pasted into the SQL text. A caller value that happens to look like SQL is just a value. You don't need to escape or sanitise anything.
