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

# The ana.* Functions

> The built-in functions a Data App uses to read data, run compute, remember state, and ask Ana

Every [Data App](/core/how-it-works/data-apps) talks to the platform through one small, built-in SDK: the `ana.*` functions. They're how an app queries its data sources, invokes compute functions, remembers a viewer's settings, shows who's online, and even starts a real conversation with Ana from inside the app.

Ana writes these calls itself when it builds your app, so you never have to touch them to get a working Data App. But knowing what's in the toolbox pays off twice: your prompts can ask for capabilities by name ("remember my filters," "let me ask Ana about this row"), and if you ever edit the code directly, this is the API you'll be working with.

The SDK has two halves, one on each side of the app:

* **In the front end**, every page gets a global `ana` object in JavaScript: rendering helpers, live queries, viewer identity, presence, and deep links.
* **In compute functions**, the server-side Python code gets an injected `ana` client: governed reads, declared writes, per-viewer state, file uploads, and asks.

## In the Front End

The browser-side `ana` object covers rendering, data, and everything live:

| Function | What it does |
| - | - |
| `ana.ui.chart / table / metric` | House-styled visuals: theme-aware ECharts charts, formatted tables, and metric tiles that pick up your org's brand accent automatically |
| `ana.data.get / sources` | The app's baked-in data: every declared data source, by name, exactly as it was when the app was last saved |
| `ana.compute.run(name, params)` | Invokes one of the app's declared compute functions and resolves with its result: the bridge to everything server-side, including live queries |
| `ana.state.load / save` | Per-viewer saved settings (filters, layout, preferences) that survive reloads; saves are debounced and capped at 64KB |
| `ana.activity` | A shared, append-only activity log: `record` an event, `list` your own history, `subscribe` to everyone's as they happen |
| `ana.presence.join(zone)` | A live roster of who's viewing, by named zone, with a callback on every change |
| `ana.realtime` | A host-relayed WebSocket to the app's own persistent server, for live boards and shared, instant updates between viewers |
| `ana.route` | Deep links: the app's URL tail updates as someone navigates, so a specific view, filter, or record can be bookmarked and shared |
| `ana.navigate(appId, opts)` | Sends the viewer to a *different* Data App. `opts` picks the target's root or a deep-link route inside it, so a set of apps can link to each other like pages of one product |
| `ana.ask.watch(chatId, handler)` | Streams a live Ana thread into the app, cell by cell (see [Asking Ana](#asking-ana-from-inside-an-app) below) |
| `ana.viewer` | The signed-in viewer's display identity (`memberId`, `displayName`, `email`), or `null` on anonymous views |
| `ana.grants` | Which gated sources and functions this viewer may invoke, for hiding tabs the viewer can't use (the server enforces the real gate) |
| `ana.upload(file, options)` | Uploads a viewer-selected file directly to storage, prepares Parquet, and returns a reference for SQL or Python compute; supports upload progress |
| `ana.uploads.list / remove` | Lists this viewer's saved uploads for the app in pages, or removes an association while keeping the dataset |
| `ana.download(filename, content)` | Hands a file to the browser's download flow from inside the sandboxed app |
| `ana.theme` | The app's resolved design tokens (colors, fonts, chart palette), for custom visuals that should match everything else |

Not every context is fully live: a public share link or a screenshot renders from the app's static snapshot, with no server behind it. The SDK is built for that: each live namespace exposes an `available` flag, baked-in data still renders, and a well-built app degrades to a read-only view instead of breaking. Ana wires in those checks by default.

## In Compute Functions

Server-side Python gets its own `ana` client. This is where anything live, private, or write-shaped happens:

| Function | What it does |
| - | - |
| `ana.query(source_name, **params)` | Runs one of the app's declared data sources live, with parameters, and returns a DataFrame |
| `ana.sql(query)` | Runs SQL against the app's private [App Database](/core/how-it-works/data-apps#app-database) |
| `ana.write(name, **params)` | Executes a declared write template against a connector: the persistence behind "mark as done" and "save this form" |
| `ana.notify(name, **params)` | Sends a branded email from a declared template; the recipient is always the person using the app, never an arbitrary address |
| `ana.state.get / set / delete / keys` | Server-side key-value state, per-viewer by default or shared app-wide with `shared=True`; 64KB per value |
| `ana.assets.put(file_name, data, content_type)` | Uploads a generated file (a CSV export, a rendered PDF, an image) and returns a signed URL; 10MB cap, and nothing scriptable is allowed |
| `ana.ask(prompt)` | Starts a real Ana thread on the viewer's behalf and returns a handle with `.poll()`, `.wait()`, and `.followup()` |
| `ana.ask_poll(ask_id, cursor)` | Re-polls an ask started earlier, even from a different invocation, so long-running asks don't have to block |

## Asking Ana from Inside an App

`ana.ask` is the one function that puts Ana herself inside your app. A compute function can hand her a prompt, and she runs it as a real thread: same agent, same tools, same governed data access as a chat you'd start yourself.

```python theme={null}
def explain_account(account_id):
    run = ana.ask(f"Why did account {account_id}'s usage drop this month?")
    result = run.wait()
    return {
        "summary": result["text"],
        "charts": result["charts"],
        "chat_id": result["chat_id"],
    }
```

The handle gives you three ways to consume the answer:

* **`run.wait()`** blocks until the thread finishes and returns everything at once: the markdown text, any charts as URLs, and the thread's `chat_id`.
* **`run.poll()`** (or `ana.ask_poll` from a later invocation) returns whatever new typed blocks (markdown, chart, link) have arrived since the last cursor, so an app can show progress instead of a spinner.
* **`run.followup(prompt)`** appends to the same thread, so "now break that down by region" keeps its context.

And because the ask is a real thread, the front end can stream it live instead of waiting on the server: pass the `chat_id` up to the UI and watch it render cell by cell.

```javascript theme={null}
const unwatch = ana.ask.watch(chatId, (event) => {
  if (event.kind === "cell") renderCell(event.cell);
  if (event.kind === "status") showStatus(event.status);
});
```

Two properties make asks safe to put in front of a whole team:

* **Asks run as the viewer, not the app's creator.** The thread is created under whoever clicked the button, with their permissions; they can only see what they could already see in a chat of their own. Anonymous views (public share links) get no ask at all.
* **Asks are rate-limited** (20 per hour per app session), so a runaway loop can't burn through your org's usage.

In a prompt, this is the difference between a static app and one with an analyst inside it: "add an *Explain this* button that asks Ana why the number changed" is all it takes.

## The Security Model, Briefly

Nothing in a Data App carries ambient authority. Every operation beyond a plain read (`write`, `notify`, server-side `state`, `assets`, `ask`) is a **capability**: declared in the app's config when Ana builds it, and re-checked by the platform on every call. The practical consequences:

* Capabilities expand what app code can *do*, never whose permission it runs with. Connector access is re-checked live against the app's creator (revoking access bites immediately), and every action is attributed to the viewer who triggered it.
* `ana.write` and `ana.notify` are gated off by default at the org level and are enabled per organization by TextQL; reach out to [support@textql.com](mailto:support@textql.com) to turn them on before any app persists to a warehouse or sends email.
* Writes and notifications are audit-logged.

You don't manage any of this by hand. It's why "let people mark a row as done" in a prompt turns into a governed, declared write rather than an app with raw warehouse credentials in it.

## Getting Support

If you have questions about the `ana.*` functions or run into issues, reach out to [support@textql.com](mailto:support@textql.com) or visit the [customer support page](/core/get-started/customer-support).

## File uploads

Use `ana.upload(file, { onProgress })` for viewer-selected files (runtime 1.10+).
Check `ana.upload.available`; uploads require a signed-in viewer. The host requests a
server-signed URL and sends the file directly to storage using chat's transport,
including Azure block uploads. Do not base64-encode files into compute params.
The limit is 500MB per original file. Show progress, disable repeat submissions, and
handle rejected promises. A `Blob` also needs `{ filename: "input.csv" }`.

```js theme={null}
const upload = await ana.upload(event.target.files[0], { onProgress: setProgress });
const result = await ana.compute.run("summarize", { file: upload });
```

`ana.upload` resolves after CSV, XLS, or XLSX ingestion has finished using chat's
conversion pipeline. Parquet uploads reuse the original stored object. Upload progress
covers byte transfer; keep the UI in a preparing state until the promise resolves.
Preparation has a separate 10-minute deadline from compute's 150-second deadline.
Recognized parsing failures and non-tabular files remain usable as raw files in Python.
The shared legacy XLS reader can interpret custom numeric formats as dates; use XLSX
or CSV for those workbooks. This upload path preserves chat's conversion behavior.

Declare `file` as an `object` parameter and pass the reference as a **top-level
parameter**, unchanged. Access is rechecked as the invoking viewer on every call,
even when the files are cached.

### List and remove saved uploads

The platform keeps a durable upload inventory per app and viewer, separate from
`ana.state`. Check `ana.uploads.available`, then load a page on app open:

```js theme={null}
const page = await ana.uploads.list({ pageSize: 25 });
// Each item has name, createdAt, and file (the reusable upload reference).
if (page.items.length > 0) {
  const result = await ana.compute.run("summarize", { file: page.items[0].file });
}
// Fetch subsequent pages with { pageSize: 25, pageToken: page.nextPageToken }.
// In a remove action: await ana.uploads.remove(item.file);
```

Handle empty lists and rejected promises before offering file actions. Page size
is 1–100 (default 25). A page can be short or empty after permission filtering;
continue while `nextPageToken` is nonempty. Items are ordered newest association
first. `createdAt` is when the association was recorded; recovered legacy pointers
receive their import time. Store only selections/preferences in `ana.state`, not
the upload inventory or file contents.

Lists contain only this viewer's uploads associated with this app, including
references recovered once from their existing ontology upload directory. Every
list and compute call checks current dataset access. Listing reads metadata only;
it does not download files, prepare Parquet, or start a worker. Removing an item
removes this viewer's app association and ontology pointer; it does not delete the
underlying dataset, revoke access, or remove it from another app/viewer's list.
Previously retained references remain usable while dataset access is valid.

### Query uploaded Parquet with SQL

Prefer a `sql` compute function for filtering, joins, and aggregations. An `object`
parameter selects the upload-query path: DuckDB scans authorized Parquet directly,
without a Python worker, pandas, or importing rows into the app database.

```json theme={null}
{
  "name": "summarize",
  "description": "Summarize an uploaded table",
  "params": [{"name": "file", "type": "object"}],
  "sql": "SELECT count(*) AS rows FROM read_parquet(:file)"
}
```

`:file` binds to a server-resolved list of Parquet paths. For a workbook, use
`read_parquet(:file[1])` to select the first processed sheet (DuckDB lists are
one-based); scanning the whole list requires compatible sheet schemas. Scalar
filter parameters still use `:name` binding. Upload SQL accepts one read-only
statement and cannot access the app's private database, arbitrary files, or the
network. A raw-only upload produces an explicit error.

### Python and server functions

Python/server-handled functions receive the same reference as local file metadata.
Use the SQL function above for DuckDB analysis, or Python libraries for custom processing:

```python theme={null}
def summarize(file):
    import pyarrow.parquet as pq
    if file["raw_only"]:
        raise ValueError("This file could not be parsed as a table")
    rows = pq.read_metadata(file["table_paths"][0]).num_rows
    return {"rows": rows}
```

The resolved object contains `dataset_id`, `version`, `name`, `path` (original file),
`table_paths` (one Parquet file per processed sheet), and `raw_only`. Non-tabular files
and recognized parse failures retain the original file with no table paths. Read it
with `open(file["path"], "rb")`; select parsers using `file["name"]`, since the local raw
path has no extension. Local paths last only while the invocation is active; do not retain them for background work. Reuse the opaque
upload reference for later invocations while the viewer retains dataset access.

### Persistence and caching

With config objects enabled and an existing org Library, preparation stores a private
JSON reference under `app-uploads/<app-id>/<viewer-key>/<dataset-id>/v<version>.json`.
The Library contains only the dataset ID and version, never file bytes, storage
credentials, or expiring URLs. The upload reference includes `libraryPath`,
`tableCount`, and `rawOnly` metadata inside `$anaUpload`; the backend never trusts
these hints for access or path resolution. A saved JSON reference can be passed to
compute later while dataset access remains valid. Without config objects, the
original and processed files still persist in dataset storage and `libraryPath` is empty.
Deleting an app removes its Library references; the underlying viewer-owned dataset
can still be used elsewhere.

The compute host caches dataset-version files for repeated queries, with a
1GiB total budget, at most 128 objects, and a 10-minute idle expiry. Files in active
queries are pinned. Individual processed files over 1GiB are rejected. SQL reads the
cache directly; Python receives independent local copies so it cannot modify cached
data used by later calls.

Uploads remain viewer-owned datasets. This does not publish an app data source or grant
other viewers access. App code is trusted with the selected contents, as with base64
uploads, and can retain derived data in app state. Existing base64 calls still work.
Existing published apps need a runtime refresh or a new save to expose `ana.upload`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.