Skip to content
Docs

Queries

Every table query in BifrostQL returns paged results. The table name is the query field, and results live inside a data array.

{
users(limit: 10, offset: 0) {
data {
userId
name
email
}
}
}

The limit and offset parameters control pagination. Without limit, the default page size from your configuration applies.

Filters use Directus-style syntax. Pass a filter argument with column names and operators:

{
orders(filter: { status: { _eq: "shipped" }, total: { _gt: 100 } }) {
data {
orderId
total
status
}
}
}
OperatorDescriptionExample
_eqEqual{ status: { _eq: "active" } }
_neqNot equal{ status: { _neq: "deleted" } }
_gtGreater than{ price: { _gt: 50 } }
_gteGreater than or equal{ price: { _gte: 50 } }
_ltLess than{ price: { _lt: 100 } }
_lteLess than or equal{ price: { _lte: 100 } }
_inIn list{ status: { _in: ["active", "pending"] } }
_ninNot in list{ status: { _nin: ["deleted"] } }
_containsContains substring{ name: { _contains: "smith" } }
_ncontainsDoes not contain{ name: { _ncontains: "test" } }
_starts_withStarts with{ name: { _starts_with: "A" } }
_ends_withEnds with{ email: { _ends_with: ".com" } }
_nullIs null{ deletedAt: { _null: true } }
_nnullIs not null{ email: { _nnull: true } }
_betweenBetween two values{ price: { _between: [10, 50] } }

Multiple fields at the same level are combined with AND:

{
products(filter: { price: { _gt: 10 }, category: { _eq: "electronics" } }) {
data { productId name price }
}
}

Use and and or for explicit boolean logic:

{
products(filter: {
or: [
{ price: { _lt: 10 } },
{ category: { _eq: "sale" } }
]
}) {
data { productId name price category }
}
}

Sort uses an enum list. Each column has _asc and _desc variants:

{
products(sort: [price_asc, name_desc]) {
data {
productId
name
price
}
}
}

Sort fields are applied in order. The first field is the primary sort, subsequent fields break ties.

Combine limit and offset for pagination:

# Page 1
{ users(limit: 20, offset: 0) { data { userId name } } }
# Page 2
{ users(limit: 20, offset: 20) { data { userId name } } }

The total field on the page type returns the count of all matching rows (before pagination):

{
users(limit: 20, offset: 0) {
total
data {
userId
name
}
}
}

Request only the fields you need. BifrostQL generates SQL that selects only the requested columns, so narrower queries are genuinely faster at the database level.

{
users {
data {
userId
email
}
}
}

This produces SELECT [userId], [email] FROM [users] — not SELECT *.

Roll up related rows inline with _agg. It computes an aggregate over a child collection per parent row, so you can return “order count” or “average score” alongside the parent without a second round-trip:

{
workshops {
data {
id
sessionCount: _agg(value: { sessions: id } operation: count)
avgScore: _agg(value: { sessions: { entry: score } } operation: avg)
}
}
}
  • value names the path to aggregate — { joinTable: column }, nested as deep as the relationship goes.
  • operation is one of count, sum, avg, min, max.
  • Alias each _agg (e.g. sessionCount:) when you request more than one.

_agg runs across all four engines — SQL Server, PostgreSQL, MySQL, and SQLite — through the dialect layer. Aggregating a column requires a nested foreign-key path; a bare-column aggregate raises a clear error.

For cross-tab / matrix output, see Pivot / Cross-Tab.