Protocol query API
The protocol query API allows you to filter and sort protocol instances by their field values.
Endpoint
POST /npl/{package}/{ProtocolName}/-/query
For example, for a protocol IOU in package com.example.iou:
POST /npl/com/example/iou/IOU/-/query
The request body is a JSON object. All fields are optional -- an empty body {} returns all instances the caller has
access to, subject to the default page size.
Request body
{
"filter": { ... },
"sort": [ ... ],
"page": 1,
"pageSize": 20,
"includeCount": false
}
| Field | Type | Default | Description |
|---|---|---|---|
filter |
object | — | Field filters; see Filters. |
sort |
array | — | Sort order; see Sorting. |
page |
integer | 1 |
1-indexed page number. |
pageSize |
integer | engine default | Number of items per page. |
includeCount |
boolean | false |
When true, adds totalPages and totalItems to the response. |
Making fields queryable
Fields must be explicitly marked with @query before they
can be used in filters or sort expressions; using a non-annotated field will result in an error.
@api
protocol[seller, buyer] Order(
@query var amount: Number,
@query var label: Text,
) {
@query var status: Status = Status(0);
}
System fields
The following fields are always available without @query:
| Field | Type | Description |
|---|---|---|
@state |
Text | Current state name of the protocol |
createdAt |
DateTime | When the protocol was created |
modifiedAt |
DateTime | When the protocol was last modified |
Filters
The filter object maps field names to operator objects:
{
"filter": {
"amount": { "gt": 100 },
"label": { "eq": "urgent" }
}
}
Multiple fields are combined with AND. All operators for a single field are also combined with AND.
Operators by type
Number, Symbol
| Operator | Description | Value type |
|---|---|---|
eq |
Equals | number |
gt |
Greater than | number |
gte |
Greater than or equal | number |
lt |
Less than | number |
lte |
Less than or equal | number |
in |
Matches any value in the array | array of numbers |
not |
Negates a nested operator | operator object |
{ "filter": { "amount": { "gte": 10, "lt": 100 } } }
{ "filter": { "amount": { "in": [10, 20, 30] } } }
{ "filter": { "amount": { "not": { "eq": 0 } } } }
Text, Enum, Identifier
| Operator | Description | Value type |
|---|---|---|
eq |
Equals | string |
in |
Matches any value in the array | array of strings |
not |
Negates a nested operator | operator object |
{ "filter": { "label": { "eq": "urgent" } } }
{ "filter": { "label": { "in": ["urgent", "normal"] } } }
{ "filter": { "priority": { "not": { "eq": "LOW" } } } }
DateTime, LocalDate
Values must be ISO-8601 strings and optionally include a timezone. DateTime requires a timezone offset, e.g.
2024-03-15T10:00:00Z or 2006-01-02T15:04:05.999+01:00[Europe/Zurich]; LocalDate requires a date-only string (e.g.
2024-03-15).
| Operator | Description | Value type |
|---|---|---|
eq |
Equals | ISO-8601 string |
gt |
After | ISO-8601 string |
gte |
At or after | ISO-8601 string |
lt |
Before | ISO-8601 string |
lte |
At or before | ISO-8601 string |
in |
Matches any value in the array | array of strings |
not |
Negates a nested operator | operator object |
{ "filter": { "dueDate": { "lt": "2024-12-31" } } }
{ "filter": { "eventTime": { "gte": "2006-01-02T15:04:05.999+01:00[Europe/Zurich]" } } }
{ "filter": { "createdAt": { "not": { "lt": "2006-01-02T15:04:05.999+01:00[Europe/Zurich]" } } } }
Boolean
| Operator | Description | Value type |
|---|---|---|
eq |
Equals | boolean |
not |
Negates a nested operator | operator object |
{ "filter": { "active": { "eq": true } } }
{ "filter": { "active": { "not": { "eq": false } } } }
Struct
When a @query-annotated field has a struct type, filter by its members using member names as keys. Each member you
filter on must itself be annotated with @query on the struct definition.
struct Details {
@query score: Number,
@query label: Text
};
{ "filter": { "details": { "score": { "gte": 5 }, "label": { "eq": "gold" } } } }
List, Set
The contains operator checks whether the collection contains a matching element.
Scalar elements — wrap the value in a JSON array:
{ "filter": { "tags": { "contains": ["kotlin"] } } }
{ "filter": { "tags": { "not": { "contains": ["archived"] } } } }
Struct elements — pass an object with struct member filters; matches if any element satisfies all conditions:
struct Inner { @query value: Number, @query tag: Text };
{ "filter": { "items": { "contains": { "value": { "gt": 10 }, "tag": { "eq": "a" } } } } }
| Operator | Description |
|---|---|
contains |
Collection contains a matching element |
not |
Negates a nested operator |
Map
| Operator | Description | Value |
|---|---|---|
exists |
Map has the given key | scalar key value |
contains |
Map has a matching entry; accepts key, value, or both |
object with key and/or value fields |
not |
Negates a nested operator | operator object |
{ "filter": { "metadata": { "exists": "region" } } }
{ "filter": { "metadata": { "contains": { "key": "env", "value": "prod" } } } }
{ "filter": { "metadata": { "not": { "exists": "deprecated" } } } }
Union
Use type to filter by the variant type name and value to filter by the variant's content.
Type names for type filters:
- Primitive NPL types use the short name:
"Text","Number","Boolean","LocalDate","DateTime" - Struct variants use the fully-qualified package path:
"/com/example/MyStruct" - The absent
Optionalvariant uses/lang/core/None
| Operator | Description |
|---|---|
type |
Filter by variant type; uses Text operators (eq, in, not) |
value |
Filter by variant content; uses scalar operators or struct field names |
not |
Negates a nested operator |
{ "filter": { "status": { "type": { "eq": "Number" } } } }
{ "filter": { "status": { "type": { "in": ["Text", "Number"] } } } }
{ "filter": { "status": { "type": { "not": { "eq": "/com/example/StatusInfo" } } } } }
{ "filter": { "status": { "value": { "gt": 42 } } } }
{ "filter": { "status": { "value": { "code": { "gte": 100 } } } } }
{ "filter": { "optionalAmount": { "type": { "eq": "/lang/core/None" } } } }
{ "filter": { "optionalAmount": { "type": { "not": { "eq": "/lang/core/None" } } } } }
type and value can be combined in one filter — both conditions must match:
{ "filter": { "status": { "type": { "eq": "Number" }, "value": { "lt": 100 } } } }
The not operator
not wraps any single operator object and negates it. It cannot wrap another not directly (doing so returns
400 Bad Request).
{ "filter": { "amount": { "not": { "in": [0, -1] } } } }
{ "filter": { "label": { "not": { "eq": "archived" } } } }
Sorting
The sort field accepts an array of sort entries, applied left to right:
{
"sort": [
{ "field": "amount", "dir": "ASC" },
{ "field": "label", "dir": "DESC" }
]
}
| Field | Values | Description |
|---|---|---|
field |
field name | Must be @query-annotated or a system field |
dir |
ASC, DESC |
Sort direction |
Sortable types: Number, Text, DateTime, LocalDate, Boolean, Enum, Identifier, Union. Collection types
(List, Set, Map) and Struct fields cannot be sorted.
System fields @state, createdAt, and modifiedAt are always sortable.
!!! note "Union sort order" Union fields are sorted by their raw JSON text representation. This means sort order across mixed variant types (e.g. numbers and strings in the same union) follows JSON string collation and may not reflect the semantic ordering of the values.
Response
The response follows the same structure as the standard list endpoint:
{
"items": [ ... ],
"page": 1,
"totalPages": 5,
"totalItems": 42
}
totalPages and totalItems are only present when includeCount: true was set in the request.
Errors
All validation errors return 400 Bad Request with a description of the problem.
| Cause | Example |
|---|---|
| Filtering on a field that does not exist | {"filter": {"nosuchfield": {"eq": 1}}} |
Filtering on a field not annotated with @query |
{"filter": {"privateField": {"eq": 1}}} |
| Operator not allowed for the field's type | {"filter": {"amount": {"contains": "x"}}} (Number) |
| Filter value is not an operator object | {"filter": {"amount": 42}} |
in value is not an array |
{"filter": {"amount": {"in": 10}}} |
not wrapping another not |
{"filter": {"amount": {"not": {"not": {"eq": 0}}}}} |
sort is not an array |
{"sort": "amount"} |
Sort entry missing field or dir |
{"sort": [{"field": "amount"}]} |
dir is not ASC or DESC |
{"sort": [{"field": "amount", "dir": "SIDEWAYS"}]} |
Sorting on a non-existent or non-@query field |
{"sort": [{"field": "nosuchfield", "dir": "ASC"}]} |
| Sorting on an unsortable type (List, Set, Map, Struct) | {"sort": [{"field": "tags", "dir": "ASC"}]} |
| URL query parameters used instead of request body | POST /-/query?page=1 |
page or pageSize is not a valid integer |
{"page": "first"} |
includeCount is not a boolean |
{"includeCount": "yes"} |
| Struct filter references a non-existent member | {"filter": {"details": {"nosuchfield": {"eq": 1}}}} |
Struct filter member is not annotated with @query |
{"filter": {"details": {"privateField": {"eq": 1}}}} |