analytics
List Charts
Every chart this organization has saved, by name, each with the spec it
runs. Use when listing what is already saved, before picking one to run or
edit. Changes nothing; run one with POST /analytics/charts/{chart_id}/run.
List Charts › Responses
Successful Response
created_atdescriptionidnameorg_idA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
updated_atCreate Chart
Save a chart, its spec validated against the dataset catalogue exactly
like preview_chart's. Use once a previewed spec is worth keeping and
running again later. No zone is resolved or stored; whoever opens it later
reads it in their own.
Create Chart › Request Body
nameA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
descriptionCreate Chart › Responses
Successful Response
created_atdescriptionidnameorg_idA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
updated_atPreview Chart
Execute an unsaved chart spec — the builder's engine; nothing needs to
be saved to build, demo or test a chart. Use while shaping a chart, before
deciding it is worth saving with create_chart.
The spec carries no timezone; days are cut in the acting person's own zone,
resolved here (PersonTimezoneDep: stated person, else the organization's,
else UTC) — analytics.md, "a chart's days belong to whoever is looking
at it".
Preview Chart › Request Body
chart_typedatasetPreview Chart › Responses
Successful Response
Get Chart
One saved chart and its spec — what it is called, what it measures and the window rule it covers. Use when opening a specific saved chart by id. Changes nothing. 404 for a chart of another organization, the same as for one that does not exist.
path Parameters
chart_idGet Chart › Responses
Successful Response
created_atdescriptionidnameorg_idA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
updated_atDelete Chart
Delete a saved chart, everywhere. Use when the chart itself should stop existing, not just come off one board — there is no undo. It stops drawing on every board it was placed on, and nothing brings it back — taking it off one board is a different act, an edit to that board's layout.
path Parameters
chart_idDelete Chart › Responses
Successful Response
Update Chart
Change a chart's name, description or spec — only the fields given. Use
to rename a chart or rework what it measures without losing which boards it
is placed on. A new spec is validated like create_chart's and resolves no
zone.
path Parameters
chart_idUpdate Chart › Responses
Successful Response
created_atdescriptionidnameorg_idA chart's whole definition — what analytics_charts.spec stores, and
what POST /analytics/charts/preview runs unsaved. dataset is one of the
catalogue's keys (GET /analytics/datasets lists them): "interactions",
"tool_executions", "node_traversal", "usage", the Analyst's
"analyst_readings" and "analyst_answers", or "collection:<uuid>" for one
of the organization's own custom_data collections.
There is no tz here, and its absence is the design.
docs/features/analytics.md: "a chart's days belong to whoever is looking
at it — the zone is resolved every time a chart is read, and is never saved
with it" (owner, 2026-09-15). This is the same rule window already obeys
one field up: what is stored is the question that was asked, not what that
question happened to mean at the moment of saving. The zone arrives instead
as a per-request argument to service.run_spec/run_chart, resolved at the
edge by backend.api.deps.get_person_timezone from the person doing the
reading.
A spec read back out of JSONB may still carry a tz key, and it is
ignored. Pydantic's default extra="ignore" does that with no branch to
maintain and nothing to remove later; DECISIONS.md records why this shape
is tolerated where the pre-series shape was rewritten by a migration
instead.
updated_atRun Chart
Execute a saved chart. Use when actually drawing a saved chart for someone to look at. The body may override its window and add filters for this run only — a board's own global filters, when this is called for a chart placed on one — and neither is written back to the saved chart.
The days are the reader's, every time. The zone is resolved here from whoever is asking, never from the chart, so the same saved chart opened by two people in one organization is cut into each of their own days.
path Parameters
chart_idRun Chart › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object | |
| type = null |
Run Chart › Responses
Successful Response
List Datasets
The dataset catalogue this organization may chart over: which datasets exist, their dimensions, their measures and their filterable columns. Use when building a chart spec, to know what fields are available to measure and filter on. The chart editor's entire vocabulary is data-driven from this.
List Datasets › Responses
Successful Response
datasetlabelList Boards
Every board this organization has saved, by name, each with its layout and its global filters. Use when listing saved dashboards before opening one. Changes nothing.
List Boards › Responses
Successful Response
created_atidA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
nameorg_idupdated_atCreate Board
Save a board: an arrangement of saved charts, and the board's own global filters. Use when putting together a dashboard view out of charts that already exist. Every chart id the layout names is checked against this organization before it is written.
Create Board › Request Body
nameA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
Create Board › Responses
Successful Response
created_atidA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
nameorg_idupdated_atGet Traversal
The dashboard tree graph: one agent's aggregate node-traversal —
entries, transitions and drops per node — over one window. Use when
showing how conversations flowed through an agent's flow over time.
Changes nothing. An agent with no traversal in the window answers with
empty nodes/edges; an
agent_id that is not one of this organization's agents — another
organization's, a deleted one, or none at all — is a 404.
The window is the same three shapes a chart's is — an absolute
since+until, a relative last+unit, or all_time — because
dashboard.md makes the tree graph a widget like any other, carrying its
own period, and a board that stored one as a rule (BoardChartPlacement .window) must be able to send that rule rather than the dates it meant.
Resolving it in the browser instead would put this slice's calendar
arithmetic into a second implementation nobody checks — see
service._minus for why months are not thirty-day blocks.
Exactly one shape, or none (the last 30 days). Half a shape — one of
since/until, one of last/unit — is refused (422) rather than
silently answered about a different window than the one asked for.
since and until must each carry a UTC offset (2026-01-01T00:00:00Z or
2026-01-01T01:00:00+01:00). A timestamp without one is refused with 422 naming
the field, rather than being guessed at.
include_tests=true counts the conversations marked as a test against a
draft as well; they are left out otherwise, as they are from every chart
(analytics.md rule 12, #571).
query Parameters
agent_idsinceuntillastunitall_timeinclude_testsGet Traversal › Responses
Successful Response
captruncatedGet Board
The board's layout and the charts it names, by id. Use when opening a
saved dashboard to render it. Changes nothing. A client reads each named
chart through the chart routes; running one with this board's
filters attached is POST /analytics/charts/{chart_id}/run's filters.
path Parameters
board_idGet Board › Responses
Successful Response
created_atidA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
nameorg_idupdated_atDelete Board
Delete a board: its layout and its global filters. Use when a dashboard arrangement itself should stop existing. The charts it placed are the organization's own objects and survive it — this deletes the arrangement, not what was arranged.
path Parameters
board_idDelete Board › Responses
Successful Response
Update Board
Change a board's name or layout — only the fields given. Use when rearranging a dashboard's charts or renaming it.
path Parameters
board_idUpdate Board › Request Body
Update Board › Responses
Successful Response
created_atidA board's arrangement, and its own global filters
(docs/features/dashboard.md:7) — merged into every chart's spec at run
time by run_chart/get_board and never written back into the chart
itself (§3.5 of the design study).
nameorg_idupdated_at