Queries
Vincent Depassier · August 29, 2026
PraxQL — Query Syntax
A query body reads data from one primary table, with optional filtering, sorting, pagination and nested relations.
Minimal query
{
"refs": {
"Customers": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"query": {
"from": "Customers"
}
}This returns every permitted column from Customers, up to the default limit.
from (required)
The primary table alias. It must exist as a key in refs.
"from": "Customers"select (optional)
An array of column names to return. Omit it to get all permitted columns.
"select": ["Name", "Email", "Phone"]Nested relation objects (joins) mix freely with plain column names:
"select": [
"Name",
"Email",
{ "table": "Orders", "select": ["Total", "Date"] }
]See PraxQL — Relations for the full nested syntax.
When two joined tables share a column name, prefix it with the alias:
"select": ["Customers.Name", "Orders.Name"]where (optional)
An array of filter conditions. Conditions at the same level are AND'd together by default.
"where": [
{ "field": "Status", "op": "eq", "value": "Active" },
{ "field": "Region", "op": "eq", "value": "EU" }
]That produces WHERE Status = 'Active' AND Region = 'EU'.
Each condition has three fields:
Field | Type | Description |
| string | Column name |
| string | Operator — see PraxQL — Operators |
| any | The comparison value: string, number, boolean, null or array |
For OR logic use the or key; for explicit AND grouping use and. Both are covered in PraxQL — Operators.
orderBy (optional)
An array of sort specifications, applied in order — the first item is the primary sort.
"orderBy": [
{ "field": "LastName", "dir": "asc" },
{ "field": "CreatedDate", "dir": "desc" }
]Field | Values |
| Column name |
|
|
The column needs CanSort = true in the credential's column scope. A credential with no column scopes defined can sort by anything.
limit and offset (optional)
"limit": 25,
"offset": 50That returns rows 51–75, the third page of 25.
| Value |
Default when | 50 |
Default cap on a newly created table scope | 200 ( |
Absolute maximum, which nothing raises | 1,000 |
The effective ceiling is the lower of the scope's cap and the absolute maximum — so out of the box that is 200, not 1,000, and somebody has to raise the scope before a bigger page is possible.
Asking for more than the ceiling does not fail: it silently gives you the ceiling. Always read meta.limit in the response rather than assuming you got what you asked for.
Pagination
Page 1:
{
"refs": { "Orders": "..." },
"query": {
"from": "Orders",
"orderBy": [{ "field": "CreatedDate", "dir": "desc" }],
"limit": 50,
"offset": 0
},
"includeTotalCount": true
}The response carries meta.total, which tells you how many pages exist.
Page 2 is the same body with "offset": 50. Only ask for includeTotalCount on the first page — every later page would pay for a second count that returns a number you already have.
Always send an orderBy when paginating. Without one, nothing guarantees row order between requests, so a row can appear on two pages or on none.
groupBy and having
For totals, averages and counts per group, use groupBy with aggregate functions in select, and optionally having to filter the groups. See PraxQL — Aggregations.
Complete example
{
"refs": {
"Customers": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"query": {
"from": "Customers",
"select": ["Name", "Email", "Status", "Region"],
"where": [
{ "field": "Status", "op": "eq", "value": "Active" },
{
"or": [
{ "field": "Region", "op": "eq", "value": "EU" },
{ "field": "Region", "op": "eq", "value": "LATAM" }
]
}
],
"orderBy": [{ "field": "Name", "dir": "asc" }],
"limit": 50,
"offset": 0
}
}Active customers in EU or LATAM, sorted by name, fifty at a time.