Ask for exactly the rows you need in one call. A Makini Query Language (MQL) statement picks fields across related records, computes values, filters, groups and aggregates, and orders the result, against the data synced for the connection.POST /v4/query runs one read-only statement and returns the rows its select list shapes: {data: [...], meta: {limit, offset}}, one object per row (or one per group). It needs the same connection-scoped token and connection:data:read scope as the list endpoints.Write a statement#
select [distinct] <values | *> from <entity>
[join <relation> [using default], …]
[where <predicates>]
[group by <value>, …] [having <predicates>]
[order by <value> [asc|desc] [nulls first|last], …]
[limit <n>] [offset <n>]
<entity> is an endpoint name: the path segment of any entity list route, such as assets, business-units or work-orders.Choose fields#
an envelope field (id, code, _idValue, _displayValue, _source, _createdAt, _updatedAt, _deletedAt, _implements)
_mappings, an object keyed by your own mapping keys
a dotted relation path such as organization.name
Dotted paths nest in the row: organization.name becomes {organization: {name}}. select * returns the entity's full record, and <relation>.* in a select list returns every field of the related record.Rename and quote#
as renames a select item: asset.name as assetName puts the value under one top-level key instead of nesting it. Identifiers that collide with a keyword go in double quotes: select "order" from ….Compute values#
Anywhere a field can stand, a computed value can too: in the select list, in where, group by, having and order by.Arithmetic: +, -, *, / on numbers. / always divides exactly: 5 / 2 is 2.5. Put spaces around -, since a-b reads as one name.
Dates: add or subtract an interval, _updatedAt > now() - interval '30 days'; subtract two dates to compare a duration, completedAt - startedAt > interval '2 days'.
Field against field: where completedAt > dueAt.
A computed select item takes a name with as: select quantity * unitPrice as total. An unnamed function call is keyed by the function's name (count, lower).Functions#
| Kind | Functions |
|---|
| Aggregate | count(*), count(x), count(distinct x), sum(x), avg(x), min(x), max(x) |
| Text | lower(x), upper(x), trim(x), length(x), concat(x, y, …), coalesce(x, y, …) |
| Number | abs(x), round(x[, digits]), floor(x), ceil(x) |
| Date | now(), current_date, date(x[, tz]), date_trunc(unit, x[, tz]), date_part(unit, x[, tz]), date_diff(unit, from, to) |
date_trunc takes second, minute, hour, day, week, month, quarter or year; date_part also takes dow, isodow, doy and epoch; date_diff takes second to week and returns a number, fractions included.
Time zones: the optional last argument is an IANA name such as 'Europe/Berlin'. Without one, dates are UTC. With one, date_trunc('day', _createdAt, 'Europe/Berlin') returns the instant Berlin's day began, as a UTC timestamp.
customFields values take the type the function needs: sum(customFields.cost) reads the values as numbers, a date function reads them as dates.
Group and aggregate#
group by returns one row per distinct key, with aggregates computed over each group. having filters the groups. Every select item is either a group key or an aggregate; group by and order by also accept a select item's name.An aggregate without group by answers one row: select count(*), max(_updatedAt) from assets. select distinct make, condition._displayValue from assets returns each combination once. Group by a relation's id or _displayValue, or a field reached through it (organization.name).Follow a relation with join#
join <relation> is the equivalent of expand. It takes a relation field name (the names expand accepts), never an ON clause, and the joined relation arrives as the full related record, one level deep. select * from business-units join organization answers exactly what GET /v4/business-units?expand=organization does.join also takes a dotted path such as parent.organization, but only with an explicit select list: select * with a dotted join is a 400.Relations that cannot be joined#
A many-relation is an array of links. Select the whole field to get the links; its members cannot be addressed one by one.
A polymorphic relation points at a different entity per row. Select its link members instead: target.id, target.rel, target._idValue or target._displayValue.
Filter with where#
where takes these predicates:Compare: =, != (or <>), <, <=, >, >=, between any two values
Match a pattern: [not] like, [not] ilike
Match a list or range: [not] in (…), [not] between … and …
Test for null: is [not] null
Match an array member: '<value>' = any(_mappings.<key>) (or any(_implements))
Combine them with and, or, not and parentheses.String literals are single-quoted. Numbers and true/false are bare. A value must match the field's type: priorityLevel is a number from 1 (highest) to 5 (lowest), so priorityLevel = 'high' is a 400.Order and page results#
order by sorts on one or more values, and limit and offset page through the result. Soft-deleted records are excluded.Results are always in a stable order: the entity id is the final tiebreaker, or the group keys in a grouped query.
Reference#
Next steps#
Filtering
Query-string filters on every list endpoint.
Relations and expand
The relations join follows, on the list endpoints.
Raw data and passthrough
Reach the raw synced tables and the connected system's own API.
Errors
The error body every endpoint returns.