Saved queries
Build, name and reuse cross-table queries without writing SQL.
A saved query is a named, reusable question about your data, written once in the visual query builder and runnable everywhere. Lookup-table blocks and the AI assistant run saved queries directly, and a chart reads one by pointing at the lookup table that runs it, so a single query update propagates across every place it is used.
Why bother saving them
Three reasons:
- Reproducibility. "Mean concentration at 4 h grouped by dose group" is a specific question. Saving it under that name means everyone in the workspace can re-ask it later, exactly the same way.
- Surface area. A saved query becomes the data source for a lookup-table block, and through that block for a chart pointed at it. The AI assistant can list, run, create and update saved queries as well. Without saving, the same logic has to be re-built in each block.
- Update once. Change the saved query — for example, add an outlier filter — and every chart and lookup table that uses it updates next time it loads.
One shape: a traversal query
A saved query is always a traversal query. It starts at one table and walks reference columns outwards, returning a row per root record with the joined fields you selected: "Animals → their study group → its test article". There is no second kind of saved query to choose between.
Measurements are reached the same way, by hopping from an entity into the result schema that measures it. That hop is where aggregation happens, and it has two modes, picked on the hop's card:
- Aggregated
- One row per parent. Each selected measurement collapses with an aggregation, and an optional "Include group size (n)" column reports how many records went into it.
- Raw rows
- One row per result record, with the result dimensions as columns. Volume-guarded: a "Max rows per parent" cap keeps a wide plate from flooding the result, and a query may contain only one raw-rows hop.
A batch-policy dropdown on the same card decides which batches are read: Active batches, the default, or Include superseded.
Group keys must be entity columns. To break measurements down by a result
dimension such as timepoint_h, either use a Raw rows hop and let the chart do
the splitting, or ask the AI assistant, whose result-data tool groups on result
dimensions directly.
Inventory tables are first-class traversal citizens: root a query at an item
type, join it to the catalog table through the entity link, and aggregate
quantities. On top of their designed columns, inventory tables expose reserved
operational fields for live custody state — _status, _container_id,
_container_name, _position, _lot_number, _lot_id, _quantity_state, _unit (the item's unit code),
and the filter-only _location, which matches the placed container or any
ancestor (so "everything anywhere inside Freezer B" is one filter). Saved
queries over these answer recurring questions like "stock per compound" or
"what's in this freezer".
Building a query
The query builder is visual, with no SQL required.
- Source
Pick the environment and the starting table.
- Joins
Follow a reference column to pull in the table it points at, or open Referenced by on a card to hop in reverse. Reverse is how you reach a result schema from the entity it measures.
- Columns and grouping
Click a column to select it. A selected column carries a toggle that turns it into a group key; once anything is grouped, every other selected column has to be a group key too or carry an aggregation.
- Filters
Add filter chips with a column, an operator and a literal value:
dose_mg_per_kg = 30. A data column offers Equals and Not equals; text and enum columns add Contains; number, integer and date columns add Greater than, Greater or equal, Less than and Less or equal. Is empty and Is not empty take no value. The reserved inventory fields carry their own shorter operator lists:_location, for instance, filters only with Equals or Contains. - Aggregates and sort
On an Aggregated result hop each measurement gets an aggregation: mean, median, sum, min, max, count or latest. Pick a sort column and toggle ascending or descending. Under grouping, the sort has to target a group key or an aggregated column. Standard deviation, SEM and 95% CI are not query aggregations: they are error bars you switch on in the chart.
The builder shows a debounced live preview of the first 5 rows with a "+N more rows" count underneath, so you can sanity-check before saving. The preview runs at a fixed page size; consumers set their own when they execute the saved query.
A realistic example
For the DLA-7 PK study, a typical saved query is:
DLA-7 exposure by dose group
Source:
In-vivo PKenvironment, rooted atanimalsHop: reverse intoPK resultsthrough theanimalreference, Aggregated mode, Active batches Group by:animals.study_groupMeasurements:concentration_ug_ml→ mean, plus "Include group size (n)" Filters:study_group.test_articleequals DLA-7 Sort:study_groupascending
This one query drives:
- the lookup-table block in the study document
- a bar chart pointed at that lookup table, in the programme summary
- whatever the AI assistant needs when you ask "summarise this study's PK"
If you later add a filter to exclude QC failures, every consumer picks up the change.
For the time course itself, save a second query with the same source and hop set
to Raw rows, and drop the group key: grouping cannot combine with a rows-mode
result hop, so a grouped query fails validation on save. You get one row per
measurement with timepoint_h alongside concentration_ug_ml, and a line chart
over that lookup table averages per timepoint and splits by dose group.
Visibility scopes
A saved query lives in one workspace and is Private unless you say otherwise. There are three visibility levels, but the Save Query dialog only offers the first two:
| Scope | Where it shows up |
|---|---|
| Private | Only you can see and run it. The default. |
| Workspace | Visible to everyone in the workspace. What you promote a query to once it is worth sharing. |
| Template | System-provisioned and read-only: nobody can create, edit or delete a template query. |
Installing a package or a template does not leave you with template-visibility queries. Its saved queries are cloned into your workspace as ordinary Workspace-visibility ones, which you can then edit like any other.
Promoting a query from private to workspace does not change the query, it just makes it discoverable.
Making one query serve several studies
Saved queries have no variables to fill in. Filter values are literals, stored with the query. Two mechanisms vary the rows anyway:
- Scope. When the root table is project- or entry-scoped, the lookup-table block running the query has a three-way scope selector: This document, This project or All. The same saved query then returns this study's animals in one document and another study's in the next.
- Execute-time filters. Callers that run the query over the API, the SDK or
the AI tools can pass
additionalFilters,projectIdsandentryIds, which are AND-ed with the query's own filters.
If you need a filter bound to the surrounding document, for instance "where
Sample is the object this page is about", build the query inline on the
lookup-table block instead of saving it. The filter input's "use current…" menu
writes a context token (:objectId, :projectId, :entryId) that the block
resolves at run time, and that resolution only runs for inline queries.
Tips
"Mean concentration at 4 h, by dose group" is a useful name. "PK select join" is not. The name is what shows up in result lists and in the AI assistant's tool surface — make it self-explanatory.
Ask the in-product chat: "Draft a saved query that returns mean body weight per treatment group, rooted at animals, in environment In-vivo PK." It can build the query and save it for you.