Docs

HTTP & Raw Queries

Object queries travel over HTTP as a raw query IR: a tree of nested nodes. Most application code uses the fluent API (see querying) — reach for raw JSON when you call the HTTP routes directly, use a generated client, or build queries outside TypeScript.

The browser builder in @sixb/client/query produces this exact IR and posts it to these routes for you. See typed queries for the type-checked path.

Routes

Every route accepts a JSON body with a top-level query node and validates it against the registered ontology.

RouteBodyReturns
POST /api/objects/query{ query, includeTotal? }{ objects, hasMore, nextPageToken?, total?, plan }
POST /api/objects/query/count{ query }{ count, plan }
POST /api/objects/query/exists{ query }{ exists, plan }
POST /api/objects/query/facets{ query, facets }{ facets, plan }

Set includeTotal: true on the query route to also count the full matching set; it is omitted by default because counting is more expensive than fetching a page.

Every response carries a diagnostic plan object for debugging. Application code ignores it and reads the result fields above. Validation and planning failures return HTTP 400 with a structured issues array (the client surfaces these as SixbQueryError).

Raw Query JSON

Nodes nest through an input field, innermost-first. This query starts with Invoice, filters, sorts, limits, and projects the returned properties:

JSON
{
  "query": {
    "kind": "project",
    "properties": ["id", "number", "amount", "status"],
    "input": {
      "kind": "limit",
      "limit": 20,
      "input": {
        "kind": "sort",
        "fields": [{ "kind": "property", "propertyId": "amount", "direction": "desc" }],
        "input": {
          "kind": "filter",
          "predicate": {
            "op": "and",
            "items": [
              { "op": "eq", "propertyId": "status", "value": "overdue" },
              { "op": "gte", "propertyId": "amount", "value": 5000 }
            ]
          },
          "input": { "kind": "start", "objectTypeId": "Invoice" }
        }
      }
    }
  }
}

Each object in the response carries its primary id, type, properties, and timestamps:

JSON
{
  "objects": [
    {
      "primaryId": "inv-1042",
      "objectTypeId": "Invoice",
      "properties": { "id": "inv-1042", "number": "2026-1042", "amount": 8200, "status": "overdue" },
      "createdAt": "2026-05-01T09:00:00.000Z",
      "updatedAt": "2026-06-10T12:30:00.000Z"
    }
  ],
  "hasMore": false,
  "plan": { "mode": "pushdown" }
}

Count, Exists, And Facets

POST /api/objects/query/count takes the same query body and returns { "count": number } without hydrating rows. POST /api/objects/query/exists returns { "exists": boolean } and stops after the first match — use it for cheap yes/no checks.

POST /api/objects/query/facets returns grouped counts. Add a facets array; each entry needs a propertyId and a limit:

JSON
{
  "query": { "kind": "start", "objectTypeId": "Invoice" },
  "facets": [{ "propertyId": "status", "limit": 10 }]
}

The response returns one result per requested property:

JSON
{
  "facets": [
    {
      "propertyId": "status",
      "buckets": [
        { "value": "paid", "count": 128 },
        { "value": "overdue", "count": 14 }
      ]
    }
  ]
}

A faceted property must declare query.searchable: true and query.facet: true in the ontology (Invoice.status does). See search metadata.

Page Tokens

A page node returns an opaque nextPageToken when another page is available:

JSON
{
  "query": {
    "kind": "page",
    "pageSize": 25,
    "input": {
      "kind": "sort",
      "fields": [{ "kind": "property", "propertyId": "dueDate", "direction": "asc" }],
      "input": { "kind": "start", "objectTypeId": "Invoice" }
    }
  }
}

Send that token back as pageToken to read the next page. Keep the inner nodes identical between page requests — only pageToken changes:

JSON
{
  "query": {
    "kind": "page",
    "pageSize": 25,
    "pageToken": "<nextPageToken>",
    "input": {
      "kind": "sort",
      "fields": [{ "kind": "property", "propertyId": "dueDate", "direction": "asc" }],
      "input": { "kind": "start", "objectTypeId": "Invoice" }
    }
  }
}

An expand node attaches linked objects to each returned object under links without changing which objects the query returns. Each entry in expansions names one link — linkId, direction (outgoing or incoming), an optional sourceObjectTypeId to pin the source of an incoming link, an optional limit + orderBy to bound a "many" expansion to the top-N targets per parent, and a nested expand for deeper hops.

JSON
{
  "query": {
    "kind": "expand",
    "expansions": [
      {
        "linkId": "customer",
        "direction": "outgoing",
        "expand": [{ "linkId": "region", "direction": "outgoing" }]
      }
    ],
    "input": { "kind": "start", "objectTypeId": "Invoice" }
  }
}

Expanded objects appear on each row under links, keyed by link id. A "one" link resolves to a single object (or null); a "many" link resolves to an array. Expanded children have the same object shape — so nested links recurse — plus an optional linkProperties carrying the relationship's edge fields:

JSON
{
  "objects": [
    {
      "primaryId": "inv-1042",
      "objectTypeId": "Invoice",
      "properties": { "id": "inv-1042", "amount": 8200, "status": "overdue" },
      "createdAt": "2026-05-01T09:00:00.000Z",
      "updatedAt": "2026-06-10T12:30:00.000Z",
      "links": {
        "customer": {
          "primaryId": "cust-7",
          "objectTypeId": "Customer",
          "properties": { "id": "cust-7", "name": "Globex" },
          "createdAt": "2026-01-02T00:00:00.000Z",
          "updatedAt": "2026-01-02T00:00:00.000Z",
          "links": {
            "region": {
              "primaryId": "eu-west",
              "objectTypeId": "Region",
              "properties": { "id": "eu-west" },
              "createdAt": "2026-01-01T00:00:00.000Z",
              "updatedAt": "2026-01-01T00:00:00.000Z"
            }
          }
        }
      }
    }
  ],
  "hasMore": false,
  "plan": { "mode": "pushdown" }
}

Node Reference

NodeFieldsPurpose
startobjectTypeId, includeSubtypes?Begin with all objects of one type.
filterinput, predicateApply a property predicate tree.
textinput, query, fields?Keyword search over search.defaultText or explicit fields.
vectorinput, vector, propertyId, kNearest-neighbor search on a numeric-array property.
traverseinput, linkId, direction, sourceObjectTypeId?Follow an outgoing or incoming link.
expandinput, expansionsAttach linked objects to each row under links, keeping the result type. See Expanding Links.
setop, inputsCombine object sets with union, intersect, or subtract.
sortinput, fieldsOrder by properties or provider relevance.
limitinput, limitBound the result count.
pageinput, pageSize, pageToken?Request a cursor page.
projectinput, properties?Return only the listed properties.

Each sort field is { "kind": "property", "propertyId", "direction" } or { "kind": "relevance", "direction" }; relevance requires a text node upstream and provider support.

Raw queries can traverse wildcard links that the fluent builder cannot (the result type can't be inferred). When several types declare a link with the same linkId, omit sourceObjectTypeId on an incoming traverse to match the union of all of them, or set it to pin one source type.

Predicate Shapes

Inside a filter node, predicate is a predicate tree.

PredicateRaw shape
and / or{ "op": "and", "items": [...] }
not{ "op": "not", "item": ... }
eq / neq / lt / lte / gt / gte{ "op": "gte", "propertyId": "amount", "value": 5000 }
in{ "op": "in", "propertyId": "status", "values": ["sent", "overdue"] }
exists{ "op": "exists", "propertyId": "dueDate", "value": true }
contains{ "op": "contains", "propertyId": "number", "value": "2026" }

Set value: false on exists to match a missing property. Values are checked against the property schema; ordered comparisons (lt/gte/sorting) require an orderable schema — string, number, date, timestamp, uuid, or enum. See querying for null-versus-missing semantics.

Calling A Route

BASH
curl -X POST http://localhost:3000/api/objects/query \
  -H "content-type: application/json" \
  -d '{
    "query": {
      "kind": "limit",
      "limit": 20,
      "input": {
        "kind": "filter",
        "predicate": { "op": "eq", "propertyId": "status", "value": "overdue" },
        "input": { "kind": "start", "objectTypeId": "Invoice" }
      }
    }
  }'

Authentication and CSRF handling follow your server configuration; see authentication.

Provider Support

SQLite and PostgreSQL object storage cover the common graph workflow: property filters, text search, sorting, limits, cursor pages, link traversal, set operations, and projection.

Vector search and relevance sorting require explicit storage-provider support. When a provider can't execute a requested feature, Sixb returns a structured planning error rather than a partial or approximate result. For simple bounded filters and sorts, Sixb can sometimes evaluate the query in the core runtime when a provider lacks native support — but treat text search, traversal, set operations, vector search, and relevance sorting as provider-backed.

Search docs

Search the documentation