Dataset Query API
The dataset query API reads only the files for the requested dataset.
Use a PAT, not a project API key.
POST /api/datasets/:projectSlug/:datasetId/query
Authorization: Bearer pat_xxx
Content-Type: application/json There is no raw SQL endpoint. Queries use a builder-style JSON shape that Loguro validates before generating SQL.
For TypeScript projects, @loguro/datasets exposes a typed query DSL that serializes to this JSON shape. See Datasets — TypeScript SDK.
The legacy project-level query endpoint still exists for the default dataset:
POST /api/datasets/:projectSlug/query Use dataset-specific queries for new code.
Request shape
{
"range": {
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-10T23:59:59Z"
},
"select": [
{ "field": "country" },
{ "aggregate": "count", "as": "events" }
],
"where": [
{ "field": "plan", "op": "=", "value": "pro" },
{ "field": "amount", "op": ">", "value": 20 }
],
"groupBy": ["country"],
"orderBy": [
{ "field": "events", "direction": "desc" }
],
"limit": 100
} All top-level properties are optional. If omitted:
rangedefaults to the last 7 days.selectdefaults tocount.limitdefaults to100.limitis hard-capped at1000.
Queryable fields
You can query:
- declared schema fields for that dataset
timestamp
You cannot filter, group, or aggregate on context in v1.
If the dataset exists but has not registered a schema yet, the API returns:
{ "error": "Dataset schema not found" } Select fields
Select raw fields:
{
"select": [
{ "field": "timestamp" },
{ "field": "country" },
{ "field": "plan" }
],
"limit": 50
} When selecting raw fields with groupBy, every non-aggregate selected field must also be in groupBy.
Valid:
{
"select": [
{ "field": "country" },
{ "aggregate": "count", "as": "events" }
],
"groupBy": ["country"]
} Invalid:
{
"select": [
{ "field": "country" },
{ "field": "plan" },
{ "aggregate": "count", "as": "events" }
],
"groupBy": ["country"]
} plan is selected but not grouped.
Aggregates
Supported aggregates:
| Aggregate | Field required | Compatible fields |
|---|---|---|
count | no | all records, or a field if supplied |
sum | yes | number |
avg | yes | number |
min | yes | number, string, boolean, timestamp |
max | yes | number, string, boolean, timestamp |
approx_count_distinct | yes | declared fields and timestamp |
Examples:
{ "aggregate": "count", "as": "events" } { "aggregate": "sum", "field": "amount", "as": "revenue" } { "aggregate": "approx_count_distinct", "field": "customer_id", "as": "customers" } Aliases must be simple identifiers:
^[A-Za-z_][A-Za-z0-9_]{0,63}$ Filters
Supported operators:
| Operator | Value | Notes |
|---|---|---|
= | required | exact match |
!= | required | not equal |
> | required | numbers and timestamps are most common |
>= | required | numbers and timestamps are most common |
< | required | numbers and timestamps are most common |
<= | required | numbers and timestamps are most common |
in | non-empty array | each value must match the field type |
contains | string | string fields only |
startsWith | string | string fields only |
exists | none | field is not null |
Examples:
{ "field": "plan", "op": "=", "value": "pro" } { "field": "amount", "op": ">", "value": 20 } { "field": "country", "op": "in", "value": ["RO", "US", "DE"] } { "field": "plan", "op": "startsWith", "value": "pro" } { "field": "signup_at", "op": ">=", "value": "2026-06-01T00:00:00Z" } Range
range always filters the standard timestamp field.
{
"range": {
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-10T23:59:59Z"
}
} If only from is supplied, the query has a lower bound. If only to is supplied, the query has an upper bound.
If range is omitted, Loguro queries the last 7 days.
Ordering
Order by selected aliases or queryable fields:
{
"orderBy": [
{ "field": "events", "direction": "desc" }
]
} direction can be asc or desc. Omitted or unknown values default to descending.
Complete examples
Revenue by country:
{
"range": {
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-10T23:59:59Z"
},
"select": [
{ "field": "country" },
{ "aggregate": "count", "as": "events" },
{ "aggregate": "sum", "field": "amount", "as": "revenue" }
],
"where": [
{ "field": "plan", "op": "in", "value": ["pro", "team"] }
],
"groupBy": ["country"],
"orderBy": [{ "field": "revenue", "direction": "desc" }],
"limit": 50
} Recent records:
{
"select": [
{ "field": "timestamp" },
{ "field": "country" },
{ "field": "plan" },
{ "field": "amount" }
],
"orderBy": [{ "field": "timestamp", "direction": "desc" }],
"limit": 100
} Unique customers:
{
"select": [
{ "aggregate": "approx_count_distinct", "field": "customer_id", "as": "customers" }
],
"where": [
{ "field": "customer_id", "op": "exists" }
]
} Empty datasets
If the dataset has a schema but no matching files, the query returns:
{ "rows": [] } This is not an error.
Errors
Invalid queries return 400:
{ "error": "Unknown field: context.user" } Common query errors:
- dataset not found
- dataset schema not found
- unknown field
- invalid operator
- invalid alias
- aggregate incompatible with field type
- value type does not match field type
- selecting a field that is not in
groupBy - trying to filter or group by
context