> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselemma.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Querying analytics

> Ask a project for one aggregate over REST or MCP, with worked examples

A project analytics query returns one aggregate: a count, a percentile, a sum, or an estimated cost, optionally grouped by time or by a field. You send it over REST or the Lemma [Model Context Protocol (MCP)](/connections/mcp) server. This page explains how a query is built, then shows [examples](#examples) you can copy.

Use a query when one number or one breakdown answers the question. For a full [Analytics](/platform/analytics) panel, request a [preset view](#preset-views) instead.

<Note>
  You need an organization API key whose scope is read or admin, plus the project id. An ingest-only key cannot call these routes. A key limited to one project works only for that project. Create the key under **API Keys** in the [Lemma dashboard](https://platform.uselemma.ai).
</Note>

## Find events and fields in the catalog

Every query reads one event, such as `trace.processed` or `tool.called`. The catalog lists each event with the fields you can filter and group by (`dimensions`), the fields you can aggregate (`measures`, each with its allowed `aggs`), and any `cost_measures`. It also lists `filter_ops`, `buckets`, and `limits`.

```bash theme={null}
curl -s \
  "https://api.uselemma.ai/projects/your_project_id/analytics/catalog" \
  -H "Authorization: Bearer your_organization_api_key"
```

On MCP, call `describe_project_analytics` with `project_id`. A field that isn’t in the catalog can’t be queried.

## Send a query

`POST` the query body to `/projects/{project_id}/analytics/query`:

```bash theme={null}
curl -s -X POST \
  "https://api.uselemma.ai/projects/your_project_id/analytics/query" \
  -H "Authorization: Bearer your_organization_api_key" \
  -H "Content-Type: application/json" \
  -d @query.json
```

On MCP, call `query_project_analytics` with `project_id` plus the same body. MCP uses camelCase for three keys: `endExclusive`, `groupBy`, and `orderBy`. Field names such as `agent_name` stay snake\_case in both.

## Build the query body

A body names an event, a window, and at least one measure. Everything else narrows or shapes the result.

| Key | Required | What it does |
| - | - | - |
| `from` | Yes | The event to read, such as `trace.processed` |
| `start` | Yes | Window start, second-aligned UTC, inclusive |
| `end_exclusive` | Yes | Window end, second-aligned UTC, exclusive |
| `measures` | Yes | What to compute; each has an alias in `as` |
| `filters` | No | Conditions a row must match to be counted |
| `group_by` | No | Split the result by a time bucket or a field |
| `order_by` | No | Sort rows by an alias |
| `limit` | No | Maximum rows to return |
| `join` | No | Add measures from one other event |

### Window

`start` and `end_exclusive` must fall on a whole second, in UTC. The end is exclusive, so a window ending at `2026-09-30T00:00:00.000Z` stops before that instant. The window can’t be longer than the catalog maximum in `limits`.

### Measures

Each measure is `{ "as": "alias", "agg": "aggregation", "field": "field" }`. `count` counts rows and takes no `field`. Other aggregations, such as `sum`, `avg`, and `p95`, need a numeric `field` that lists that aggregation in its catalog `aggs`. `uniqExact` counts distinct values of a field.

A cost measure, `trace_cost` or `generation_cost`, takes no `field`. A query can have one cost measure. See [Cost estimates](#cost-estimates).

An alias names the column in each result row. It starts with a letter, then uses lowercase letters, digits, and underscores, up to 41 characters.

### Filters

Each filter is `{ "field": "field", "op": "operator", "value": "value" }`. The operators are `filter_ops` in the catalog:

* A comparison operator, such as `eq`, takes one `value`
* `in` takes an array of values
* `is_null` and `is_not_null` take no `value`

A row must match every filter to be counted.

### Group and order

A `group_by` entry is either a time bucket, `{ "bucket": "day", "as": "day" }`, or a field, `{ "field": "tool", "as": "tool" }`. The bucket names are in the catalog `buckets`.

An `order_by` entry is `{ "measure": "alias", "direction": "desc" }`. The alias can name a measure or a group. Without `order_by`, grouped rows are sorted by the group.

### Join

`join` adds measures from one other event, matched on a field both events have, such as `agent_name`. Cost measures belong on the primary event only.

## Read the response

A response has three keys:

* `rows`: one object per group, keyed by the aliases you chose
* `truncated`: true when the rows reached `limit`, or `limits.max_rows` if you set no limit
* `window`: the `start` and `end_exclusive` that were queried

When `truncated` is true, further rows exist. Narrow the filters or raise `limit`. REST and MCP return the same snake\_case shape.

Lemma returns HTTP 400 with the reason for an invalid query, and HTTP 404 for an unknown project. A query that runs longer than 15s fails.

## Examples

Each example below is a complete body. Save it as `query.json` and send it with the request in [Send a query](#send-a-query).

### Daily volume and latency for one agent

Count one agent’s traces per day, with the 95th percentile of root duration:

```json theme={null}
{
  "from": "trace.processed",
  "start": "2026-09-23T00:00:00.000Z",
  "end_exclusive": "2026-09-30T00:00:00.000Z",
  "filters": [
    {
      "field": "agent_name",
      "op": "eq",
      "value": "support_agent"
    }
  ],
  "group_by": [{ "bucket": "day", "as": "day" }],
  "measures": [
    { "as": "traces", "agg": "count" },
    { "as": "p95_ms", "agg": "p95", "field": "duration_ms" }
  ]
}
```

Each row is one day. `p95_ms` is in milliseconds.

```json theme={null}
{ "day": "2026-09-23", "traces": 120, "p95_ms": 840.5 }
```

### Tools that fail most

Rank tools by failed calls. On `tool.called`, `error` is `1` when the tool span failed and `0` otherwise, so its sum is the failure count:

```json theme={null}
{
  "from": "tool.called",
  "start": "2026-09-23T00:00:00.000Z",
  "end_exclusive": "2026-09-30T00:00:00.000Z",
  "group_by": [{ "field": "tool", "as": "tool" }],
  "measures": [
    { "as": "calls", "agg": "count" },
    { "as": "failures", "agg": "sum", "field": "error" }
  ],
  "order_by": [{ "measure": "failures", "direction": "desc" }],
  "limit": 20
}
```

Divide `failures` by `calls` for the failure rate. This row is 4.5%:

```json theme={null}
{ "tool": "search_docs", "calls": 400, "failures": 18 }
```

### Estimated cost by model

Estimate generation cost per model. `generation_cost` needs a `model` group with the alias `model`:

```json theme={null}
{
  "from": "generation.completed",
  "start": "2026-09-23T00:00:00.000Z",
  "end_exclusive": "2026-09-30T00:00:00.000Z",
  "group_by": [{ "field": "model", "as": "model" }],
  "measures": [{ "as": "cost", "agg": "generation_cost" }]
}
```

The row reports the estimate in `estimated_cost_usd`, not under the `cost` alias:

```json theme={null}
{ "model": "gpt-4o", "estimated_cost_usd": 12.4, "priced": true }
```

`priced: false` means Lemma has no list price for that model, and `estimated_cost_usd` is `null`.

### Traces and tool calls per agent

Join tool calls to traces on `agent_name`:

```json theme={null}
{
  "from": "trace.processed",
  "start": "2026-09-23T00:00:00.000Z",
  "end_exclusive": "2026-09-30T00:00:00.000Z",
  "group_by": [{ "field": "agent_name", "as": "agent_name" }],
  "measures": [{ "as": "traces", "agg": "count" }],
  "join": {
    "from": "tool.called",
    "on": "agent_name",
    "measures": [{ "as": "tool_calls", "agg": "count" }]
  }
}
```

```json theme={null}
{ "agent_name": "support_agent", "traces": 120, "tool_calls": 640 }
```

## Cost estimates

`trace_cost` and `generation_cost` are estimates from recorded token counts and public list prices, not an invoice. A backfilled row keeps the price from that backfill.

`trace_cost` runs on `trace.processed` and needs no `model` group. Its row includes `estimated_cost_usd` and `cost_lower_bound`. When `cost_lower_bound` is true, the estimate can be low, and `cost_lower_bound_reasons` lists why.

## Preset views

`GET /projects/{project_id}/analytics` returns one dashboard panel. Pass one `view`. MCP has one tool for each view. View names, parameters, and responses are on [Get project analytics](/api-reference/projects/get-project-analytics).

## Related pages

<CardGroup cols={2}>
  <Card title="Analytics" href="/platform/analytics">
    Dashboard charts for volume, latency, errors, tools, models, and cost.
  </Card>

  <Card title="Analytics catalog" href="/api-reference/projects/describe-project-analytics-catalog">
    Events, fields, filters, and limits for a project query.
  </Card>

  <Card title="Query project analytics" href="/api-reference/projects/query-project-analytics">
    The REST operation for one aggregate.
  </Card>

  <Card title="Analytics telemetry" href="/reference/analytics-telemetry">
    SDK fields the aggregates are computed from.
  </Card>
</CardGroup>
