data_aggregate
Aggregate rows SERVER-SIDE — stats over a whole (optionally filtered) table WITHOUT pulling the rows into context (feedback #191). Use this for totals and distributions over a large vivier (e.g. total kWc, average score, count per department) instead of reading 300+ rows and summing them yourself.
metrics = list of {op, field?}; op ∈ count|count_rows|sum|avg|min|max
(default [{"op":"count"}]). count without field = total rows;
sum/avg/min/max require a numeric field and ignore non-numeric values.
group_by = a column to group on (omit = one global row). Results are sorted
by the first metric descending when grouped (so group_by gives you the TOP
groups first).
A metric may carry its OWN condition — where, same clauses as filters —
so the total and a subset are counted in the SAME query. That is how you get a
RATE without crossing two calls whose scopes can silently differ. Give such a
metric a label, and it comes back under that name.
group_by also accepts a LIST of columns: their values are POOLED, one row
contributing one occurrence per filled column ("all ranks together"). Under a
pooled group, count counts OCCURRENCES and count_rows counts ROWS — two
different questions, so ask for the one you mean.
A list column (e.g. contacts) is aggregated ACROSS ITS ITEMS with
contacts[].<attribute>. group_by: "contacts[].fonction" makes every item of
every matching row one occurrence — same vocabulary: count counts the
contacts, count_rows the rows; a contact without that attribute falls in the
null group. filters still select ROWS (a row kept by
contacts[].fonction eq DRH contributes ALL its contacts). sum/avg/min/max
with field: "contacts[].<numeric attribute>" aggregate over all items (read
on the current item when grouping by the same list). A list path cannot be
pooled with other columns, nor sorted; contacts[0].x targets one rank.
A list of plain VALUES (e.g. tags) is read element by element with
tags[]: group_by: "tags[]" counts each tag (count) and the rows carrying
it (count_rows); filters on tags[] keep the rows holding the value.
⚠️ Pooling is NOT a two-dimensional group-by, and group_by: "a,b" is not one
either — it is REFUSED (oto#50). A comma-separated string used to be read as a
single column name containing a comma: no row carries it, so you got 200 with
ONE group of key null holding every row, and no way to tell that from empty
data. Group on one column per call, or pass the list if you meant to pool.
Crossing two dimensions is not served yet.
Returns {results: [...]} — each entry carries the group_by value (when set)
plus one key per metric (count, sum_<field>, avg_<field>, or label).
count_by = a list of columns to profile in ONE call (not with group_by
or metrics): returns {counts: {column: {values: [{value, count}], empty, distinct, total, truncated}}} — most frequent values first, at most
values_limit (default 50, max 500) per column; truncated says more exist.
Examples —
- total rows matching a filter: metrics omitted, filter={"statut":"qualified"}
- MWc by department: group_by="departement",
metrics=[{"op":"sum","field":"kwc_estime"}, {"op":"count"}]
- share of companies with an HR contact on ANY rank, by headcount band —
one call, no rows pulled:
group_by="tranche_effectif",
metrics=[{"op":"count","label":"fiches"},
{"op":"count","label":"avec_rh","where":[
{"fields":["contact1_fonction","contact2_fonction",
"contact3_fonction"],
"op":"in","value":["DRH","DAF"]}]}]
- which roles appear across all contact ranks:
group_by=["contact1_fonction","contact2_fonction","contact3_fonction"]
- the same on a list column, contacts and companies counted apart:
group_by="contacts[].fonction",
metrics=[{"op":"count"}, {"op":"count_rows"}]
Tool call template
Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {
"datastore": "<datastore>"
},
"name": "data_aggregate"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | free-text search over the row. Every WORD of `q` must be found, in any order, either by meaning (French stemming, accents and case folded: `cahiers` finds "Le Cahier", `corp acme` finds "ACME Corp") or as a literal FRAGMENT (a piece of a SIREN, an URL or a reference pasted as is). Without an explicit sort, best matches come first (rows matched by meaning, then rows matched only by fragment); an explicit sort stays yours. Where to search is `q_scope`. Aggregates the same set a `data_rows` search shows (ranking aside). | |
| _org | No | Org (id) de CET APPEL. `oto_list_orgs()`. | |
| _group | No | Équipe (id) de CET APPEL, sous son org parente. | |
| filter | No | dict `{column: value}` exact match to scope the aggregate. | |
| _run_id | No | OBLIGATOIRE à chaque appel dès `run_start` ou `data_claim_next` : sans lui, l'écriture sur une ligne réservée est refusée. Hérité seulement si le serveur tient un run actif — n'y compte pas. | |
| filters | No | list of clauses, incl. multi-column ones — same grammar as `data_rows.filters`. Combines with `filter` (AND). | |
| metrics | No | list of `{op, field?, where?, label?}` aggregations (default = count of rows). | |
| q_scope | No | where `q` searches: `values` = only the values a reader is served (no column names, no layers — `origine`/`comment`/`link` — no JSON quoting); `all` = the whole stored row, names and layers included (finds a row by its `comment`). Default `all`. Any other value is refused. | |
| _project | No | Projet (id) de CET APPEL — jeton PRIMAIRE. `oto_project(op='list')`. | |
| count_by | No | columns to count values and empties of, in one call. | |
| group_by | No | column to group by, `list[].attribute` to group the items of a list column, or a LIST of columns whose values are pooled (omit = global aggregate, single row). | |
| datastore | Yes | target datastore, or `slot:<name>` (active project). | |
| values_limit | No | with `count_by`, values kept per column (1-500). |