Phase
Concepts

Query

llms.txt

Query lets you answer product questions with read-only SQL. The dashboard editor and the Public API run the same engine against your app data.

Where to find it

Open Dashboard → Analytics → Query.

Use Instructions in the editor for a copyable SQL reference. Save queries as presets for your team.

Virtual tables

Query exposes three virtual tables. App scoping is injected automatically. You never filter by app_id yourself.

events

ColumnTypeNotes
timestamptimestamptzEvent time
user_idtextAnonymous user identifier
nametextEvent name (e.g. purchase, level_win)
paramstextJSON string of event properties

events queries run on QuestDB when used alone. Use QuestDB JSON syntax:

json_extract(params, '$.duration_seconds')
cast(json_extract(params, '$.level_number') as long)

Modulo is supported: expr % 5 = 0

users

ColumnTypeNotes
user_idtext
platformtextios, android, or unknown
countrytextISO country code
localetext
modeltextDevice model. Apple hardware IDs (e.g. iPhone17,1) are resolved to marketing names (e.g. iPhone 16 Pro). Android and already-friendly names pass through.
os_versiontext
first_seentimestamptzFirst seen time
propertiesjsonbCustom user properties — use properties->>'key'

The virtual table is named users. Do not use devices or device_id in SQL. Those names are rejected.

sessions

ColumnTypeNotes
session_idtext
user_idtext
started_attimestamptz
last_activity_attimestamptz
duration_secondsnumberComputed session length

Each query uses one virtual table. events runs on QuestDB. users and sessions run on Postgres.

Time range

If your SQL does not filter on timestamp, started_at, last_activity_at, or first_seen, events and sessions default to the last 30 days.

Add your own predicates to control the window:

WHERE timestamp >= '2026-01-01'

Pagination

Pagination is page-based, not OFFSET in SQL.

SurfacePage sizeMax page sizePagination
DashboardLIMIT in SQL (default 100)1000UI page controls
Public APILIMIT in SQL (default 100)50page in request body

Rules:

  • LIMIT sets how many rows each page returns.
  • Do not write OFFSET in SQL. It is rejected.
  • Use page controls in the dashboard, or send page in the API request.
  • Deep pagination is capped at 100,000 skipped rows.
  • Export downloads the current page only.

API example with pagination:

{
  "sql": "SELECT name, count(*) AS events FROM events GROUP BY name ORDER BY events DESC LIMIT 50",
  "page": 2
}

The response includes meta.page, meta.pageSize, meta.offset, meta.hasNextPage, and meta.hasPreviousPage so you can loop programmatically.

Rules

  • SELECT only. No INSERT, UPDATE, DELETE, or DDL.
  • Single statement. No semicolons.
  • Read-only sandbox. Queries cannot mutate data.
  • Debug events excluded automatically.
  • One table per query. No JOIN and no multi-table FROM.
  • Legacy names rejected: devices, device_id.

Examples

Recent events:

SELECT timestamp, user_id, name AS event_name
FROM events
ORDER BY timestamp DESC
LIMIT 100

Top events:

SELECT name AS event_name, count(*) AS events
FROM events
GROUP BY name
ORDER BY events DESC
LIMIT 100

Users by platform:

SELECT platform, count(*) AS users
FROM users
GROUP BY platform
ORDER BY users DESC
LIMIT 50

Average level duration (every 5th level):

SELECT
  count(*) AS level_wins,
  avg(cast(json_extract(params, '$.duration_seconds') AS double)) AS avg_duration_seconds
FROM events
WHERE name = 'level_win'
  AND cast(json_extract(params, '$.level_number') AS long) % 5 = 0

Presets

Save the current SQL as a preset from the Query page. Presets are per app and shared with your team. Loading a preset replaces the editor contents.

Automation

Use the same queries from your backend with a public API key. See Query via API for the endpoint and request shape. Full SQL, pagination, and limit details live on this page.