Skip to content

Query

Resource querying with filtering, sorting, pagination, and relationships

Extension URN: urn:vnd:ext:query


The query extension enables rich querying capabilities for collection and resource functions. It bundles filtering, sorting, pagination, sparse fieldsets, and relationship inclusion into a cohesive package.

When enabled, responses MUST use the Resource Object format (type/id/attributes structure) since query features depend on this structure.


Query SHOULD be used for:

  • CRUD-style resource APIs
  • Collection endpoints returning lists of entities
  • APIs requiring filtering, sorting, or pagination
  • Graph-like data with relationships between resources

Query SHOULD NOT be used for:

  • Simple RPC functions returning scalar values
  • Action-oriented endpoints (e.g., payments.process)
  • Functions with custom response structures

The query extension provides five capabilities:

CapabilityDescription
FilteringQuery resources by attribute conditions
SortingOrder results by attributes
PaginationNavigate large result sets
Sparse FieldsetsRequest only needed fields
RelationshipsInclude related resources

Filtering allows clients to request a subset of resources matching specific criteria. Filters are applied server-side before returning results.

A filter specifies a single condition:

{
"attribute": "status",
"operator": "equals",
"value": "pending"
}
FieldTypeRequiredDescription
attributestringYesAttribute to filter on
operatorstringYesComparison operator
valueanyConditionalValue to compare (type depends on operator)
booleanstringNoBoolean combinator (and, or)
OperatorSQL EquivalentValue TypeDescription
equals=anyExact match
not_equals!=anyNot equal
{ "attribute": "status", "operator": "equals", "value": "active" }
{ "attribute": "type", "operator": "not_equals", "value": "draft" }
OperatorSQL EquivalentValue TypeDescription
greater_than>number/stringGreater than
greater_than_or_equal_to>=number/stringGreater than or equal
less_than<number/stringLess than
less_than_or_equal_to<=number/stringLess than or equal
{ "attribute": "amount", "operator": "greater_than", "value": 100 }
{ "attribute": "created_at", "operator": "greater_than_or_equal_to", "value": "2024-01-01T00:00:00Z" }
OperatorSQL EquivalentValue TypeDescription
likeLIKEstringPattern match (use % wildcard)
not_likeNOT LIKEstringNegative pattern match
{ "attribute": "email", "operator": "like", "value": "%@example.com" }
{ "attribute": "name", "operator": "like", "value": "John%" }
{ "attribute": "code", "operator": "not_like", "value": "TEST%" }
OperatorSQL EquivalentValue TypeDescription
inINarrayValue in set
not_inNOT INarrayValue not in set
{ "attribute": "status", "operator": "in", "value": ["pending", "processing", "shipped"] }
{ "attribute": "country_code", "operator": "not_in", "value": ["US", "CA"] }
OperatorSQL EquivalentValue TypeDescription
betweenBETWEENarray[2]Value in range (inclusive)
not_betweenNOT BETWEENarray[2]Value outside range
{ "attribute": "price", "operator": "between", "value": [10, 100] }
{ "attribute": "created_at", "operator": "between", "value": ["2024-01-01", "2024-01-31"] }
OperatorSQL EquivalentValue TypeDescription
is_nullIS NULLAttribute is null
is_not_nullIS NOT NULLAttribute is not null
{ "attribute": "deleted_at", "operator": "is_null" }
{ "attribute": "verified_at", "operator": "is_not_null" }

Note: value is not required for null operators.

Filters are organized by resource using an object:

{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "active" },
{ "attribute": "created_at", "operator": "greater_than", "value": "2024-01-01T00:00:00Z" }
]
}
}

The self key targets the primary resource. Additional keys target relationships.

Without explicit boolean, filters are combined with AND:

{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "active" },
{ "attribute": "type", "operator": "equals", "value": "premium" }
]
}
}
// SQL: WHERE status = 'active' AND type = 'premium'

Explicit AND (same as default):

{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "active", "boolean": "and" },
{ "attribute": "verified", "operator": "equals", "value": true, "boolean": "and" }
]
}
}
// SQL: WHERE status = 'active' AND verified = true
{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "pending", "boolean": "or" },
{ "attribute": "status", "operator": "equals", "value": "processing", "boolean": "or" }
]
}
}
// SQL: WHERE status = 'pending' OR status = 'processing'

Note: For simple OR on same attribute, prefer in operator:

{ "attribute": "status", "operator": "in", "value": ["pending", "processing"] }

Filters are applied sequentially, with each filter’s boolean determining how it connects to the previous condition:

{
"filters": {
"self": [
{ "attribute": "type", "operator": "equals", "value": "order" },
{ "attribute": "status", "operator": "equals", "value": "pending", "boolean": "or" },
{ "attribute": "status", "operator": "equals", "value": "failed", "boolean": "or" }
]
}
}
// SQL: WHERE type = 'order' OR status = 'pending' OR status = 'failed'

Important: Boolean operators connect filters left-to-right without automatic grouping. For complex grouping logic, use the in operator:

{
"filters": {
"self": [
{ "attribute": "type", "operator": "equals", "value": "order" },
{ "attribute": "status", "operator": "in", "value": ["pending", "failed"] }
]
}
}
// SQL: WHERE type = 'order' AND status IN ('pending', 'failed')

Filters can target different resources in a query:

{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "active" }
],
"customer": [
{ "attribute": "country_code", "operator": "equals", "value": "FI" }
]
}
}
KeyDescription
selfFilters on the primary resource
<relationship>Filters on related resources

Filtering by relationship creates a WHERE EXISTS condition:

{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "pending" }
],
"customer": [
{ "attribute": "type", "operator": "equals", "value": "vip" }
]
}
}
// SQL: WHERE status = 'pending'
// AND EXISTS (SELECT 1 FROM customers WHERE customers.id = orders.customer_id AND type = 'vip')

Servers MUST define which attributes are filterable per resource:

{
"filters": {
"self": ["id", "status", "created_at", "total_amount"],
"customer": ["id", "type", "country_code"]
}
}
  • Filtering on non-allowed attributes MUST return an error
  • Servers SHOULD expose allowed filters via vend.describe

Most operators accept string values:

{ "attribute": "status", "operator": "equals", "value": "pending" }

Comparison operators work with numbers:

{ "attribute": "quantity", "operator": "greater_than", "value": 10 }
{ "attribute": "price", "operator": "between", "value": [10.00, 99.99] }
{ "attribute": "is_active", "operator": "equals", "value": true }
{ "attribute": "verified", "operator": "equals", "value": false }

Use ISO 8601 format:

{ "attribute": "created_at", "operator": "greater_than", "value": "2024-01-15T10:30:00Z" }
{ "attribute": "date", "operator": "between", "value": ["2024-01-01", "2024-12-31"] }

Use null operators, not equals with null:

// Correct
{ "attribute": "deleted_at", "operator": "is_null" }
// Incorrect
{ "attribute": "deleted_at", "operator": "equals", "value": null }

Sorting allows clients to specify the order of resources in collection responses. Multiple sort criteria are applied in sequence.

A sort specifies ordering for a single attribute:

{
"attribute": "created_at",
"direction": "desc"
}
FieldTypeRequiredDescription
attributestringYesAttribute to sort by
directionstringYesSort direction
ValueSQL EquivalentDescription
ascASCAscending (smallest first)
descDESCDescending (largest first)

Multiple sorts create compound ordering:

{
"sorts": [
{ "attribute": "status", "direction": "asc" },
{ "attribute": "created_at", "direction": "desc" }
]
}

Sorts are applied in array order:

{
"sorts": [
{ "attribute": "country_code", "direction": "asc" },
{ "attribute": "city", "direction": "asc" },
{ "attribute": "name", "direction": "asc" }
]
}
// SQL: ORDER BY country_code ASC, city ASC, name ASC

Primary sort is first, then secondary, etc.

Servers MUST define which attributes are sortable:

{
"sorts": {
"self": ["name", "created_at", "updated_at", "status", "total_amount"]
}
}
  • Sorting on non-allowed attributes MUST return an error
  • Servers SHOULD expose allowed sorts via vend.describe

When no sort is specified, servers SHOULD apply a default sort:

  • By primary key (id) ascending
  • By creation timestamp (created_at) descending
  • By a natural ordering attribute

Servers MUST document their default sort behavior.

For pagination stability, servers SHOULD include a unique attribute (like id) as the final sort criterion, even if not explicitly requested:

// Client requests
{ "sorts": [{ "attribute": "status", "direction": "asc" }] }
// Server applies (for stability)
// ORDER BY status ASC, id ASC
TypeOrder
Numbers1, 2, 3, 10, 100
StringsA, B, C, a, b, c (locale-dependent)
DatesOldest first
Booleansfalse, true
NullsFirst or last (implementation-defined)
TypeOrder
Numbers100, 10, 3, 2, 1
Stringsc, b, a, C, B, A (locale-dependent)
DatesNewest first
Booleanstrue, false
NullsFirst or last (implementation-defined)

Servers SHOULD document null ordering behavior:

BehaviorDescription
nulls_firstNulls sort before non-null values
nulls_lastNulls sort after non-null values

Functions returning lists SHOULD support pagination to handle large result sets efficiently. Vend defines three pagination styles that cover common use cases.

Pagination parameters are passed in the extension options:

{
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"pagination": {
"limit": 25,
"cursor": "eyJpZCI6MTAwfQ"
}
}
}
]
}

Simple pagination using limit and offset. Familiar from SQL.

Request:

{
"pagination": {
"limit": 25,
"offset": 0
}
}

Response:

{
"result": {
"data": [...],
"meta": {
"pagination": {
"limit": 25,
"offset": 0,
"total": 150,
"has_more": true
}
}
}
}
FieldTypeDescription
limitintegerMaximum items to return
offsetintegerNumber of items to skip
totalintegerTotal items available (optional)
has_morebooleanWhether more items exist
  • Simple to implement and understand
  • Allows “jump to page N”
  • Performance degrades with large offsets
  • Results can shift if data changes between requests

Use when: Small datasets, admin interfaces, or when random page access is needed.

Opaque cursor token that encodes position. Server generates cursor, client passes it back.

First Request:

{
"pagination": {
"limit": 25
}
}

First Response:

{
"result": {
"data": [...],
"meta": {
"pagination": {
"limit": 25,
"next_cursor": "eyJpZCI6MTAwLCJkaXIiOiJuZXh0In0",
"prev_cursor": null,
"has_more": true
}
}
}
}

Subsequent Request:

{
"pagination": {
"limit": 25,
"cursor": "eyJpZCI6MTAwLCJkaXIiOiJuZXh0In0"
}
}
FieldTypeDescription
limitintegerMaximum items to return
cursorstringOpaque cursor from previous response
next_cursorstringCursor for next page (null if none)
prev_cursorstringCursor for previous page (null if none)
has_morebooleanWhether more items exist in this direction

Cursors SHOULD be:

  • Opaque to clients (implementation detail)
  • URL-safe (base64url encoded)
  • Tamper-resistant (optionally signed)

Example cursor payload (before encoding):

{
"id": 100,
"created_at": "2024-01-15T10:30:00Z",
"dir": "next"
}
  • Efficient for any position (no offset scanning)
  • Stable during concurrent modifications
  • Cannot jump to arbitrary page
  • Cursor may expire or become invalid

Use when: Large datasets, infinite scroll, or when consistency matters.

Pagination using explicit field values. Ideal for feeds and timelines.

Request (ID-based):

{
"pagination": {
"limit": 25,
"after_id": "msg_abc123",
"before_id": null
}
}

Request (Time-based):

{
"pagination": {
"limit": 25,
"since": "2024-01-15T10:00:00Z",
"until": "2024-01-15T12:00:00Z"
}
}

Response:

{
"result": {
"data": [...],
"meta": {
"pagination": {
"limit": 25,
"newest_id": "msg_xyz789",
"oldest_id": "msg_abc124",
"has_newer": true,
"has_older": true
}
}
}
}
FieldTypeDescription
limitintegerMaximum items to return
after_idstringReturn items after this ID (exclusive)
before_idstringReturn items before this ID (exclusive)
sincestringReturn items after this timestamp (ISO 8601)
untilstringReturn items before this timestamp (ISO 8601)
FieldTypeDescription
newest_idstringID of newest item in response
oldest_idstringID of oldest item in response
has_newerbooleanWhether newer items exist
has_olderbooleanWhether older items exist
  • after_id + limit — Get next N items after ID
  • before_id + limit — Get previous N items before ID
  • since + until — Get items in time range
  • since + limit — Get N items since timestamp
  • Perfect for polling (“give me everything since last fetch”)
  • Efficient keyset queries
  • Requires monotonic IDs or indexed timestamps
  • More complex query logic

Use when: Activity feeds, timelines, event logs, or polling scenarios.

Functions MAY support one or more pagination styles. Servers SHOULD:

  1. Document which style(s) each function supports
  2. Return an error if unsupported pagination parameters are provided
  3. Apply sensible defaults when pagination is omitted

Servers SHOULD:

  • Define a default limit (e.g., 25)
  • Define a maximum limit (e.g., 100)
  • Return an error if requested limit exceeds maximum

When no items match:

{
"result": {
"data": [],
"meta": {
"pagination": {
"limit": 25,
"has_more": false
}
}
}
}

Cursor-based example:

cursor = null
do {
response = call("orders.list", {
extensions: [{
urn: "urn:vnd:ext:query",
options: { pagination: { limit: 25, cursor: cursor } }
}]
})
process(response.result.data)
cursor = response.result.meta.pagination.next_cursor
} while (response.result.meta.pagination.has_more)

Keyset-based example:

last_id = null
loop {
response = call("events.list", {
extensions: [{
urn: "urn:vnd:ext:query",
options: { pagination: { limit: 100, after_id: last_id } }
}]
})
if (response.result.data.length > 0) {
process(response.result.data)
last_id = response.result.meta.pagination.newest_id
}
sleep(interval)
}

Sparse fieldsets allow clients to request a subset of attributes for each resource type. This reduces payload size and improves performance by excluding unnecessary data.

The fields object specifies which attributes to include per resource type:

{
"fields": {
"self": ["id", "status", "total_amount", "created_at"],
"customer": ["id", "name"]
}
}
KeyDescription
selfFields for the primary resource
<relationship>Fields for related resources
  • Keys are resource type identifiers
  • Values are arrays of attribute names
  • id and type are always included (not specified in fieldset)
  • Empty array [] returns only type and id
  • Absent key returns all fields for that type
{
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"fields": {
"self": ["id", "status", "total_amount"],
"customer": ["id", "name", "email"]
},
"relationships": ["customer"]
}
}
]
}

Response includes only requested fields:

{
"result": {
"data": {
"type": "order",
"id": "12345",
"attributes": {
"status": "pending",
"total_amount": { "amount": "99.99", "currency": "USD" }
},
"relationships": {
"customer": {
"data": { "type": "customer", "id": "42" }
}
}
},
"included": [
{
"type": "customer",
"id": "42",
"attributes": {
"name": "Alice",
"email": "alice@example.com"
}
}
]
}
}

Note: type and id are always present even when not in the fieldset.

Servers MUST define which fields are queryable per resource:

{
"fields": {
"self": ["id", "order_number", "status", "total_amount", "created_at", "updated_at"],
"customer": ["id", "name", "email", "type"],
"items": ["id", "sku", "name", "quantity", "price"]
}
}
  • Requesting non-allowed fields MUST return an error
  • Servers SHOULD expose allowed fields via vend.describe

When fields is absent, servers return all allowed fields:

// Request without fields option
{
"extensions": [{
"urn": "urn:vnd:ext:query",
"options": {}
}]
}
// Response includes all fields
{
"data": {
"type": "order",
"id": "12345",
"attributes": {
"order_number": "ORD-2024-001",
"status": "pending",
"total_amount": { "amount": "99.99", "currency": "USD" },
"item_count": 3,
"notes": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:35:00Z"
}
}
}

Empty array returns only type and id:

// Request
{
"fields": {
"self": []
}
}
// Response
{
"data": {
"type": "order",
"id": "12345"
}
}

Request only identifiers:

{
"fields": {
"self": []
}
}

Request key fields for list views:

{
"fields": {
"self": ["id", "status", "total_amount", "created_at"]
}
}

Request all fields including relationships:

{
"fields": {
"self": ["id", "order_number", "status", "total_amount", "notes", "created_at", "updated_at"],
"customer": ["id", "name", "email"],
"items": ["id", "sku", "name", "quantity", "price"]
},
"relationships": ["customer", "items"]
}

Relationships define connections between resources. Vend supports requesting related resources in a single call, reducing round trips and enabling efficient data fetching.

Use the relationships array to include related resources:

{
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"relationships": ["customer", "items"]
}
}
]
}
  • relationships is an array of relationship names
  • Related resources are returned in the included array
  • Only declared relationships can be requested
  • Order in the array does not affect response

Each relationship contains a data member with resource identifiers:

{
"data": {
"type": "order",
"id": "12345",
"attributes": { ... },
"relationships": {
"customer": {
"data": { "type": "customer", "id": "42" }
},
"items": {
"data": [
{ "type": "order_item", "id": "1" },
{ "type": "order_item", "id": "2" }
]
}
}
}
}
Typedata ValueDescription
To-one{ "type": "...", "id": "..." }Single related resource
To-many[{ "type": "...", "id": "..." }, ...]Multiple related resources
Empty to-onenullNo related resource
Empty to-many[]No related resources

When relationships are requested, full resource objects appear in included:

{
"result": {
"data": {
"type": "order",
"id": "12345",
"attributes": { "status": "pending" },
"relationships": {
"customer": {
"data": { "type": "customer", "id": "42" }
}
}
},
"included": [
{
"type": "customer",
"id": "42",
"attributes": {
"name": "Alice",
"email": "alice@example.com"
}
}
]
}
}

A compound document contains the primary resource(s) plus related resources.

{
"result": {
"data": { ... }, // Primary resource(s)
"included": [ ... ], // Related resources
"meta": { ... } // Optional metadata
}
}
  1. Each resource in included MUST be unique by type + id
  2. Resources in included MUST be referenced by at least one relationship
  3. included resources MAY have their own relationships
  4. Circular references are allowed (resource A → B → A)

When multiple resources reference the same related resource, it appears once in included:

{
"data": [
{
"type": "order",
"id": "1",
"relationships": {
"customer": { "data": { "type": "customer", "id": "42" } }
}
},
{
"type": "order",
"id": "2",
"relationships": {
"customer": { "data": { "type": "customer", "id": "42" } }
}
}
],
"included": [
{
"type": "customer",
"id": "42",
"attributes": { "name": "Alice" }
}
// Customer 42 appears only once
]
}

Request relationships of relationships using dot notation:

{
"relationships": ["customer", "items", "items.product"]
}

This includes:

  • customer - The order’s customer
  • items - The order’s line items
  • items.product - Each item’s product
{
"data": {
"type": "order",
"id": "12345",
"relationships": {
"customer": { "data": { "type": "customer", "id": "42" } },
"items": {
"data": [
{ "type": "order_item", "id": "1" },
{ "type": "order_item", "id": "2" }
]
}
}
},
"included": [
{
"type": "customer",
"id": "42",
"attributes": { "name": "Alice" }
},
{
"type": "order_item",
"id": "1",
"attributes": { "quantity": 2, "price": { "amount": "29.99", "currency": "USD" } },
"relationships": {
"product": { "data": { "type": "product", "id": "prod_abc" } }
}
},
{
"type": "order_item",
"id": "2",
"attributes": { "quantity": 1, "price": { "amount": "49.99", "currency": "USD" } },
"relationships": {
"product": { "data": { "type": "product", "id": "prod_xyz" } }
}
},
{
"type": "product",
"id": "prod_abc",
"attributes": { "name": "Widget", "sku": "WDG-001" }
},
{
"type": "product",
"id": "prod_xyz",
"attributes": { "name": "Gadget", "sku": "GDG-002" }
}
]
}

Servers SHOULD enforce a maximum nesting depth (e.g., 3 levels):

// Allowed
"items.product.category"
// May be rejected
"items.product.category.parent.parent"

Servers MUST define which relationships are available:

{
"relationships": ["customer", "items", "shipping_address", "billing_address"]
}
  • Requesting non-allowed relationships MUST return an error
  • Servers SHOULD expose allowed relationships via vend.describe

Filter the primary resource based on related resource attributes:

{
"filters": {
"self": [
{ "attribute": "status", "operator": "equals", "value": "pending" }
],
"customer": [
{ "attribute": "type", "operator": "equals", "value": "vip" }
]
},
"relationships": ["customer"]
}

This returns orders that:

  1. Have status “pending”
  2. Have a customer with type “vip”
SELECT orders.* FROM orders
JOIN customers ON customers.id = orders.customer_id
WHERE orders.status = 'pending'
AND customers.type = 'vip'

Control which fields are returned for related resources:

{
"fields": {
"self": ["id", "status", "total_amount"],
"customer": ["id", "name"],
"items": ["id", "quantity", "price"]
},
"relationships": ["customer", "items"]
}

Request relationship data without full resources:

// Request without relationships option - identifiers only
{
"extensions": [{
"urn": "urn:vnd:ext:query",
"options": {}
}]
}
// Response includes relationship identifiers but no included array
{
"data": {
"type": "order",
"id": "12345",
"attributes": { ... },
"relationships": {
"customer": {
"data": { "type": "customer", "id": "42" }
}
}
}
// No "included" - only identifiers
}

This allows clients to:

  • See what relationships exist
  • Fetch related resources separately if needed
  • Reduce payload when relationships aren’t needed

FieldTypeRequiredDescription
fieldsobjectNoSparse fieldset selection per resource type
filtersobjectNoFilter conditions per resource type
sortsarrayNoSort criteria in priority order
paginationobjectNoPagination parameters
relationshipsarrayNoRelationships to include

FieldTypeDescription
capabilitiesarrayQuery capabilities the function supports

When the query extension is included:

  1. Server MUST validate all query parameters against function schema
  2. Server MUST apply filters before sorting
  3. Server MUST apply sorting before pagination
  4. Server MUST include requested relationships in included array
  5. Server MUST return only requested fields if sparse fieldsets specified

Query responses MUST use resource object format:

{
"result": {
"data": { ... }, // Resource object(s)
"included": [ ... ], // Related resources (if requested)
"meta": { ... } // Query metadata (pagination info, etc.)
}
}

Functions SHOULD declare which query capabilities they support via vend.describe:

{
"result": {
"function": "orders.list",
"extensions": {
"urn:vnd:ext:query": {
"capabilities": ["filtering", "sorting", "pagination", "relationships"],
"filters": {
"self": ["id", "status", "created_at", "total_amount"],
"customer": ["id", "type"]
},
"sorts": {
"self": ["id", "status", "created_at", "total_amount"]
},
"relationships": {
"available": ["customer", "items"],
"nested": { "items": ["product"] },
"max_depth": 3
},
"pagination": {
"styles": ["cursor", "offset"],
"default_limit": 25,
"max_limit": 100
}
}
}
}
}

{
"protocol": { "name": "vend", "version": "0.1.0" },
"id": "req_123",
"call": {
"function": "orders.list",
"version": "1",
"arguments": {}
},
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"filters": {
"self": [
{ "attribute": "status", "operator": "in", "value": ["pending", "processing"] }
]
},
"sorts": [
{ "attribute": "created_at", "direction": "desc" }
],
"pagination": {
"limit": 25
}
}
}
]
}
{
"protocol": { "name": "vend", "version": "0.1.0" },
"id": "req_123",
"result": {
"data": [
{
"type": "order",
"id": "12345",
"attributes": {
"status": "pending",
"total_amount": { "amount": "99.99", "currency": "USD" },
"created_at": "2024-01-15T10:30:00Z"
}
},
{
"type": "order",
"id": "12346",
"attributes": {
"status": "processing",
"total_amount": { "amount": "149.99", "currency": "USD" },
"created_at": "2024-01-15T09:15:00Z"
}
}
],
"meta": {
"pagination": {
"limit": 25,
"next_cursor": "eyJpZCI6MTIzNDZ9",
"has_more": true
}
}
},
"extensions": [
{
"urn": "urn:vnd:ext:query",
"data": {
"capabilities": ["filtering", "sorting", "pagination", "relationships"]
}
}
]
}
{
"protocol": { "name": "vend", "version": "0.1.0" },
"id": "req_124",
"call": {
"function": "orders.get",
"version": "1",
"arguments": { "id": "12345" }
},
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"fields": {
"self": ["id", "status", "total_amount"],
"customer": ["id", "name", "email"]
},
"relationships": ["customer", "items"]
}
}
]
}
{
"protocol": { "name": "vend", "version": "0.1.0" },
"id": "req_124",
"result": {
"data": {
"type": "order",
"id": "12345",
"attributes": {
"status": "pending",
"total_amount": { "amount": "99.99", "currency": "USD" }
},
"relationships": {
"customer": {
"data": { "type": "customer", "id": "42" }
},
"items": {
"data": [
{ "type": "order_item", "id": "1" },
{ "type": "order_item", "id": "2" }
]
}
}
},
"included": [
{
"type": "customer",
"id": "42",
"attributes": {
"name": "Alice",
"email": "alice@example.com"
}
},
{
"type": "order_item",
"id": "1",
"attributes": {
"quantity": 2,
"price": { "amount": "29.99", "currency": "USD" }
}
},
{
"type": "order_item",
"id": "2",
"attributes": {
"quantity": 1,
"price": { "amount": "39.99", "currency": "USD" }
}
}
]
},
"extensions": [
{
"urn": "urn:vnd:ext:query",
"data": {
"capabilities": ["filtering", "sorting", "pagination", "relationships", "sparse_fieldsets"]
}
}
]
}
{
"protocol": { "name": "vend", "version": "0.1.0" },
"id": "req_complex",
"call": {
"function": "tracking_events.list",
"version": "1",
"arguments": {}
},
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"fields": {
"self": ["id", "status", "location", "occurred_at"],
"shipment": ["id", "tracking_number"]
},
"filters": {
"self": [
{ "attribute": "occurred_at", "operator": "greater_than", "value": "2024-01-01T00:00:00Z" },
{ "attribute": "status", "operator": "in", "value": ["in_transit", "delivered"] }
],
"shipment": [
{ "attribute": "carrier", "operator": "equals", "value": "posti" }
]
},
"sorts": [
{ "attribute": "occurred_at", "direction": "desc" }
],
"relationships": ["shipment"],
"pagination": {
"limit": 50
}
}
}
]
}

{
"errors": [{
"code": "INVALID_ARGUMENTS",
"message": "Filter attribute not allowed: secret_field",
"retryable": false,
"source": {
"pointer": "/extensions/0/options/filters/self/0/attribute"
},
"details": {
"attribute": "secret_field",
"allowed": ["id", "status", "created_at"]
}
}]
}
{
"errors": [{
"code": "INVALID_ARGUMENTS",
"message": "Sort attribute not allowed: internal_score",
"retryable": false,
"source": {
"pointer": "/extensions/0/options/sorts/0/attribute"
}
}]
}
{
"errors": [{
"code": "INVALID_ARGUMENTS",
"message": "Relationship not available: secret_notes",
"retryable": false,
"source": {
"pointer": "/extensions/0/options/relationships/0"
},
"details": {
"relationship": "secret_notes",
"available": ["customer", "items"]
}
}]
}
{
"errors": [{
"code": "INVALID_ARGUMENTS",
"message": "Pagination limit exceeds maximum",
"retryable": false,
"source": {
"pointer": "/extensions/0/options/pagination/limit"
},
"details": {
"requested": 500,
"max_limit": 100
}
}]
}
{
"errors": [{
"code": "INVALID_ARGUMENTS",
"message": "Field not allowed: secret_notes",
"retryable": false,
"source": {
"pointer": "/extensions/0/options/fields/self/3"
},
"details": {
"field": "secret_notes",
"resource": "self",
"allowed": ["id", "order_number", "status", "total_amount", "created_at", "updated_at"]
}
}]
}

If previously using query parameters in call.arguments, migrate to the query extension:

Before (arguments):

{
"call": {
"function": "orders.list",
"version": "1",
"arguments": {
"filters": { ... },
"sorts": [ ... ],
"pagination": { ... }
}
}
}

After (extension):

{
"call": {
"function": "orders.list",
"version": "1",
"arguments": {}
},
"extensions": [
{
"urn": "urn:vnd:ext:query",
"options": {
"filters": { ... },
"sorts": [ ... ],
"pagination": { ... }
}
}
]
}

This separation clarifies that query capabilities are optional protocol features, not function-specific arguments.


Functions SHOULD advertise query capabilities via vend.describe:

{
"result": {
"function": "orders.list",
"extensions": {
"urn:vnd:ext:query": {
"capabilities": ["filtering", "sorting", "pagination", "relationships", "sparse_fieldsets"],
"filters": {
"self": ["id", "status", "created_at", "total_amount"],
"customer": ["id", "type", "country_code"]
},
"sorts": {
"self": ["id", "order_number", "status", "total_amount", "created_at", "updated_at"],
"default": [{ "attribute": "created_at", "direction": "desc" }]
},
"fields": {
"self": ["id", "order_number", "status", "total_amount", "item_count", "notes", "created_at", "updated_at"],
"customer": ["id", "name", "email", "type", "country_code"],
"items": ["id", "sku", "name", "quantity", "price", "total"],
"defaults": {
"self": ["id", "order_number", "status", "total_amount", "created_at"]
}
},
"relationships": {
"available": ["customer", "items", "shipping_address", "billing_address"],
"nested": {
"items": ["product", "product.category"]
},
"max_depth": 3
},
"pagination": {
"styles": ["cursor", "offset"],
"default_limit": 25,
"max_limit": 100
}
}
}
}
}