AnalyticsCatalogRequest
No fields.
These independent analytics operations do not use the retired dashboard/KPI
surface. Both require the exact analytics:read API-key scope. Neither * nor
analytics:* substitutes for this scope.
| Operation | POST path | MCP tool |
|---|---|---|
ListCatalog | /public-api/publicapi.v1.AnalyticsService/ListCatalog | heyx_analytics_catalog |
ExecuteSQL | /public-api/publicapi.v1.AnalyticsService/ExecuteSQL | heyx_analytics_query |
ExecuteQuery | /public-api/publicapi.v1.AnalyticsService/ExecuteQuery | - |
The current actor must have Board full access and full access to each referenced domain. Membership, expiry, account/workspace lifecycle and live role authority are checked independently of API scopes. Restricted actors are rejected, not silently given partial totals. Queries run through the standard queued execution, admission, fairness and usage-accounting path. No query or widget is saved.
Call ListCatalog with {} first. It returns authorized entities, field types,
operators, aggregations, relationships, current-member support, sqlSupport and
engine limits.
The current roots are customers, appointments, orders, offers,
workorders, problems, payments and appointment_results. Offers are
workspace quotes; commercial orders and work orders are different counting
grains. fieldlessAggregations lists COUNT and RATIO.
POST /public-api/publicapi.v1.AnalyticsService/ListCatalog Lists authorized entities, fields, relationships, operations and limits.
No fields.
| Field | Type |
|---|---|
entities | repeated AnalyticsEntityDescriptor |
limits | AnalyticsLimits |
authorization_policy | string |
fieldless_aggregations | repeated AnalyticsAggregation |
sql_support | string |
The MCP tool accepts only a sql string. The public endpoint accepts the same
AnalyticsSQLRequest:
{ "sql": "SELECT count(*) AS count FROM customers WHERE heyx.timezone('UTC') LIMIT 100"}This is a restricted adapter over the typed query engine, not database SQL.
Submitted text is parsed and never sent to PostgreSQL. Use one virtual catalog
entity, start WHERE with heyx.timezone('IANA/Zone'), and include LIMIT 1..100.
Optional time predicates immediately follow the timezone:
SELECT scheduled_start_at AS day, count(*) AS countFROM appointmentsWHERE heyx.timezone('Europe/Amsterdam') AND heyx.calendar_range(scheduled_start_at, '2026-08-01', '2026-08-31') AND status <> 'cancelled'GROUP BY scheduled_start_atORDER BY day ASC NULLS LASTLIMIT 100The grammar also supports heyx.timestamp_range, contains, current_member
and exact correlated relationship EXISTS. sqlSupport is the authoritative
summary. Table aliases, joins, CTEs, arbitrary subqueries/functions, parameters,
casts, DML and schema-qualified or physical tables fail closed.
POST /public-api/publicapi.v1.AnalyticsService/ExecuteSQL Parses restricted SQL into the typed current-state query contract, then executes that contract. Submitted SQL is never sent to the database.
| Field | Type |
|---|---|
sql | string |
| Field | Type |
|---|---|
result | AnalyticsResult |
Count non-cancelled appointments by scheduled day in an explicit local period:
{ "query": { "version": 1, "entity": "appointments", "semantics": "ANALYTICS_QUERY_SEMANTICS_CURRENT_STATE", "timezone": "Europe/Amsterdam", "timeRange": { "field": "scheduled_start_at", "calendar": {"startDate": "2026-08-01", "endDate": "2026-08-31"} }, "filter": { "compare": { "field": "status", "operator": "ANALYTICS_OPERATOR_NEQ", "values": [{"text": "cancelled"}] } }, "aggregate": { "measures": [{"aggregation": "ANALYTICS_AGGREGATION_COUNT"}], "dimensions": [{"field": "scheduled_start_at", "bucket": "ANALYTICS_TIME_BUCKET_DAY"}] }, "sort": [{"column": "scheduled_start_at_day"}], "limit": 100 }}For raw records, replace aggregate with records: {"fields": ["id", "status"]}.
Only one operation is accepted. Sort columns must be selected output column IDs.
Unknown fields, operators, oneof combinations, executable input and unsupported
semantics fail rather than being ignored.
Filters use all/any groups containing children, typed compare nodes, or
related: {relationship, filter}. Each related filter is one correlated existence
test, preserving the root grain regardless of matching child count. Predicates
inside it match the same child row; separate related nodes can match different
rows. Catalogued member fields accept {"currentMember": true}. The primary
appointment assignee field does not represent secondary-assignee membership.
COUNT counts root rows and omits field. COUNT_DISTINCT counts non-null values.
SUM, AVG, MIN, MAX and PERCENTILE_DISC are available on catalogued compatible
fields. Discrete percentile uses percentile: {"unscaled": "95", "scale": 2}
for the 95th percentile and selects an original value without money interpolation.
Money aggregates require an unbucketed currency dimension. RATIO is the other
fieldless aggregation; see Ratios.
Dimension and measure id values are optional. Empty IDs normalize to field
for unbucketed dimensions, field_bucket for bucketed dimensions, count for
COUNT, and aggregation_field for other measures. Explicit IDs are preserved
and must be valid and unique. Generated collisions receive deterministic _2,
_3, and later suffixes after all explicit IDs are reserved. Sort may reference
these generated aliases; an empty sort column is invalid. The response’s
normalizedQuery always includes the resolved IDs.
Calendar dates are inclusive in the query timezone; timestamp ranges use an
inclusive start and exclusive end, encoded as RFC3339 strings with at most
microsecond precision. Empty timezone uses the workspace timezone. DAY, WEEK
(Monday start), MONTH, QUARTER and YEAR buckets require an explicit range.
Time buckets are sparse: absent periods are not fabricated zeroes.
timeRange.relative resolves on the server at execution time in the query
timezone. Periods are TODAY, THIS_WEEK (ISO Monday start), THIS_MONTH,
THIS_QUARTER, THIS_YEAR, and the rolling LAST_7_DAYS, LAST_30_DAYS,
LAST_90_DAYS (ending today inclusive) and LAST_12_MONTHS (whole months,
ending this month). offset shifts whole periods within -24..24: -1 is the
previous period, and rolling periods shift by their own length. The response’s
resolvedTimeRange carries the absolute half-open bounds while normalizedQuery
keeps the relative form, so a saved query stays current.
{ "timeRange": { "field": "scheduled_start_at", "relative": {"period": "ANALYTICS_RELATIVE_PERIOD_THIS_MONTH", "offset": -1} }}Period-over-period comparison is two requests with different offsets, not one query.
On TIMESTAMP fields a comparison value may be {"nowOffsetSeconds": "-86400"}:
execution time plus the signed offset, bounded to five years. It is input-only
and preserved as-is in normalizedQuery.
{ "compare": { "field": "created_at", "operator": "ANALYTICS_OPERATOR_GTE", "values": [{"nowOffsetSeconds": "-604800"}] }}measure.condition is an ordinary filter (same fields, operators, related,
currentMember and nowOffsetSeconds rules as the root filter). Only root rows
matching it contribute to that measure, so one grouped query can carry several
differently filtered measures. Condition nodes count toward the 100-node filter
budget. SUM over no matching rows is NULL; COUNT is zero.
{ "id": "cancelled", "aggregation": "ANALYTICS_AGGREGATION_COUNT", "condition": { "compare": {"field": "status", "operator": "ANALYTICS_OPERATOR_EQ", "values": [{"text": "cancelled"}]} }}ANALYTICS_AGGREGATION_RATIO is fieldless. numerator and denominator name
two sibling non-RATIO measures in the same aggregate whose output type is
INTEGER or DECIMAL (money, timestamp and text measures are rejected). Omit
field, percentile and condition on the ratio itself; put conditions on the
sibling measures. The result is a DECIMAL and is NULL when the denominator is
zero or NULL. An empty id normalizes to <numerator>_per_<denominator>.
{ "aggregate": { "measures": [ {"id": "total", "aggregation": "ANALYTICS_AGGREGATION_COUNT"}, {"id": "cancelled", "aggregation": "ANALYTICS_AGGREGATION_COUNT", "condition": {"compare": {"field": "status", "operator": "ANALYTICS_OPERATOR_EQ", "values": [{"text": "cancelled"}]}}}, {"id": "cancel_rate", "aggregation": "ANALYTICS_AGGREGATION_RATIO", "numerator": "cancelled", "denominator": "total"} ], "dimensions": [{"field": "scheduled_start_at", "bucket": "ANALYTICS_TIME_BUCKET_WEEK"}] }}Relative ranges, now-relative values, conditional measures and ratios are
available only through ExecuteQuery. The restricted SQL grammar used by
ExecuteSQL and the heyx_analytics_query MCP tool has no syntax for them.
All queries describe current persisted state. Time filters select current rows; they do not reconstruct historical status, ownership or balances. There are no implicit archive, cancellation, date or personal-scope filters. Historical as-of, event-history reconstruction, funnels, arbitrary formulas beyond sibling ratios, custom fields and arbitrary JSON paths are unsupported.
POST /public-api/publicapi.v1.AnalyticsService/ExecuteQuery Executes a typed current-state aggregate or bounded record projection. No saved widget, executable code, history or implicit status filters.
| Field | Type |
|---|---|
query | AnalyticsQuery |
| Field | Type |
|---|---|
result | AnalyticsResult |
The response is {result} with ordered columns and matching rows[].cells,
normalizedQuery, generatedAt, resolvedTimeRange, timezone, status,
truncated, warnings and aggregationComplete. SQL responses also include
canonical normalizedSql. Preserve the normalized forms for follow-up requests
instead of guessing date windows or changing the query grain.
Result status is READY or EMPTY. Cells distinguish READY, NULL and UNAVAILABLE; non-ready cells have no value. An authorized empty COUNT is READY zero. A failed query is an error, never an empty population or zero. Unresolved commercial snapshots make their affected grouped monetary measure UNAVAILABLE rather than silently producing an incomplete amount.
Integer values use quoted int64 JSON strings. Decimal is {unscaled, scale}:
its value is unscaled * 10^-scale. Money is {amount: Decimal, currency} in
major units. Do not convert exact
values through JavaScript Number. Different currencies and one-time, monthly and
yearly amounts are never combined automatically.
The engine allows a 32 KiB submitted SQL string, 100 output rows, 100 filter nodes, eight filter levels, two
relationship hops, eight measures, three dimensions, 20 projected fields, a
32 KiB protobuf query, a five-year range and a 15-second execution deadline.
The public JSON request envelope is limited to 256 KiB. Results are complete
core projections, bounded to 256 KiB in public protobuf and JSON. Oversize results
fail without partial rows. truncated means more output rows/groups exist;
aggregates are computed before that output limit. aggregationComplete does not
mean every group was returned or every measure was available.
MCP additionally bounds its combined text and structured-content result envelope to 256 KiB. A result that fits Connect may therefore exceed MCP’s limit. Narrow filters/projection or lower the output limit; an oversize response is not an empty result. Read-only retries re-execute under current data and authority; they do not promise an identical snapshot.