> For the complete documentation index, see [llms.txt](https://docs.tiny.plus/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tiny.plus/api/endpoints/ai-filter.md).

# Ask filters

<mark style="color:green;">`POST`</mark> `https://api.tiny.plus/v2/ai/filter`

Turns a plain-English question into the query parameters for a [record list](/api/endpoints/records.md#list-records). Send `"won projects over 50k that Maya is on"`, get back `record_status=won`, `fee_value=>50000` and `staff=<Maya's id>`, ready to put on `GET /v2/projects`.

It doesn't read or return any records itself — you run the list. It reads your account's fields, labels, statuses, tags and team so the words map to your setup.

{% hint style="info" %}
This is the same "Ask" feature as the search box in tiny+. It needs the **Pro plan**, with **Ask switched on** in Account Settings → AI Usage. Where it's off, the endpoint still answers `200`, telling you to fall back to a plain search — see [When Ask falls back](#when-ask-falls-back). A token's user can check `integrations.ai_ask` in [`/me`](/api/endpoints/me.md#integrations) first.
{% endhint %}

## Request

Send a JSON body:

```bash
curl -X POST "https://api.tiny.plus/v2/ai/filter" \
  -H "Authorization: Bearer {{user_access_token}}" \
  -H "Content-Type: application/json" \
  -d '{
        "flavour": "projects",
        "q": "won projects over 50k this year that Maya is on, biggest first"
      }'
```

| Field             | Description                                                                                                                                                                                                                       |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flavour`         | Required. The record type to build a filter for, such as `projects` or `contacts`.                                                                                                                                                |
| `q`               | Required. The question, 1 to 500 characters.                                                                                                                                                                                      |
| `current_filters` | Optional. A filter already in force, as a `{field: value}` object in the [list grammar](/api/concepts/listing.md#filtering). Ask refines it and returns the whole new set — this is how you follow up "…and only the ones I own". |
| `client`          | Optional label for your integration, recorded in tiny+'s AI log.                                                                                                                                                                  |

## Response

```json
{
    "flavour": "projects",
    "query": {
        "record_status": "won",
        "fee_value": ">50000",
        "close_date": "2026-01-01..2026-09-30 23:59:59",
        "staff": "191",
        "sort": "fee_value desc"
    },
    "explanation": "Shows won projects with a Project Fee over 50000 AUD, closed this year, assigned to Maya Lin, biggest first.",
    "assumptions": [
        "Assumed 'over 50k' with no field named applies to the default money field, Project Fee.",
        "Assumed 'Maya is on' means Maya Lin is the Assigned To staff member."
    ],
    "unresolved": [],
    "confidence": "medium",
    "source": "model",
    "chips": [
        { "key": "record_status", "label": "Project Status: Won!" },
        { "key": "fee_value", "label": "Project Fee > 50,000 AUD" },
        { "key": "staff", "label": "Team: Maya Lin" }
    ]
}
```

| Field         | Meaning                                                                                                                                                                 |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`       | The parameters to send to `GET /v2/{flavour}`: field conditions, `tags_{id}`, `custom_{id}`, `me`, `staff` and `sort`. `{}` when nothing in the question could be used. |
| `explanation` | One sentence describing the filter, in the account's own labels.                                                                                                        |
| `assumptions` | The readings Ask had to make — worth showing the user so they can correct one.                                                                                          |
| `unresolved`  | Parts it couldn't map, each `{phrase, reason}` — for example a name that matches no team member.                                                                        |
| `confidence`  | `high` (a clean mapping), `medium` (it made assumptions), or `low` (something's unresolved, or the query is empty).                                                     |
| `chips`       | The filter as a list of `{key, label}`, for showing it back to the user.                                                                                                |
| `source`      | `model` for a fresh answer, `cache` for a repeat within the account.                                                                                                    |

{% hint style="warning" %}
`explanation`, `assumptions` and `chips` are model-written and use your account's labels. Escape them before putting them in HTML.
{% endhint %}

## Applying the filter

1. If the response has a `reason`, run the fallback search (below).
2. Otherwise, show the user the `explanation`, `chips`, `assumptions` and anything `unresolved`.
3. When `query` isn't empty and the `confidence` is good enough for you, send its entries as query parameters on `GET /v2/{flavour}`. URL-encode the values — `>`, `<`, `!`, spaces and `|` all need it — and add your own `limit`, `start` and `fields`.
4. To refine, send that `query` back as `current_filters` with the next question.

```bash
curl "https://api.tiny.plus/v2/projects?record_status=won&fee_value=%3E50000&staff=191&sort=fee_value%20desc&return_format=array" \
  -H "Authorization: Bearer {{user_access_token}}"
```

{% hint style="warning" %}
An empty `query` (`{}`) filters nothing, so a `GET` with it returns every record. Check `confidence` and `unresolved` before applying a filter, rather than sending an empty one.
{% endhint %}

## When Ask falls back

If Ask is switched off, over a limit, or the model can't help, you still get `200` — with `query: null` and a `reason`, plus a `fallback` telling you to run a plain [search](/api/endpoints/records.md#search):

```json
{
    "flavour": "projects",
    "query": null,
    "fallback": { "endpoint": "/v2/search/projects", "q": "won projects over 50k" },
    "reason": "disabled"
}
```

`reason` is one of `disabled` (Ask isn't on for the account), `capped` (a [rate limit](/api/concepts/fair-use.md#limits-we-enforce) is used up), `timeout`, `overloaded`, `refusal`, `invalid` or `error`. In every case, run `GET {fallback.endpoint}?q={fallback.q}`.

{% hint style="info" %}
A question the model understands but can't turn into a filter — "what's the weather in Paris?" — is **not** a fallback. It comes back `200` with `query: {}`, `confidence: low` and the phrase under `unresolved`. Branch on `reason` (or `query === null`) to tell the two apart.
{% endhint %}

## Limits and errors

Ask draws on your account's AI allowance, with a per-minute limit as well. When either is used up, the response is the `capped` fallback above. The numbers are on the [Fair use](/api/concepts/fair-use.md#limits-we-enforce) page.

| Case                                                       | Response                                                                              |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `flavour` missing, not a string, or not an API record type | `400 {"error": "'flavour' must name a record type this account exposes to the API."}` |
| `q` missing or blank                                       | `400 {"error": "'q' is required."}`                                                   |
| `q` over 500 characters                                    | `400 {"error": "'q' is limited to 500 characters."}`                                  |
| `current_filters` not an object                            | `400 {"error": "'current_filters' must be an object of field=value pairs."}`          |
| Missing or invalid token                                   | `401`, in the [OAuth error shape](/api/concepts/requests.md#errors)                   |
