Querying against the Hubuum API¶
Hubuum list endpoints share a common query interface for filtering, sorting, and cursor pagination. These query options are applied in the database, not by loading a full result set into memory first.
The response body for list endpoints remains a plain JSON array. Pagination metadata is returned in response headers.
For endpoint-specific field support, see query_support_matrix.md. The mutation-side contract is documented in Resource revisions and conditional mutations.
Try it with Atlas¶
Load the Atlas dataset to use the same objects as the other guides. This query returns web-01 followed by web-02:
GET /api/v1/classes/by-name/Server/objects?name__startswith=web-&sort=name
Authorization: Bearer <token>
Use limit=1 to page through them and pass the response's X-Next-Cursor as
cursor on the next request while preserving the filter and sort. The corpus
checks verify the filtered result and pagination against the server.
Query syntax¶
Query parameters are passed as standard query string parameters:
field=valuemeansfield__equals=valuefield__operator=valueapplies an explicit operator- filters are combined with
AND - repeated
sortfields are expressed as a comma-separated list - one common resource query may contain at most 128 parameters and 64 filters
Example:
Supported operators¶
String fields¶
equalsiequalscontainsicontainsstartswithistartswithendswithiendswithlikeregexinis_null
Numeric and date fields¶
equalsgtgteltltebetweeninis_null
Integer lists accept comma-separated values and inclusive ranges, including
negative ranges such as -6--2. A single filter may expand to at most 1,024
unique integers; larger ranges are rejected before they are materialized. Some
operators and endpoints enforce smaller limits. Prefer between for one large
continuous interval.
Revisioned resource and temporal-history lists expose the positive BIGINT
field revision. It supports exact, in, comparison, and between filters,
plus ascending or descending sorting:
revision=17
revision__in=15,16,17
revision__gte=10&revision__lt=20
revision__between=10,20
sort=-revision
Revision parsing is independent of 32-bit resource IDs. Zero, negative,
overflowing, malformed, and oversized list values return 400 Bad Request.
Audit events additionally support before_revision and after_revision
filters, but do not support a global revision sort because event revisions
belong to different resources.
Array fields¶
equalscontainsis_null
Boolean fields¶
equalsis_null
IP/network JSON fields¶
within_networkcontains_networkcontains_ipoverlaps_networkinet_equals
Negation¶
You can negate an operator by prefixing it with not_.
Examples:
name__not_equals=alicename__not_icontains=testcreated_at__not_between=2026-01-01T00:00:00Z,2026-02-01T00:00:00Z
Sorting¶
Use sort to request ordering. order_by is accepted as an alias.
Supported forms:
sort=idsort=id.ascsort=id.descsort=-idsort=collection_id.asc,name.desc
Notes:
- Sort support is endpoint-specific.
- A query may request at most eight distinct sort fields. Repeating the same field, including with a different direction, is rejected.
- Cursor pagination requires a stable sort, so Hubuum appends a deterministic tie-breaker automatically.
- If you omit
sort, each endpoint uses its own default stable sort. - Some relation endpoints support sorting on contextual fields like
from_*,to_*,depth, andpath. - Class object lists can sort enabled computed fields with
computed.shared.<key>or the owning user'scomputed.personal.<key>.computed.public.<key>andcomputed.private.<key>are aliases. See Computed fields for visibility, null, and pagination semantics. Requests that use computed sorting support at most two explicit sort fields.
Computed filtering¶
Class object lists and class object-aggregate queries can filter enabled computed fields with the same scope and alias names used for computed sorting:
GET /api/v1/classes/12/?computed.shared.display_name__icontains=edge
GET /api/v1/classes/12/?computed.personal.my_priority__between=10,20
GET /api/v1/classes/12/object-aggregates?computed.shared.lifecycle__equals=active&group_by=description
Computed filtering is intentionally endpoint-specific. Other endpoints reject
computed filter parameters instead of silently ignoring them. The
definition's declared result type determines which operators and values are
valid. At most two computed filter parameters may appear in one request.
String, numeric, boolean, object, and array definitions are supported;
see Computed fields for the full
operator table, visibility rules, null behavior, and JSON value syntax.
Computed keys may contain __; only a recognized operator at the final
__<operator> suffix is parsed as filter syntax.
Filtering class objects by related targets¶
The two class object-list routes support bounded, bidirectional relationship filters:
Every related target is a named group. The group alias correlates its class, object, and depth parameters:
related.<alias>.class.name=Room
related.<alias>.object.name__iequals=foo
related.<alias>.depth__lte=5
Use exactly one unnegated equality class selector in each group:
class.name=<name> or class.id=<id>. The alias must start with an ASCII
letter or _, may then contain ASCII letters, digits, and _, and may be at
most 64 characters. A request may contain at most four groups.
Object predicates in one group must match the same related target object. The supported target fields are:
idnamedescriptioncollection_idcreated_atupdated_atrevisionjson_data
Each field accepts its normal type-appropriate operators. json_data uses the
same path=value grammar and JSON operators documented below. Related fields
cannot be used for sorting.
Groups are independent existential predicates and are combined with AND.
For example, this returns hosts related to a room named foo, a person named
bar, and a person named zoot:
GET /api/v1/classes/by-name/Host/objects?related.room.class.name=Room&related.room.object.name=foo&related.bar.class.name=Person&related.bar.object.name=bar&related.zoot.class.name=Person&related.zoot.object.name=zoot
The two person aliases may resolve to distinct targets. The same target may
also satisfy more than one compatible group. Ordinary filters and computed
filters apply to the returned host objects and are combined with the related
groups using AND.
depth__lte is optional and is the only supported depth operator. It defaults
to 1, has a hard maximum of 10, and is evaluated independently for each
group. Paths may traverse objects of any class in either relation direction.
The target itself is not considered related to itself.
Negated target-field operators retain existential semantics. For example,
related.person.object.name__not_equals=bar means that a visible related
person exists whose name is not bar; it does not mean that no related person
is named bar. Boolean combinations such as OR, group-level NOT, and
distinct-target requirements are reserved for a structured search API.
Authorization applies to the whole path. The target class and target object, every traversed object, and every traversed object relation must be visible to the caller. A hidden path is ignored, while a fully visible alternative path may still satisfy the group. A valid query whose target does not exist or is not visible returns an empty result instead of disclosing the target.
Related filtering adds a bounded graph walk to both the page query and, unless
include_total=false, the exact count query. Use the smallest useful depth and
set include_total=false on latency-sensitive requests that do not need
X-Total-Count.
External policy authorization traverses the graph level by level so hidden
paths are rejected before they can expand the next frontier. Each group has a
safety budget of 1,000 candidate targets, 10,000 examined objects, and 20,000
examined relations. A query that exceeds a budget returns 400 Bad Request;
narrow the target predicates or reduce the requested depth.
Cursor pagination¶
List endpoints use cursor pagination.
Parameters:
limit: maximum number of items to returnsort: page ordercursor: opaque token returned by a previous responseinclude_total: whether to run the exact count query and returnX-Total-Count; defaults totrue
Limits:
- default page size:
100 - maximum page size:
250 - maximum encoded cursor size:
64 KiB - maximum JSON cursor nesting depth:
64 - a positive
limitabove the configured maximum is clamped to the maximum limit=0remains a400 Bad Request
Behavior:
- the current page is returned as a JSON array
- by default, paginated responses include
X-Total-Countwith the exact number of matching results - set
include_total=falseto skip that count query on latency-sensitive requests;X-Total-Countis then omitted - if another page exists, the response includes
X-Next-Cursor X-Page-Limitreports the effective page size after applying the default and maximum- send that cursor back unchanged to fetch the next page
- if
X-Next-Cursoris absent, there is no next page - total pages can be derived client-side as
ceil(X-Total-Count / X-Page-Limit) - encoded cursors are limited to 64 KiB; a page whose sort values would exceed
that limit returns
400 Bad Request, so clients must select fewer sort fields or smaller sortable values - malformed cursors, including JSON sort values PostgreSQL cannot represent or
values deeper than 64 nested array or object levels, return
400 Bad Request
Clients should read the effective default and maximum limits from the public client configuration endpoint rather than assuming the built-in values shown above:
The served /api-doc/openapi.json also applies the effective values to the
default and maximum schema constraints for limit and unified search's
limit_per_kind. The committed docs/openapi.json is a build-time snapshot and
normally reflects the built-in defaults.
Example:
Example response header:
X-Total-Count: 6
X-Next-Cursor: eyJzb3J0cyI6W3siZmllbGQiOiJpZCIsImRlc2NlbmRpbmciOmZhbHNlfV0sInZhbHVlcyI6W3sidHlwZSI6ImludGVnZXIiLCJ2YWx1ZSI6Mn1dfQ
Next page:
GET /api/v1/classes?collections=12&limit=2&sort=id.asc&cursor=eyJzb3J0cyI6W3siZmllbGQiOiJpZCIsImRlc2NlbmRpbmciOmZhbHNlfV0sInZhbHVlcyI6W3sidHlwZSI6ImludGVnZXIiLCJ2YWx1ZSI6Mn1dfQ
JSON filtering¶
JSON filters are only available on endpoints that expose JSON-backed fields such as json_schema, json_data, from_json_data, or to_json_data.
Class json_schema example¶
If a class schema contains a numeric property definition such as:
you can filter classes whose schema defines a latitude minimum below zero:
Object json_data example¶
If objects store payloads such as:
you can filter objects in a class by JSON field value:
You can also use string-oriented operators for textual JSON values:
Nested JSON paths use comma-separated keys. Every path segment must be
non-empty and contain only ASCII letters, digits, _, or $:
JSON-backed numeric, boolean, and date/datetime values build on the same filter interface used elsewhere:
- numeric/date operators:
equals,gt,gte,lt,lte,between - boolean operators:
equals
Examples:
/api/v1/classes/12/?json_data__equals=metrics,cpu_count=8
/api/v1/classes/12/?json_data__gte=metrics,cpu_count=4
/api/v1/classes/12/?json_data__equals=flags,enabled=true
/api/v1/classes/12/?json_data__gt=maintenance,window_start=2026-03-01
/api/v1/classes/12/?json_data__between=maintenance,window_start=2026-03-01,2026-03-31
Date-oriented JSON filters accept the same date formats as other date filters:
- RFC3339 timestamps such as
2026-03-01T12:30:00Z - calendar dates such as
2026-03-01
between uses the same comma-separated min,max format as the rest of the query interface.
JSON-backed IP address and CIDR values also support network-aware operators:
within_networkMatches when the stored IP/network is inside the filter network, including equality. Example: stored10.0.0.10, filter10.0.0.0/24-> match. Example: stored10.0.0.0/25, filter10.0.0.0/24-> match.contains_networkMatches when the stored network fully contains the filter IP/network, including equality. Example: stored10.0.0.0/24, filter10.0.0.0/25-> match. Example: stored10.0.0.0/24, filter10.0.1.0/24-> no match.contains_ipMatches when the stored network strictly contains the filter host IP. Example: stored10.0.0.0/24, filter10.0.0.10-> match. Example: stored10.0.0.10, filter10.0.0.10-> no match, because a host does not strictly contain itself.overlaps_networkMatches when the stored IP/network overlaps the filter network at all. Example: stored10.0.0.0/24, filter10.0.0.64/26-> match. Example: stored10.0.1.0/24, filter10.0.0.0/24-> no match.inet_equalsMatches normalized network equality using PostgreSQLinetsemantics rather than raw string equality. Example: stored10.0.0.10, filter10.0.0.10/32-> match. Example: stored10.0.0.0/24, filter10.0.0.0/25-> no match.
Examples:
/api/v1/classes/12/?json_data__within_network=network,address=10.0.0.0/24
/api/v1/classes/12/?json_data__contains_network=network,address=10.0.0.0/25
/api/v1/classes/12/?json_data__contains_ip=network,address=10.0.0.10
/api/v1/classes/12/?json_data__overlaps_network=network,address=10.0.0.64/26
/api/v1/classes/12/?json_data__inet_equals=network,address=10.0.0.10
JSON array and structure operators¶
inallarray_lengthhas_keyis_null
JSON fields support operators for arrays, key existence, and null checking.
in is aliased as any; both names parse to the same operator.
in(alias:any): Matches when the stored JSON scalar value is one of the given values, or when a stored JSON array contains any of the given values. Values are comma-separated. Example:json_data__in=status=active,standbymatches ifstatusis"active"or"standby". Example:json_data__in=tags=web,apimatches iftagsis["web", "frontend"]because"web"is present.all: Matches when the stored JSON array contains all of the given values. Values are comma-separated. Example:json_data__all=tags=web,apimatches only iftagscontains both"web"and"api".array_length: Matches when the stored JSON array has exactly the given number of elements. Example:json_data__array_length=tags=3matches iftagshas exactly 3 elements.has_key: Matches when the stored JSON object contains the given key, regardless of the key's value (including JSONnull). Example:json_data__has_key=config=hostnamematches ifconfigis an object with ahostnamekey.is_null: Matches when the stored JSON path is null or does not exist. Unlike other operators,is_nulldoes not use akey=valueformat; the entire right-hand side is the JSON path. Example:json_data__is_null=optional_fieldmatches ifoptional_fieldis missing or JSONnull.
Examples:
/api/v1/classes/12/?json_data__in=status=active,standby,maintenance
/api/v1/classes/12/?json_data__all=tags=web,api
/api/v1/classes/12/?json_data__array_length=tags=2
/api/v1/classes/12/?json_data__has_key=config=hostname
/api/v1/classes/12/?json_data__is_null=decommissioned_at
/api/v1/classes/12/?json_data__not_is_null=hostname
If the JSON path does not exist, or the stored value cannot be interpreted as the requested JSON type, the filter does not match, but it does not fail the request.
Aggregated object queries¶
Object aggregation is a separate read-only collection resource, so the normal object-list response remains unchanged:
GET /api/v1/classes/{class_id}/object-aggregates
GET /api/v1/classes/by-name/{class_name}/object-aggregates
The explicit by-name alias applies the same query behavior while treating
numeric-looking class names as names.
Supply group_by once for each ordered dimension. Up to three dimensions are
supported:
namedescriptioncollection_idcreated_atupdated_atjson_data.<path>, using the existing comma-separated nested path grammarcomputed.shared.<key>computed.personal.<key>
Supply aggregate once for each numeric measure, using
operation:field. Up to four measures are evaluated in request order. The
operations are sum, average, min, and max; fields can be
json_data.<path>, computed.shared.<key>, or
computed.personal.<key>. Computed measures require a definition whose result
type is number or integer.
At least one group_by or aggregate parameter is required. Omitting
group_by produces one global aggregate row when at least one readable,
filtered source object exists. For example:
GET /api/v1/classes/12/object-aggregates?aggregate=sum:json_data.cost&aggregate=average:computed.shared.health_score
Each measure includes value_count, the number of finite supported numeric
values that contributed, and skipped_count, the remaining objects in the
group. Missing paths, JSON nulls, non-numeric JSON values, unavailable computed
results, and numbers outside the supported computed-decimal range are skipped.
A measure with no contributing values has state=empty and omits value;
otherwise it has state=value. This makes partial data visible instead of
silently presenting a number as if it covered the whole group.
The endpoint accepts the normal object filters, including scalar, json_data,
permissions, and enabled shared or owned personal computed filters. Computed
source filters use the same typed operators, public/private aliases, and
two-filter limit as class object lists. For example, this request first applies
JSON and computed filters, groups the remaining readable objects by country
and shared lifecycle, and calculates a cost total per group:
GET /api/v1/classes/12/object-aggregates?json_data__equals=status=active&computed.shared.environment__equals=production&group_by=json_data.location,country&group_by=computed.shared.lifecycle&aggregate=sum:json_data.cost&sort=object_count.desc&limit=50
Grouping by created_at or updated_at uses the exact timestamp. The endpoint
does not accept date bucketing, arbitrary expressions, or object-list sort
fields. Aggregate ordering is selected with one of:
sort=dimensions.asc, the default;sort=dimensions.desc;sort=object_count.asc;sort=object_count.desc.
Count ordering always appends the complete dimension tuple in ascending order
as a deterministic tie-breaker. Measures do not change row ordering. Cursor
tokens are bound to the selected dimensions, measures, and sort; changing any
of them while following a cursor returns
400 Bad Request. The cursor limit is calculated for each request from a
common 8 KiB HTTP line budget after reserving its route, non-cursor query
parameters, request-line and response-header framing, separators, and line
terminators. If a JSON or computed value at a page boundary would make the
response header or replay request line exceed that limit, the request returns
413 Payload Too Large;
shorten the filters, narrow the grouping dimensions, or choose a page limit
that does not end on that value.
When computed aggregation or an external permission backend requires source object
snapshots, Hubuum streams them into byte-bounded batches. An individual snapshot
larger than 8 MiB returns 413 Payload Too Large. External authorization does
not retain a database connection during its calls, and its compacted
intermediate aggregate rows are also limited to 8 MiB. Narrow the source
filters, grouping dimensions, or measures when either bound is exceeded.
Each response row is self-describing:
[
{
"dimensions": [
{
"field": "json_data.location,country",
"state": "value",
"value": "NO"
},
{
"field": "computed.shared.lifecycle",
"state": "value",
"value": "production"
}
],
"measures": [
{
"field": "json_data.cost",
"operation": "sum",
"state": "value",
"value_count": 35,
"skipped_count": 2,
"value": 917.5
}
],
"object_count": 37
}
]
Dimension states have explicit meanings:
value: the dimension has a value;valuepreserves its JSON type;null: the JSON path or computed field produced JSONnull;missing: ajson_datapath does not exist;unavailable: a computed field could not produce a value.
JSON objects and arrays retain their structure and group by PostgreSQL JSONB
equality. The value member is omitted for null, missing, and
unavailable states.
Authorization and all supported supplied object filters are applied before aggregation. This rule also applies when the permission backend cannot push visibility into SQL: candidate object snapshots are authorized in bounded batches, and only the immutable authorized snapshots are grouped. Hidden objects therefore cannot affect bucket counts or aggregate cardinality, and rows are not reloaded after authorization.
Computed aggregation snapshots current definitions and applies computed source
filters, dimensions, and measures to source-object snapshots. A filter,
dimension, and measure in the same request share that definition snapshot.
Only the definitions needed by dimensions and measures and the enabled scope
needed by computed-filter evaluation are loaded. With a non-pushdown permission
backend, ordinary-filter candidates are authorized first; if none is visible,
the endpoint returns an empty page without resolving computed fields.
Otherwise, unknown, disabled, inaccessible, wrong-class, and non-numeric
measure selectors return 400 Bad Request. SQL-pushdown computed filters are
resolved before their database query, matching class object-list behavior even
when the filtered result is empty.
Personal filters, dimensions, and measures require a human owner with
ReadClass access and can only use that owner's enabled definitions.
Aggregation does not reload source objects or perform computed read repair.
Service accounts cannot filter, group by, or measure personal fields. Responses
depending on computed state include Cache-Control: private, no-store.
Pagination headers describe aggregate rows, not source objects:
X-Total-Countis the total number of aggregate rows and is omitted wheninclude_total=false;X-Next-Cursoris present when another aggregate page exists;X-Page-Limitis the effective aggregate page size.
Contextual endpoints¶
Some list endpoints derive part of the query from the path.
Examples:
/api/v1/classes/{class_id}/related/classesalways constrains the result to classes connected to the class in the path/api/v1/classes/{class_id}/related/relationsalways constrains the result to direct relations touching the class in the path/api/v1/classes/{class_id}/always constrains the result to objects in that class/api/v1/classes/{class_id}/object-aggregatesalways constrains source objects to that class and returns aggregate rows/api/v1/classes/{class_id}/objects/{object_id}/related/objectsalways constrains the result to objects connected to the object in the path/api/v1/classes/{class_id}/objects/{object_id}/related/relationsalways constrains the result to direct relations touching the object in the path
Some contextual endpoints also accept endpoint-specific query options in addition to the shared filter grammar:
/api/v1/classes/{class_id}/objects/{object_id}/related/objectssupportsignore_classeswith a comma-separated class ID list/api/v1/classes/{class_id}/objects/{object_id}/related/objectssupportsignore_self_class=true|falseand defaults it totrue
Permission checks are also applied before returning results, so the effective result set is always the intersection of:
- the path context
- the authenticated caller's permissions
- the query filters you supplied
Endpoint coverage¶
The shared query interface is currently used by:
- user lists, user tokens, and user groups
- group lists and group members
- collection lists and collection permission listings
- class lists, class permissions, connected-class listings, direct class-relation listings, objects in class, and aggregated objects in class
- global class relation and object relation lists
- connected-object listings
- direct related-relation listings
For the exact filter and sort fields per endpoint, see query_support_matrix.md.