Analytics query cookbook

Copy-paste recipes for common questions. Each is the query value you send to POST /query. See the Schema reference for every column.

Reminder: monetary columns are cents (divide by 100; fact_cost.cost_amount is fractional cents), and UInt64/Int64 results come back as JSON strings.

Prerequisites

  • A Paid API key with access to your organization’s Analytics API.
  • Signals, costs, delivered value, invoices, or credit activity already present for the tables you want to query.
  • Familiarity with the table names and joins in the Schema reference.

Usage & signals

Total signals in the last 30 days

1SELECT count() AS signals
2FROM fact_signal
3WHERE created_at > now() - INTERVAL 30 DAY

Signal volume by type

1SELECT signal_name, count() AS signals
2FROM fact_signal
3WHERE created_at > now() - INTERVAL 30 DAY
4GROUP BY signal_name
5ORDER BY signals DESC

Daily signal time-series

1SELECT toDate(created_at) AS day, count() AS signals
2FROM fact_signal
3WHERE created_at > now() - INTERVAL 30 DAY
4GROUP BY day
5ORDER BY day

Signals by customer (resolving names)

1SELECT c.name AS customer, count() AS signals
2FROM fact_signal s
3JOIN dim_customer c ON s.customer_id = c.id
4WHERE s.created_at > now() - INTERVAL 30 DAY
5GROUP BY customer
6ORDER BY signals DESC
7LIMIT 20

Querying signal metadata

Signals carry a free-form JSON data payload. Access a field with data.<path> and cast it to a type. First discover available keys with GET /signals/metadata, then filter or group on them.

Casting a missing key with ::String yields an empty string (''), not NULL, so a '' bucket in a grouped result represents signals that don’t carry that key.

Group by a string metadata field

1SELECT data.opportunity_stage::String AS stage, count() AS signals
2FROM fact_signal
3WHERE created_at > now() - INTERVAL 30 DAY
4GROUP BY stage
5ORDER BY signals DESC

Filter on a metadata field

1SELECT count() AS completed_calls
2FROM fact_signal
3WHERE signal_name = 'call'
4 AND data.call_status::String = 'completed'
5 AND created_at > now() - INTERVAL 7 DAY

Aggregate a numeric metadata field

1SELECT
2 data.account_tier::String AS tier,
3 avg(data.call_length_sec::Float64) AS avg_call_seconds
4FROM fact_signal
5WHERE signal_name = 'call'
6 AND created_at > now() - INTERVAL 30 DAY
7GROUP BY tier

AI cost

cost_amount is exact fractional cents (a typical AI call costs a fraction of a cent), so SUM(cost_amount) / 100 gives exact dollars and matches the Cost Explorer in the Paid UI.

Total spend (dollars) by vendor

1SELECT vendor, count() AS calls, sum(cost_amount) / 100 AS dollars
2FROM fact_cost
3WHERE start_time > now() - INTERVAL 30 DAY
4GROUP BY vendor
5ORDER BY dollars DESC

Spend by model with token usage

1SELECT
2 model,
3 count() AS calls,
4 sum(input_tokens) AS input_tokens,
5 sum(output_tokens) AS output_tokens,
6 sum(cost_amount) / 100 AS dollars
7FROM fact_cost
8WHERE start_time > now() - INTERVAL 30 DAY
9GROUP BY model
10ORDER BY dollars DESC

Daily cost trend

1SELECT toDate(start_time) AS day, sum(cost_amount) / 100 AS dollars
2FROM fact_cost
3WHERE start_time > now() - INTERVAL 30 DAY
4GROUP BY day
5ORDER BY day

Cost by customer

1SELECT c.name AS customer, sum(co.cost_amount) / 100 AS dollars
2FROM fact_cost co
3JOIN dim_customer c ON co.customer_id = c.id
4WHERE co.start_time > now() - INTERVAL 30 DAY
5GROUP BY customer
6ORDER BY dollars DESC
7LIMIT 20

Cost per signal (joining usage and cost)

Signals and costs share a trace_id, so you can attribute AI cost to the signal that incurred it.

1SELECT
2 s.signal_name,
3 count(DISTINCT s.signal_id) AS signals,
4 sum(co.cost_amount) / 100 AS dollars,
5 sum(co.cost_amount) / 100 / count(DISTINCT s.signal_id) AS dollars_per_signal
6FROM fact_signal s
7JOIN fact_cost co ON s.trace_id = co.trace_id
8WHERE s.created_at > now() - INTERVAL 30 DAY
9GROUP BY s.signal_name
10ORDER BY dollars DESC

Revenue

Monthly revenue (posted invoices)

1SELECT
2 toStartOfMonth(issue_date) AS month,
3 sum(total_incl_tax) / 100 AS revenue
4FROM fact_invoice
5WHERE status = 'posted'
6 AND issue_date > now() - INTERVAL 12 MONTH
7GROUP BY month
8ORDER BY month

Revenue by customer

1SELECT c.name AS customer, sum(i.total_incl_tax) / 100 AS revenue
2FROM fact_invoice i
3JOIN dim_customer c ON i.customer_id = c.id
4WHERE i.status = 'posted'
5GROUP BY customer
6ORDER BY revenue DESC
7LIMIT 20

Revenue by product (invoice lines → order lines → products)

1SELECT p.name AS product, sum(il.total_after_tax) / 100 AS revenue
2FROM fact_invoice_line il
3JOIN fact_order_line ol ON il.order_line_id = ol.id
4JOIN dim_product p ON ol.product_id = p.id
5GROUP BY product
6ORDER BY revenue DESC

Fixed-fee vs usage revenue split (posted invoices)

charge_type is usage for metered lines and oneTime/recurring/seatBased for fixed charges. It is empty on manual lines (no linked pricing) and on lines predating charge-type attribution. Bucket those as unknown rather than folding them into fixed, or they will inflate the fixed-fee share.

1SELECT
2 multiIf(
3 il.charge_type = 'usage', 'usage',
4 il.charge_type IN ('oneTime', 'recurring', 'seatBased'), 'fixed',
5 'unknown'
6 ) AS billing_basis,
7 count() AS lines,
8 sum(il.total_after_tax) / 100 AS revenue
9FROM fact_invoice_line il
10JOIN fact_invoice i ON il.invoice_id = i.id
11WHERE i.status = 'posted'
12GROUP BY billing_basis
13ORDER BY revenue DESC

A non-trivial unknown total means manual/unattributed lines carry real revenue. inspect them before treating the fixed-vs-usage split as complete. For the full breakdown, group by il.charge_type directly (oneTime / recurring / seatBased / usage, plus empty for unattributed).

Outstanding / overdue invoices

1SELECT c.name AS customer, sum(i.amount_due) / 100 AS outstanding
2FROM fact_invoice i
3JOIN dim_customer c ON i.customer_id = c.id
4WHERE i.payment_status IN ('pending', 'partiallyPaid', 'overdue')
5GROUP BY customer
6ORDER BY outstanding DESC

Payments / cash flow

1SELECT toDate(payment_date) AS day, sum(amount) / 100 AS collected
2FROM fact_payment
3WHERE payment_status = 'posted'
4 AND payment_date > now() - INTERVAL 30 DAY
5GROUP BY day
6ORDER BY day

Billed usage

Which signals drive billed revenue

1SELECT signal_name, sum(subtotal) / 100 AS billed
2FROM fact_usage_summary
3WHERE status = 'billed'
4GROUP BY signal_name
5ORDER BY billed DESC

Overage (usage above the included allowance)

1SELECT
2 signal_name,
3 sum(signal_count) AS used,
4 sum(included_quantity) AS included,
5 sum(greatest(signal_count - included_quantity, 0)) AS overage
6FROM fact_usage_summary
7WHERE included_quantity IS NOT NULL
8GROUP BY signal_name

Credits

Net credit flow by currency

1SELECT cc.name AS currency, sum(t.amount) AS net_credits
2FROM fact_credit_transaction t
3JOIN dim_credits_currency cc ON t.credits_currency_id = cc.id
4GROUP BY currency

Top credit-spending signals

1SELECT signal_name, -sum(amount) AS credits_spent
2FROM fact_credit_transaction
3WHERE type = 'SPEND'
4 AND created_at > now() - INTERVAL 30 DAY
5GROUP BY signal_name
6ORDER BY credits_spent DESC

Credit consumption by user within a customer

If your signals include external_user_id in the data payload (see First signals), you can break down credit spend per user:

1SELECT
2 s.data.external_user_id::String AS external_user_id,
3 cc.name AS currency,
4 -sum(t.amount) AS credits_spent
5FROM fact_credit_transaction t
6JOIN fact_signal s ON t.signal_mongo_id = s.signal_id
7JOIN dim_credits_currency cc ON t.credits_currency_id = cc.id
8WHERE t.type = 'SPEND'
9 AND t.created_at > now() - INTERVAL 30 DAY
10GROUP BY external_user_id, currency
11ORDER BY credits_spent DESC

Delivered value

delivered_value_cents is denominated in each row’s delivered_value_currency, which is not always USD — organizations using value models can deliver value in EUR, GBP, etc. Always group by (or filter on) delivered_value_currency; summing cents across currencies produces meaningless totals. Cents are exact and may carry fractions, so round after aggregating.

Value delivered by type

1SELECT
2 value_type_name,
3 delivered_value_currency AS currency,
4 round(sum(delivered_value_cents) / 100, 2) AS value_amount,
5 sum(unit_value) AS total_units
6FROM fact_delivered_value
7WHERE created_at > now() - INTERVAL 30 DAY
8GROUP BY value_type_name, delivered_value_currency
9ORDER BY value_amount DESC

Value vs. cost (ROI) by customer

1SELECT
2 c.name AS customer,
3 dv.delivered_value_currency AS currency,
4 round(sum(dv.delivered_value_cents) / 100, 2) AS value_amount
5FROM fact_delivered_value dv
6JOIN dim_customer c ON dv.customer_id = c.id
7WHERE dv.created_at > now() - INTERVAL 30 DAY
8GROUP BY customer, currency
9ORDER BY value_amount DESC
10LIMIT 20

Paginating large result sets

Results are capped at 10,000 rows. For larger extracts, page with a WHERE filter on an ordered column (keyset pagination) rather than a large OFFSET:

1SELECT signal_id, signal_name, created_at
2FROM fact_signal
3WHERE created_at < '2026-06-01 00:00:00' -- last value from the previous page
4ORDER BY created_at DESC
5LIMIT 1000