Skip to main content
Every Data App 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: 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:

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.
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.
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 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 or visit the customer support page.

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" }.
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:
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.
: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:
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.