Query language
The JSON behind every chart — trends, funnels, retention, raw events, SQL and web analytics — with filters, breakdowns and date ranges.
Every chart in Datahop is a JSON object with a kind. The insight editor builds it, saved insights store it, and agents send it to the MCP query tool or POST /api/projects/:projectId/query. That means an agent can rebuild any chart you see, and you can open any insight an agent saved. You only send what differs from the defaults.
kind | Answers | Can be saved as an insight |
|---|---|---|
trends | Counts or totals over time | Yes |
funnel | Conversion through ordered steps | Yes |
retention | Do people come back? | Yes |
paths | Common sequences of pages or events | Yes |
lifecycle | New, returning, resurrecting and dormant people | Yes |
stickiness | How many days people did it | Yes |
events | Raw events, newest first | No |
sql | Anything else, in read-only SQL | No |
web_overview, web_stats, web_trend | Traffic (needs Web analytics) | No |
Every result includes a summary: a few lines of plain text with the numbers that matter.
Building blocks
Date ranges
{ "dateRange": { "from": "-30d" } }from takes -24h, -7d, -12w, -6m, -1y, today, yesterday, mtd, ytd, all, or an ISO date such as 2026-10-01. to is optional and defaults to now. A date in to includes that whole day. Ranges follow the project timezone. The default is the last 30 days (7 days for events and web analytics).
Intervals
hour, day (default), week or month. Weeks start on Monday. Hourly intervals work for up to 31 days.
Filters
{ "type": "event", "key": "amount", "operator": "gte", "value": 100 }type | Filters on |
|---|---|
event (default) | A property of the event, such as utm_source or $browser |
person | The person's current property, such as plan |
group | A property of the event's group. Add groupTypeIndex. |
cohort | Membership: operator in or not_in, value the cohort id |
| Operator | Meaning |
|---|---|
eq / neq | Equals / doesn't equal. An array works like in. |
in / not_in | Is (not) one of the values in an array |
contains / not_contains | Contains the text (not case-sensitive) |
regex | Matches a regular expression |
gt / gte / lt / lte | Number comparisons |
is_set / is_not_set | Has a value / doesn't. Leave out value. |
All filters in a list must match. For "either/or", use in with several values, or a cohort that matches any rule.
Breakdowns
{ "breakdown": { "type": "person", "key": "plan", "limit": 5 } }type is event, person, group (add groupTypeIndex) or cohort (with cohortIds, up to 10). limit keeps the most common values (10 by default, up to 50). Empty values show as (none).
Shared fields
trends, funnel, retention and events also take filters (applied everywhere) and cohortId (only people in that cohort).
trends
{
"kind": "trends",
"series": [{ "event": "order_completed", "math": "sum", "mathProperty": "amount" }],
"interval": "week",
"dateRange": { "from": "-12w" },
"compare": true
}| Field | |
|---|---|
series | 1–10 series: { event, math?, mathProperty?, groupTypeIndex?, filters?, name? }. event: null means all events. |
math | total (default), unique_persons, unique_sessions, unique_groups (with groupTypeIndex), or sum, avg, min, max, median, p90, p99 (with mathProperty) |
interval | See above |
breakdown | Optional |
compare | true adds the previous period |
display | line, area, bar, number or table. Only changes how the app draws it. |
The result has labels (bucket starts) and, per series, data (one value per label) and total (over the whole range). incompleteFrom is the index of a bucket that's still in progress.
order_completed · sum(amount): total 1,811,500; last complete week 427,391 (+0.6% vs previous week); peak 427,391 on 2026-10-26.
The last week is still in progress.funnel
{
"kind": "funnel",
"steps": [{ "event": "signed_up" }, { "event": "project_created" }, { "event": "invite_sent" }],
"windowDays": 14,
"dateRange": { "from": "-30d" }
}| Field | |
|---|---|
steps | 2–12 steps: { event, name?, filters? } |
windowDays | Days to convert from the first step, 1–365 (default 14) |
order | ordered (default) or strict |
breakdown | Optional. The value comes from the first step. |
aggregationGroupTypeIndex | Count companies (or another group type) instead of people |
Each step in the result has count, conversionFromStart, conversionFromPrevious and dropoff.
signed_up 1,759 → project_created 625 (35.5%) → invite_sent 194 (31.0%)
Overall conversion 11.0%. Biggest drop-off: project_created → invite_sent (69.0% lost).retention
{
"kind": "retention",
"startEvent": { "event": "signed_up" },
"returnEvent": { "event": "project_opened" },
"period": "week",
"periods": 8
}period is day, week (default) or month. periods is the number of columns, 2–30 (default 8). Each cohort in the result has a start, a size and values, the number of people who returned in each period. average is the weighted retention per period.
paths
{ "kind": "paths", "include": "pageviews", "startPoint": "/pricing", "steps": 5, "per": "session", "dateRange": { "from": "-7d" } }include is pageviews, events or both. startPoint and endPoint take a page path or an event name, and endPoint returns the steps leading up to it. steps is 2–8, and per is session or person. The result has nodes (id is "<step>:<name>"), links between them, and total.
lifecycle
{ "kind": "lifecycle", "event": { "event": "board_opened" }, "interval": "week", "dateRange": { "from": "-8w" } }Returns labels and, per period, new, returning, resurrecting and dormant counts. The current period's dormant is 0 until it ends.
stickiness
{ "kind": "stickiness", "series": [{ "event": "board_opened" }], "interval": "day", "dateRange": { "from": "-30d" } }For each event, counts[i] is how many people were active on exactly i + 1 days (or weeks, with "interval": "week"). periods is the number of days or weeks in the range.
events
{ "kind": "events", "event": "order_completed", "limit": 50 }Optional fields are event, personId, distinctId, groupTypeIndex + groupKey, limit (up to 1,000, default 100) and before. When truncated is true, pass the last row's timestamp as before to get the next page.
sql
{ "kind": "sql", "sql": "SELECT properties.plan AS plan, count() AS signups FROM events WHERE event = 'signed_up' GROUP BY plan" }One read-only SELECT (or WITH … SELECT) over your project's data, in ClickHouse SQL. Times are in your project's timezone, and person_id already accounts for merged identities, so uniq(person_id) counts real people. The SQL editor lists every table, column and property.
| Table | One row per |
|---|---|
events | Event. event, distinct_id, person_id, session_id, timestamp, properties, pathname, referrer_domain, utm_*, browser, os, device_type, country, group_0…group_4 |
persons | Person. id, properties, created_at |
sessions | Visit. started_at, duration_seconds, pageviews, entry_pathname, exit_pathname, referrer_domain, utm_source, is_bounce and more |
groups | Company or other group. group_type_index, group_key, properties |
| Any saved query | Whatever it returns: SELECT * FROM paying_users |
| Shorthand | Means |
|---|---|
properties.plan, properties['my key'] | A property as text |
toFloat64OrNull(properties.amount) | A property as a number |
person.properties.email (on events) | The person's property |
inCohort(person_id, 3) | Whether the person is in cohort 3 |
A query runs for at most 20 seconds and returns at most 10,000 rows. Add "display": { "type": "line", "x": "day", "y": ["users"] } to say how the result should be charted (table, line, bar or number).
Web analytics
| Kind | Fields | Returns |
|---|---|---|
web_overview | dateRange, filters, compare (default true) | Visitors, pageviews, sessions, bounce rate (0–1), average session duration (seconds) and views per session, each with the previous period |
web_stats | by, dateRange, filters, limit (default 20) | Top rows with visitors, views and bounce rate |
web_trend | dateRange, interval, filters | Visitors, pageviews and sessions per interval |
by is one of page, entry_page, exit_page, referrer, channel, utm_source, utm_medium, utm_campaign, country, device_type, browser, os or host. See Web analytics for what the numbers mean.
Cohort definitions
Cohorts aren't a query kind, but their rules use the same building blocks:
{
"match": "all",
"criteria": [
{ "type": "behavior", "event": "order_completed", "withinDays": 90, "minCount": 2 },
{ "type": "person_property", "key": "country", "operator": "in", "value": ["DE", "AT"] },
{ "type": "cohort", "cohortId": 3, "negate": true }
]
}| Rule | Fields |
|---|---|
person_property | key, operator, value |
behavior | event, withinDays (default 30), minCount (default 1), performed (false = did not do it), sumProperty + minSum (a total, such as amount ≥ 500), filters |
cohort | cohortId, negate |
match is all (default) or any, with 1–20 rules.
Errors
An invalid query returns 400 invalid_query with a message that names the field, and usually a hint:
{ "error": "invalid_query", "message": "Invalid date \"last week\"", "hint": "Use relative values like \"-7d\", \"-24h\", \"-12w\", \"-6m\", \"mtd\", \"ytd\", \"today\", or an ISO date \"2026-01-31\"" }