BlitzGraph beta · occasional interruptions may occur
Documentation

Build with BQL

BlitzStore is a polymorphic graph database. Define your data as , connect them with , and query everything, including nested relationships, in a single JSON request.

JSON in, JSON out
No SQL strings, no ORM chains
Graph traversal
Expand relations without N+1
Atomic mutations
Batch operations, all or nothing

Connect

Set up HTTP access, choose the right token family, and keep app-session auth separate from dataserver auth.

#

Authenticate

The BlitzStore data server accepts Authorization: Bearer <token> only. There is no API-Key header. Four token families use that same transport:

  • bzt_*: scoped customer operator grant tokens (server, database or namespace scope). Mint these through the BlitzGraph control plane and use them for data-plane calls (/units/query, /units/mutate, /resources/mutate).
  • bzu_*: subject credentials for portal end-users, bound to one unit inside one namespace and confined to the app surface they were minted for. The raw API refuses them with SURFACE_CONFINED.
  • bzi_*: unrestricted per-instance internal keys reserved for explicit break-glass recovery.
  • bzc_*: 60-second capabilities bound to one instance, HTTP action, and optional database/namespace scope, within the signing key’s action/audience policy. The quarantine worker uses a restricted server-target capability bound to the exact request body. Internal-key management remains bzi_*-only.
BQL · first call with a grant token
curl -X POST https://api.blitzgraph.com/units/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer bzt_your.token-here" \
  -d '{"query":{"$kinds":"User","$fields":"*","$limit":10}}'

Runtime rotation of ordinary bzi_* keys happens through /server/mutate. The bzi_bootstrap.* key is owned by BLITZSTORE_SECRET_AUTH_BOOTSTRAP_KEY in the instance secret container: publish that container first, then restart the server. New grant tokens are projected through /server/mutate.

Better Auth boundary

Better Auth bearer access tokens do not authenticate directly against the dataserver. They authenticate agents on BlitzGraph app routes such as POST /connect/connection, which then issue a scoped bzt_* credential for the Space the user approved. The dataserver only ever sees the minted bzt_*.

A portal end-user is a different lane: their session is minted by the dataserver as a bzu_* subject credential, confined to the one app surface it was issued for and refused on the raw API.

MCP target binding

Run claude mcp add --transport http blitzgraph https://blitzgraph.com/mcp or codex mcp add blitzgraph --url https://blitzgraph.com/mcp. Then sign in in your browser and choose a Space. Codex requires codex mcp login blitzgraph after adding the server.

The MCP OAuth connect flow authorizes the issued connection grant against a Space, not a fixed namespace or server. Every call resolves the Space's current placement live, so relocating a Space never invalidates an existing grant. Tools inherit the selected namespace target; use opts.defaultSubspace when one call should target a different subspace default.

Platform

Storage scopes isolate data; live queries keep it synchronized; durable workflows orchestrate work; effective limits explain capacity; and BlitzStudio operates all of those surfaces.

#

Namespaces & Subspaces

A is the top-level isolation boundary inside a database. Each has its own schema, data, and indexes. A is another isolation boundary inside that namespace: data, schema, portals, and indexes are scoped to it. Free-tier users get one namespace and can create multiple subspaces (e.g. main, archive, drafts).

BQL · target a subspace in queries and mutations
// HTTP envelope default
{ "query": { "$kinds": "Note", "$fields": "*" }, "opts": { "defaultSubspace": "archive" } }

// Per-query override
{ "$kinds": "Note", "$subspace": "archive", "$fields": "*" }

// Per-mutation override
{ "$setKinds": ["Note"], "content": "temp", "$subspace": "drafts" }

// Omit $subspace → defaults to "main"
{ "$kinds": "Note", "$fields": "*" }
BQL · create and manage subspaces (admin API)
// POST /resources/mutate: create a subspace
{ "mutation": { "$type": "subspace", "$op": "create", "name": "archive", "storage": "memory" } }

// POST /resources/query: list subspaces
{ "query": { "$type": "subspace", "$op": "query" } }

Data is fully isolated between subspaces. A query on "main" never sees data from "archive". Each subspace can have its own schema, defined with plus opts.defaultSubspace, or via ns.defaultSubspace("name").definitions.import() in the SDK.

For namespace-scoped self-service transfer, use runs.transfer operations through /runs/query and /runs/mutate. Upload strict .bzg bundles through /runs/artifacts; imports run detached, and the same bundle can resume a matching interrupted import.

Per-unit $history is unbounded by default. Set the server env vars BLITZSTORE_HISTORY_RETENTION_DAYS and/or BLITZSTORE_HISTORY_UNIT_MAX_EVENTS to bound it; a background sweep removes events past the retention window or cap, and $history reads only see surviving entries.

#

Live Queries

A keeps one ordered BQL result synchronized over SSE. The raw POST /units/live lane is for scoped grants and internal keys. Portal sessions subscribe only through a validated named query operation at /_blitz/ops/query/{op}/live, so a browser never gains a raw-query capability.

BQL · raw SDK subscription
for await (const frame of client.queryLive({
  query: { $kinds: 'Ticket', $sort: ['priority', '$iid'], $fields: '*' },
  subspace: 'main',
  resume: lastAppliedCheckpoint,
})) {
  // Persist frame.checkpoint only after the whole snapshot/patch applies.
  applyAtomically(frame)
}
BQL · React and portal hooks
// Scoped grant / internal-key application
const tickets = useLiveQuery({ $kinds: 'Ticket', $sort: ['$iid'], $fields: '*' })

// Portal AppUser: generated named-operation ref
const myTickets = useLiveOperation(myTicketsRef, { status: 'open' })
Atomic LiveFrame
Snapshots, patches, heartbeats, and terminal frames share one generated contract. A patch is all-or-nothing.
Opaque resume
Reconnect with the last applied checkpoint. Never parse, edit, or persist a checkpoint before its frame applies.
Fail-closed authority
Expiry, revocation, subject-floor changes, and operation-policy changes are revalidated before protected delivery.

A retryable LIVE_CONTINUITY_GAP means the server fenced a gap and is repairing it. LIVE_RESUME_UNAVAILABLE is stable: an operator must configure BLITZSTORE_SECRET_LIVE_RESUME_KEYRING before resume is available. The client store backs off reconnects and falls back to a fresh authorized snapshot when a retained replay can no longer be used.

#

Actions & Workflows

Available in BlitzGraph 0.61.0. An Action is a typed reusable step; a is a durable state machine that orchestrates Actions, BQL, waits, signals, nested workflows, and bounded fan-out. Definitions are linted before persistence, and every run pins its executable closure and program digest.

BQL · inline TypeScript action
{ $op: 'create', $type: 'action', name: 'scoreLead',
  type: 'pure', mode: 'inline',
  inputSchema: { type: 'object', properties: { score: { type: 'number' } } },
  outputSchema: { type: 'number' },
  $ts: 'return (input as { score: number }).score satisfies number'
}
BQL · protected WASM action
{ $op: 'create', $type: 'action', name: 'protectedScore',
  type: 'pure', mode: 'wasm',
  module: 'scorer@sha256:<64 lowercase hex>',
  inputSchema: { type: 'object' },
  outputSchema: { type: 'number' }
}

Inline code accepts $js, $ts, or explicit $code. TypeScript is stored as authored source, but the server rejects imports, modules, JSX, decorators, enums, namespaces, and other runtime-generating syntax before a definition persists. Studio diagnostics use the same generated restricted-profile manifest; server lint remains authoritative.

BQL · namespace-scoped module lifecycle
const uploaded = await client.resources.module.upload('scorer', wasmBlob)
const pinned = uploaded.data.module // name@sha256:<digest>, abiVersion: 1

await client.resources.query({ query: { $type: 'module', $op: 'query' } })
await client.resources.query({ query: { $type: 'module', $op: 'query', name: pinned } })
const originalBytes = await client.resources.module.download(pinned)
await client.resources.mutate({ mutation: { $type: 'module', $op: 'delete', name: pinned } }) // rejected while an Action references it

WASM is available only for pure×wasm. Components export process(string) → result<string, string>, have no host imports, and run with bounded memory, output, errors, logs, cache bytes, and deadlines. The server pins and validates the WASM ABI in the durable workflow program; public Action definitions do not accept an abiVersion field.

BQL · run once, inspect or stream the durable task
const run = await client.runs.mutate<{ runId: string }>({
  mutation: {
    $type: 'workflow', $op: 'execute', workflow: 'onboardCustomer',
    idempotencyKey: 'signup:customer-01J...',
    input: { customerId: 'customer-01J...' },
  },
})

const runId = run.data.runId
const status = await client.runs.query({ query: { $type: 'run', $op: 'inspect', $iid: runId } })
for await (const event of client.runs.stream(runId)) {
  console.log(event)
}
Durable control flow
Waits, signals, retries, nested calls, keyed concurrency, and saga compensation survive process restarts.
Bounded map
Map over a named Action or Workflow with bounded concurrency, ordered results, retries, and throw or collect behavior.
Inspectable recovery
Typed events expose child waves, joins, retries, compensation, owning workflow, and W3C trace correlation.

Crash recovery resumes the full caller stack and the remaining compensation cursor. Mapped child membership is recorded before dispatch, so joins and cancellation do not lose children after a worker handoff. The runs.mutate and runs.query routes expose the same lifecycle.

#

Effective Limits

An is the value the server actually enforces after defaults, configuration, environment overrides, and request scope are resolved. The catalog and rejection details are projections of that same object, so documented and enforced values cannot drift.

BQL · SDK
const { data: config } = await client.resources.query<{ limits: Array<{ limitId: string; effectiveValue: number; unit: string }> }>({
  query: { $type: "config", $op: "resolveEffectiveConfig" }
})

for (const limit of config.limits) {
  console.log(limit.limitId, limit.effectiveValue, limit.unit)
}
BQL · HTTP / MCP
POST /resources/query
{ "query": { "$type": "config", "$op": "resolveEffectiveConfig" } }
// Admission limits are returned in data.limits.

// MCP tool: no input body
limits.effective
Exact value and unit
Configured, effective, and observed values use generated units and stable limit identities.
Authority scoped
Database and namespace callers see only their exact effective scope; AppUser and guest access is denied.
Actionable rejection
retryAfterMs plus retryLater or afterRequestChange tells clients how to respond without parsing messages.
#

BlitzStudio

BlitzStudio is the built-in visual interface for managing your data. It connects to any BlitzStore instance and gives you a full workspace with schema editing, data browsing, and a live query console.

BlitzSheet
Spreadsheet-style data browser. View, edit, create, and delete units in a grid. Filter and sort by any field.
QueryStudio
Live BQL console. Write queries and mutations, see results in real-time as a JSON tree.
Schema Editor
Visual schema management. Create kinds, define fields, set up roles and relations.
Apps (Portals)
Deploy custom frontends that connect to your BlitzStore data. Each app gets its own route, served from your namespace.
Namespace transfer
Export stored artifacts or download .bzg bundles, import into a fresh target, and poll transfer status without leaving Studio.
Env values
Manage namespace constants and write-only sealed secrets through the typed env admin surface.

Schema

Define your data model with , fields, , validations, computed fields, and mutation hooks. Import your once and the same model drives /units/query, /units/mutate, and BlitzStudio.

#

Database Structure

BlitzStore separates (your units and connections) from ( and ).

data ── units, arcs, indexes
definitions
schema ── kinds, dataFields, roleFields, linkFields
portals ── apps, pages, layouts, components
  • POST /units/query and POST /units/mutate operate on data
  • POST /definitions/import, /definitions/query, /definitions/mutate operate on definitions (see )
  • POST /resources/mutate uses { mutation: { $type, $op, ... } }
  • POST /units/import returns JSON by default; add Accept: text/event-stream for SSE progress
#

Kinds & Fields

  • A is a schema definition (like a class or table)
  • A is a stored instance of one or more kinds
  • Kinds have three field types:
    • holds values, validations, and compute behavior
    • defines connection points (see )
    • is a shortcut to traverse from the other side
  • Mutation behavior that spans multiple fields belongs on kind-level
BQL · schema definition
{
  "kinds": {
    "User": {
      "dataFields": {
        "name": { "valueType": "TEXT" },
        "email": { "valueType": "EMAIL", "unique": true },
        "age": { "valueType": "INTEGER" }
      }
    },
    "Article": {
      "dataFields": {
        "title": { "valueType": "TEXT", "required": true, "fts": true },
        "body": { "valueType": "TEXT", "fts": true },
        "status": { "valueType": "TEXT" }
      }
    }
  }
}
TEXT
Strings
INTEGER
Whole numbers (i64)
DECIMAL
Exact base-10 (money)
FLOAT
IEEE 754 f64 (ML, science)
PERCENTAGE
Decimal-backed 0–1
CURRENCY
Money: amount + ISO 4217
BOOLEAN
true / false
DATE
YYYY-MM-DD
DATETIME
ISO with TZ (required)
TIME
HH:MM:SS.ffffff
DURATION
Combined w/d/h/m/s/ms/u
INTERVAL
Set of ranges (#intervals in, intervalFormat out)
EMAIL
With validation
URL
With validation
FILE
Upload via #file marker
RICH_TEXT
Rich text payload
COLOR
Color value
PHONE
Phone text
PASSWORD
Secret text
JSON
Nested objects
FLEX
Any type
ID
Identifier
REF
Unit or schema ref
FILE uploads

A FILE field stores a blob plus metadata. The TS client auto-detects File / Blob values in mutate() and switches to multipart; the JSON body carries a { "#file": "<key>" } marker that the server matches to the named part. On read, the value materialises as FileValue with filename / mime / size / url / thumbnailUrl.

BQL · upload via multipart marker (raw HTTP shape)
// multipart body has parts: "mutation" (JSON) + named file parts
{
  "$setKinds": ["Document"],
  "title": "Annual report",
  "attachment": { "#file": "file_0" }
}
// → server stores blob, returns FileValue with signed url
Field conversions

Change a field's valueType within INTEGER, DECIMAL, FLOAT, PERCENTAGE, within DATE, DATETIME, or between TEXT, RICH_TEXT. A background task rewrites stored units. TEXT becomes literal escaped markup; RICH_TEXT becomes visible text. Numeric conversions use numericPolicy: rejectOnLoss (default), round, truncate, or allowPrecisionLoss. NaN and infinities reject. DATETIME to DATE requires exact UTC midnight. CURRENCY and TIME have no conversion family.

#

Polymorphism & Inheritance

  • Polymorphism. A can hold several at once, and evolve them over time. A User can gain or lose Admin without being re-created.
  • Inheritance. A kind can declare extends; it inherits that parent's fields and roles, and $kinds: "Parent" queries match all descendants by default.
BQL · compose and evolve kinds
// Create a unit with two kinds at once
{ "$setKinds": ["User", "Admin"], "name": "Alice" }

// Add a kind to an existing unit
{ "$id": "user_abc", "$setKinds": [{ "$op": "add", "$kinds": ["Moderator"] }] }

// Remove a kind (at least one must remain)
{ "$id": "user_abc", "$setKinds": [{ "$op": "remove", "$kinds": ["Admin"] }] }
BQL · inherit fields and roles from a parent
{
  "kinds": {
    "User":       { "dataFields": { "email": { "valueType": "EMAIL" } } },
    "Admin":      { "extends": "User", "dataFields": { "level": { "valueType": "INTEGER" } } },
    "SuperAdmin": { "extends": "Admin" }
  }
}

// Matches User + Admin + SuperAdmin
{ "$kinds": "User" }

// Exact match, no descendants
{ "$kinds": "User", "$descendants": false }
#

Validations & Computed Fields

Field rules live next to each . Use built-in validators for type and range checks, custom $js validators for business rules, and computed fields when a value should be derived instead of stored. Computed fields can also read meta values like $id or $createdAt.

BQL · built-in + custom field validations
{
  "kinds": {
    "User": {
      "dataFields": {
        "age": {
          "valueType": "INTEGER",
          "validations": { "required": true, "min": 0, "max": 150 }
        },
        "email": { "valueType": "EMAIL" },
        "backupEmail": {
          "valueType": "TEXT",
          "validations": {
            "custom": [{
              "on": ["create", "update"],
              "$js": "$value.includes('@')",
              "message": "must be a valid email",
              "severity": "error"
            }]
          }
        }
      }
    }
  },
  "opts": { "defaultSubspace": "main" }
}
BQL · computed on read + editable default
{
  "kinds": {
    "User": {
      "idField": "email",
      "dataFields": {
        "firstName": { "valueType": "TEXT" },
        "familyName": { "valueType": "TEXT" },
        "email": { "valueType": "EMAIL", "unique": true },
        "fullName": {
          "valueType": "FLEX",
          "computeType": "computed",
          "$js": "firstName + ' ' + familyName"
        },
        "displayId": {
          "valueType": "FLEX",
          "computeType": "computed",
          "$js": "$id"
        },
        "role": {
          "valueType": "TEXT",
          "computeType": "editable",
          "$js": "'user'"
        }
      }
    }
  }
}
What is enforced
  • Content types like EMAIL already run system validation
  • Custom validators use $value and can target create and/or update
  • severity: "error" blocks the mutation
Computed behavior
  • computeType: "computed" runs on every query and rejects writes
  • computeType: "editable" + $js acts as a create-time default that can still be overwritten later
  • Computed fields can reference $id, $iid, $version, $createdAt, $updatedAt, and $kinds
#

Hooks

Hooks live at the top of the schema in a schema.hooks map keyed by name and target Units via { ops, $kinds, $filter }. Each hook declares its category through type (unit.validate, unit.transform, or unit.effect) and runs inside the mutation pipeline when its target matches the Unit draft. Use them when a write needs to derive fields, validate a final draft across multiple fields, or fire post-commit side effects.

BQL · transform + validate + effect
{
  "kinds": {
    "BlogPost": {
      "dataFields": {
        "title": { "valueType": "TEXT" },
        "slug": { "valueType": "TEXT" },
        "status": { "valueType": "TEXT" },
        "word_count": { "valueType": "INTEGER" }
      }
    }
  },
  // Hooks live as a sibling of kinds at the schema level (refactored 2026-05-09).
  // Each hook declares its category via "type" and applicability via "target".
  "hooks": {
    // transform: derive or normalize fields before validation
    "BlogPost.generateSlug": {
      "type": "unit.transform",
      "target": {
        "ops": ["create", "update"],
        "$kinds": { "$any": ["BlogPost"] }
      },
      "when": { "$js": "$this.title" },
      "$js": "({ slug: $this.title.toLowerCase().replace(/\\s+/g, '-') })"
    },
    "BlogPost.defaultStatus": {
      "type": "unit.transform",
      "target": {
        "ops": ["create"],
        "$kinds": { "$any": ["BlogPost"] }
      },
      "when": { "$js": "!$this.status" },
      "$js": "({ status: 'draft' })"
    },
    // validate: reject an invalid final draft state
    "BlogPost.requireTitle": {
      "type": "unit.validate",
      "target": {
        "ops": ["create", "update"],
        "$kinds": { "$any": ["BlogPost"] }
      },
      "$js": "$this.title && $this.title.length > 0",
      "message": "Title is required",
      "severity": "error"
    },
    // effect: post-commit side effects (no pre-effect on schema.hooks)
    "BlogPost.postLog": {
      "type": "unit.effect",
      "target": {
        "ops": ["create"],
        "$kinds": { "$any": ["BlogPost"] }
      },
      "$js": "true"
    }
  }
}
BQL · remote transform hook
{
  "kinds": {
    "Person": {
      "dataFields": {
        "name": { "valueType": "TEXT" }
      }
    }
  },
  "hooks": {
    "Person.upcase": {
      "type": "unit.transform",
      "target": {
        "ops": ["create", "update"],
        "$kinds": { "$any": ["Person"] }
      },
      "remote": "upcase_name@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    }
  }
}
How hooks run
  • Order is transform → validate → effect
  • Validate hooks see the final transaction state and can also inspect $input
  • Hooks targeting ancestor kinds can include descendants with $descendants
  • Transform hooks can return patches inline or with an explicit return
  • Hook bodies accept $js, $ts (types erased), or a pinned WASM remote module
Current limits
  • Hooks mutate $this; cross-unit cascades are not supported yet
  • unit.effect runs post-commit; blocking checks belong in unit.validate
  • Transform hooks on link are not implemented yet
  • Non-converging transform loops fail with max-depth protection

When a unit is deleted and that break severs its relationships, each break surfaces to the surviving unit's update hooks as an unlink event in $delta.arcs. The same stream also appears in $history and in normalized mutation output, so a survivor can react to losing a connection.

BQL · observe unlink events in an update hook
{
  "hooks": {
    // Post survives when its author User is deleted; the broken arc
    // arrives as an unlink entry in $delta.arcs on the survivor's update.
    "Post.markUnlinkSeen": {
      "type": "unit.transform",
      "target": {
        "ops": ["update"],
        "$kinds": { "$any": ["Post"] }
      },
      "$js": "var arcs = ($delta && $delta.arcs) || []; return { unlinkSeen: arcs.some(a => a.$op === 'unlink') };"
    }
  }
}
#

Relations

  • A lives on the relation side and defines who can connect and how many ()
  • A lives on the player side and names the relation kind plus the role it plays there
  • are kinds too, so they can carry their own data fields and be queried directly
  • target: "relation" returns the relation units; target: "role" and targetRoles project through to the opposite endpoint(s)

There are two shapes to remember. For a simple dependency like book → author, put the on one side and keep the reverse as a . That is a direct relation, not a tunnel. Giulietta's Rule is just a recommendation for choosing the owner. When the connection needs its own fields, create an intermediate relation kind and expose one raw link plus one projected tunnel on each side if that helps query ergonomics.

BQL · direct binary relation: Author ↔ Book
// Direct relation, no tunnel.
// One valid ownership choice: Book owns the roleField, Author keeps the reverse linkField.
{
  "kinds": {
    "Author": {
      "dataFields": {
        "name": { "valueType": "TEXT" },
        "slug": { "valueType": "TEXT", "unique": true }
      },
      "linkFields": {
        "books": {
          "relation": "Book",
          "plays": "author"
        }
      }
    },
    "Book": {
      "dataFields": {
        "title": { "valueType": "TEXT" },
        "publishedYear": { "valueType": "INTEGER" }
      },
      "roleFields": {
        "author": {
          "playedBy": ["Author"],
          "cardinality": "ONE",
          "required": true
        }
      }
    }
  }
}
BQL · intermediate relation: Company ↔ Membership ↔ Employee
{
  "kinds": {
    "Company": {
      "dataFields": {
        "name": { "valueType": "TEXT" }
      },
      "linkFields": {
        "memberships": {
          "relation": "Membership",
          "plays": "company",
          "target": "relation"
        },
        "employees": {
          "relation": "Membership",
          "plays": "company",
          "target": "role",
          "targetRoles": ["employee"]
        }
      }
    },
    "Employee": {
      "dataFields": {
        "name": { "valueType": "TEXT" },
        "email": { "valueType": "EMAIL" }
      },
      "linkFields": {
        "memberships": {
          "relation": "Membership",
          "plays": "employee",
          "target": "relation"
        },
        "companies": {
          "relation": "Membership",
          "plays": "employee",
          "target": "role",
          "targetRoles": ["company"]
        }
      }
    },
    "Membership": {
      "dataFields": {
        "title": { "valueType": "TEXT" },
        "startDate": { "valueType": "DATE" }
      },
      "roleFields": {
        "company": { "playedBy": ["Company"], "cardinality": "ONE" },
        "employee": { "playedBy": ["Employee"], "cardinality": "ONE" }
      }
    }
  }
}
BQL · query relation data fields
{
  "$kinds": "Company",
  "$fields": [
    "name",
    {
      "$expand": "memberships",
      "$fields": [
        "title",
        "startDate",
        { "$expand": "employee", "$fields": ["name", "email"] }
      ]
    }
  ]
}
ONE
0 or 1 connection. Add required for exactly 1.
MANY
0..N connections. Supports min and max bounds.
INTERVAL
Data-field only. Role and link fields reject it. Values use the { "#intervals": ... } input shape. Results carry no cast key; their shape follows the field's intervalFormat.
Polymorphic roles

A role can be played by several kinds via playedBy: ["A", "B"]. Nested creates must specify $setKinds so the engine knows which kind to create; when playedBy has exactly one kind, it is inferred.

BQL · polymorphic role + projected tunnel
{
  "kinds": {
    "Employee": {
      "dataFields": { "name": { "valueType": "TEXT" } }
    },
    "Agency": {
      "dataFields": { "name": { "valueType": "TEXT" } }
    },
    "Project": {
      "dataFields": { "name": { "valueType": "TEXT" } },
      "linkFields": {
        "assignments": {
          "relation": "Assignment",
          "plays": "project",
          "target": "relation"
        },
        "assignees": {
          "relation": "Assignment",
          "plays": "project",
          "target": "role",
          "targetRoles": ["assignee"]
        }
      }
    },
    "Assignment": {
      "roleFields": {
        "project": { "playedBy": ["Project"], "cardinality": "ONE" },
        "assignee": { "playedBy": ["Employee", "Agency"], "cardinality": "ONE" }
      }
    }
  }
}

// Nested create: $setKinds disambiguates the polymorphic assignee
{
  "$setKinds": ["Assignment"],
  "project": "project_123",
  "assignee": { "$setKinds": ["Agency"], "name": "Acme Staffing" }
}
Symmetric & self relations

symmetric: true on a role field collapses A↔B and B↔A to a single edge — the duplicate is rejected with SYMMETRIC_RELATION_EXISTS. By default each role rejects the same player appearing in two distinct roles of the same relation unit (SELF_RELATION_FORBIDDEN); opt in per role with allowSelfRelation: true.

BQL · symmetric friendship + self-reference allowed on Document
{
  "kinds": {
    "Friendship": {
      "roleFields": {
        "friends": { "playedBy": ["User"], "cardinality": "MANY", "symmetric": true }
      }
    },
    "Reference": {
      "roleFields": {
        "from": { "playedBy": ["Document"], "allowSelfRelation": true },
        "to":   { "playedBy": ["Document"], "allowSelfRelation": true }
      }
    }
  }
}
#

Schema Operations

Four endpoints manage at runtime: import, export, query, and mutate. Import/export use direct JSON bodies with opts.defaultSubspace. Query/mutate use the query / mutation envelopes.

BQL · definitions/import · full schema (additive only)
// POST /definitions/import
{
  "schema": {
    "kinds": {
      "User": {
        "dataFields": {
          "name": { "valueType": "TEXT" },
          "email": { "valueType": "EMAIL", "unique": true }
        },
        "linkFields": {
          "posts": { "relation": "Post", "plays": "author" }
        }
      },
      "Post": {
        "dataFields": { "title": { "valueType": "TEXT" } },
        "roleFields": {
          "author": { "playedBy": ["User"], "cardinality": "ONE" }
        }
      }
    }
  },
  "opts": { "defaultSubspace": "main" }
}
BQL · definitions/export · current definitions for the subspace
// POST /definitions/export
{ "opts": { "defaultSubspace": "main" } }

// → { "data": { "$bzv": "<release>", "schema": { "kinds": { ... } }, "portals": { ... } } }
// The native artifact includes its schema branch; read data.schema for schema-only consumers.
// For per-field type metadata: GET /docs/list?section=term, then GET /docs/term/<TYPE>.

Definition endpoints move schema, portals, automations, and env declarations. The runs.transfer operations on /runs/query and /runs/mutate, with bytes on /runs/artifacts, move stored artifacts and namespace/subspace .bzg bundles. They default to the portable profile: selected ordinary data, including User/Persona units, is preserved; source accounts, credentials, sessions, grants, and active authority are excluded. Public constants retain values; privateConstants and secrets travel as unconfigured name declarations. Subspace templates project the declarations their definitions need; importing root env also requires runs.transfer.import. Bundle imports run detached; re-upload resumes only the same artifact and pinned options. The explicit sealedRecovery profile is available only on direct bundle routes to database/server operators and requires exact source scope with definitions and units; stored artifacts reject it.

BQL · namespace bundle export → detached import
// Preview the selected namespace through the existing export planner.
POST /runs/query
{ "query": { "$type": "transfer", "$op": "planExport" } }

// Stage a portable export; writes pause only during artifact staging.
POST /runs/mutate
{ "mutation": {
  "$type": "transfer", "$op": "export", "profile": "portable", "scope": "namespace",
  "include": { "definitions": true, "units": true, "files": false },
  "subspaces": ["main"]
} }
GET /runs/artifacts/<returned-artifactId>?source=stored

// Stage raw bytes; retain the returned artifactId.
POST /runs/artifacts
Content-Type: application/octet-stream

// Import the staged artifact asynchronously in the selected namespace.
POST /runs/mutate
{ "mutation": {
  "$type": "transfer", "$op": "import", "profile": "portable", "scope": "namespace",
  "include": { "definitions": true, "units": true, "files": false },
  "source": { "kind": "uploaded", "artifactId": "<staged-id>" }
} }

// Poll the accepted importId. Only terminal.status === "succeeded" confirms completion.
POST /runs/query
{ "query": { "$type": "transfer", "$op": "query", "$iid": "<accepted-import-id>" } }

// SDK: portable subspace rename in the selected destination namespace.
const staged = await admin.runs.artifact.upload(file, { namespace: "destination" })
await admin.runs.mutate({ mutation: {
  $type: "transfer", $op: "import", profile: "portable", scope: "subspace", $subspace: "renamed",
  include: { definitions: true, units: true, files: false },
  source: { kind: "uploaded", artifactId: staged.data.artifactId }
} }, { namespace: "destination" })

// Sealed recovery uses the same custody owner with explicit sealedRecovery profile.
// It requires database/server authority and exact source coordinates.

FILE round-trip is controlled by include.files. On bundle routes it defaults to false to avoid accidental egress; set it to true to carry all blobs or to a { "perSubspace": { ... } } object for selected FILE fields. Store-level artifact routes default include.files to true because the transfer stays server-side.

App-user authorization is also authored in definitions. A subject kind marks itself with userKind and an email identity field. Protected kinds use permissions rules evaluated with $me. Portals use access rules for app/page/endpoint admission.

BQL · userKind + permissions + portal access
{
  "schema": {
    "kinds": {
      "Member": {
        "userKind": { "authFields": ["email"] },
        "idField": "email",
        "dataFields": {
          "email": { "valueType": "EMAIL", "unique": true },
          "name": { "valueType": "TEXT" }
        }
      },
      "Admin": {
        "extends": "Member",
        "userKind": { "authFields": ["email"] }
      },
      "Note": {
        "dataFields": {
          "title": { "valueType": "TEXT" },
          "privateText": { "valueType": "TEXT" },
          "ownerId": { "valueType": "TEXT", "required": true }
        },
        "permissions": {
          "query": "ownerId",
          "create": { "check": { "$me.$kinds": "Member", "ownerId": "$me" } },
          "fields": {
            "privateText": { "query": { "$me.$kinds": "Admin" } }
          }
        }
      }
    }
  },
  "env": {
    "domains": {
      "notes-auth": {
        "secrets": ["resend_api_key"]
      }
    }
  },
  "portals": {
    "apps": {
      "notes": {
				"slug": "notes",
        "env": ["notes-auth"],
        "access": { "$kinds": ["Member"] },
        "auth": {
          "providers": {
            "emailPassword": { "enabled": true }
          },
          "delivery": {
            "provider": "resend",
            "apiKey": { "$secret": "env.notes-auth.resend_api_key" },
            "fromAddress": "Notes <[email protected]>"
          },
          "signup": {
            "mode": "open",
            "blueprint": {
              "$setKinds": ["Member"],
              "$authAnchorKind": "Member",
              "email": { "$auth": "email" },
              "name": { "$input": "name" }
            }
          }
        },
        "pages": {
          "/": {
            "tsxSource": "export default function NotesPage() { return null }"
          }
        }
      }
    }
  }
}
BQL · definitions/query · any definition type
// POST /definitions/query: all kinds
{ "query": { "$type": "kind" }, "opts": { "defaultSubspace": "main" } }

// Query a specific kind by $did
{ "query": { "$type": "kind", "$did": "K:abc123" } }

// Query fields of a kind
{ "query": { "$type": "dataField", "$filter": { "ownerKind": "User" } } }
BQL · definitions/mutate · incremental changes
// POST /definitions/mutate
{
  "mutation": [
    {
      "$op": "create",
      "$type": "kind",
      "name": "Product",
      "dataFields": {
        "title": { "valueType": "TEXT" },
        "price": { "valueType": "INTEGER" }
      }
    },
    {
      "$op": "create",
      "$type": "roleField",
      "ownerKind": "Order",
      "name": "customer",
      "playedBy": ["User"],
      "cardinality": "ONE"
    },
    {
      "$op": "create",
      "$type": "linkField",
      "ownerKind": "User",
      "name": "orders",
      "relation": "Order",
      "plays": "customer",
      "target": "relation"
    }
  ],
  "opts": { "defaultSubspace": "main" }
}

Queries

Every query is a JSON object sent to POST /units/query. Results come back as JSON, the shape is predictable based on your query.

#

Basics

Use $kinds to select by kind, $id to fetch a specific unit, and $fields to control what comes back.

BQL · all users
// Returns all User units (array)
{ "$kinds": "User", "$fields": "*" }
BQL · by ID
// Returns one unit (object or null)
{ "$id": { "User": "abc123" }, "$fields": "*" }
BQL · specific fields + sort + pagination
{
  "$kinds": "User",
  "$fields": ["name", "email", "age"],
  "$sort": [{ "$by": "age", "$order": "desc" }],
  "$limit": 10,
  "$offset": 20
}

For large result sets, prefer $cursor over $offset. Pass $cursor together with $limit; the response returns an opaque meta.$nextCursor token that you replay verbatim as the next $cursor. Absence of meta.$nextCursor means you reached the last page. Never parse or construct the token. Cursor paging requires $limit, excludes $offset, is root-query only, and is rejected with $groupBy and root aggregates.

BQL · cursor pagination · replay $nextCursor
// First page
{ "$kinds": "Task", "$limit": 50 }

// → { "data": [ ... ], "meta": { "count": 50, "$nextCursor": "eyJ..." } }

// Next page: replay meta.$nextCursor verbatim as $cursor
{ "$kinds": "Task", "$limit": 50, "$cursor": "<token from meta.$nextCursor>" }

// No meta.$nextCursor in the response → last page
#

Projections

$fields controls what data comes back. Use "*" for all fields, an array for specific ones, or $excludedFields to exclude. Arc fields in $fields return raw IDs. Use $expand to fetch full units instead. When composed kinds define the same field name differently, use $definedOn to select one defining kind; a genuinely ambiguous multi-kind unit returns an $ambiguous marker instead of guessing.

BQL · field selection modes
// All fields
{ "$kinds": "User", "$fields": "*" }

// Specific fields only
{ "$kinds": "User", "$fields": ["name", "email"] }

// All except some
{ "$kinds": "User", "$fields": "*", "$excludedFields": ["password"] }

// Arc fields without $expand → raw IDs
{ "$kinds": "Post", "$fields": ["title", "author"] }
// → { "title": "Hello", "author": "user_abc123" }
BQL · $definedOn · disambiguate a composed field name
{
  "$kinds": ["Bot", "User"],
  "$fields": [
    { "$fetch": "name", "$definedOn": "User", "$as": "userName" },
    { "$fetch": "name", "$definedOn": "Bot", "$as": "botName" }
  ]
}
#

Expanding Relations

$expand traverses relationships and inlines the connected units. No N+1. BlitzStore batches all traversals automatically. You can nest $expand at any depth, with its own $fields, $filter, $sort, and $limit. Use to include connection timestamps.

BQL · expand with sibling relations
{
  "$kinds": "Post",
  "$fields": [
    "title",
    { "$expand": "author", "$fields": ["name", "email"] },
    { "$expand": "comments", "$fields": ["text"] }
  ]
}
BQL · nested expand with filter and sort
{
  "$kinds": "User",
  "$fields": [
    "name",
    {
      "$expand": "posts",
      "$filter": { "status": "published" },
      "$sort": [{ "$by": "title", "$order": "asc" }],
      "$limit": 5,
      "$fields": ["title", "status"]
    }
  ]
}

Use $as to rename an expanded or virtual field in the response when the schema name does not read well on the client.

BQL · $as · rename a field in the response
{
  "$kinds": "User",
  "$fields": [
    { "$expand": "posts", "$as": "articles", "$fields": ["title"] }
  ]
}
#

Arc Metadata

works like but wraps each result in an metadata envelope with $arcCreatedAt (ISO timestamp). Useful for seeing when connections were made: friendships, memberships, audit trails.

BQL · $expandArc · connections with timestamps
{
  "$kinds": "Team",
  "$id": { "Team": "team1" },
  "$fields": [
    "name",
    { "$expandArc": "members", "$fields": ["name"] }
  ]
}

// Response wraps each member in an arc envelope:
// { "name": "Devs", "members": [
//   { "$arcCreatedAt": "2026-03-15T10:30:00Z", "$unit": { "$id": "...", "name": "Alice" } },
//   { "$arcCreatedAt": "2026-03-16T09:00:00Z", "$unit": { "$id": "...", "name": "Bob" } }
// ]}

Inside you can also filter and sort the relationships by when each connection was created, using $arcCreatedAt as a filter key and a sort key. The arc-time predicate is evaluated before the connected units are fetched, so non-matching connections are pruned early.

BQL · $arcCreatedAt · filter + sort connections by creation time
{
  "$kinds": "Team",
  "$id": { "Team": "t1" },
  "$fields": [
    {
      "$expandArc": "members",
      "$filter": { "$arcCreatedAt": { "$gte": { "#datetime": "2026-01-01T00:00:00.000000Z" } } },
      "$sort": "-$arcCreatedAt",
      "$limit": 10
    }
  ]
}
  • Comparison operators only: $gte, $gt, $lte, $lt, $eq, $neq
  • As a filter, $arcCreatedAt must be a top-level condition (or inside a top-level $and)
  • As a sort key it must be the primary (first) sort key
  • Valid only inside , never in plain
#

Aggregations

$groupBy groups results by field values, but it is not required for a single root summary row. Aggregate with $agg operators like COUNT, SUM, AVG, MIN, MAX, and more. Works at root level and nested inside $expand.

BQL · root aggregate without grouping
{
  "$kinds": "Order",
  "$aggFields": [
    { "%count": { "$agg": "COUNT" } },
    { "%total": { "$agg": "SUM", "$field": "amount" } },
    { "%avg": { "$agg": "AVG", "$field": "amount" } }
  ]
}

$groupBy emits distinct group-key rows by itself; add $aggFields when those rows need metrics. $groupBy.$filter is the only post-group filter, and the $groupBy object's $sort, $limit, and $offset order and paginate groups. Node-level $sort / $limit / $offset window candidate units before grouping. Group keys may be stored or computed scalar fields, ONE arc identities, or authored $js/$ts/$code/$expr expressions with an $as name. MANY-valued keys reject explicitly rather than creating ambiguous groups.

BQL · group by with multiple aggregations
{
  "$kinds": "Order",
  "$groupBy": {
    "$by": ["status"],
    "$filter": { "total": { "$gte": 1000 } },
    "$sort": [{ "$by": "total", "$order": "desc" }],
    "$limit": 5
  },
  "$aggFields": [
    { "%count": { "$agg": "COUNT" } },
    { "%total": { "$agg": "SUM", "$field": "amount" } }
  ]
}
BQL · group by arc identity and normalized expression
{
  "$kinds": "Order",
  "$groupBy": {
    "$by": [
      "customer",
      { "$js": "status.trim().toLowerCase()", "$as": "normalizedStatus" }
    ]
  },
  "$aggFields": [
    { "%count": { "$agg": "COUNT" } }
  ]
}
BQL · aggregation inside $expand
{
  "$kinds": "Team",
  "$fields": [
    "name",
    {
      "$expand": "members",
      "$groupBy": ["position"],
      "$aggFields": [
        { "%count": { "$agg": "COUNT" } }
      ]
    }
  ]
}
COUNT
SUM
AVG
MIN
MAX
MEDIAN
LIST
SET
FIRST
LAST
EVERY
SOME
Putting it all together
BQL · polymorphic search + computed fields
{
  "$kinds": { "$all": ["Human", "Spanish"] },
  "$search": "senior backend rust",
  "$filter": { "role": { "$in": ["engineer", "designer"] } },
  "$fields": [
    "name", "salary", "bonus",
    { "%total": { "$js": "salary + bonus" } },
    {
      "$expand": "projects",
      "$sort": [{ "$by": "budget", "$order": "desc" }],
      "$limit": 3,
      "$fields": [
        "title", "budget", "spent",
        { "%remaining": { "$js": "budget - spent" } }
      ]
    }
  ]
}

Mutations

Send mutations to POST /units/mutate. Operations are inferred from the shape of your JSON, or set $op explicitly.

#

Create, Update, Delete

$setKinds without a selector creates, and a selector with fields updates. Deleting requires $op: "delete".

BQL · create, update, delete
// Create: $setKinds without $id
{ "$setKinds": ["User"], "name": "Alice", "email": "[email protected]" }

// Update: $id with fields
{ "$id": "user_abc123", "email": "[email protected]" }

// Delete: $op with a selector
{ "$op": "delete", "$id": "user_abc123" }

// Bulk delete: with $filter
{ "$op": "delete", "$kinds": "Task", "$filter": { "done": true } }
create
$setKinds without $id
update
$id with data fields
delete
$op: "delete" with a selector
#

Upsert

$op: "upsert" updates when the identity resolves an existing unit and creates when it does not. Because it may create, $setKinds is required. Identity can come from either $id or $filter, but never both.

BQL · upsert by $id
// Kind has idField: "email"
// First call creates
{
  "$op": "upsert",
  "$id": "[email protected]",
  "$setKinds": ["User"],
  "email": "[email protected]",
  "name": "Alice"
}

// Second call updates same unit
{
  "$op": "upsert",
  "$id": "[email protected]",
  "$setKinds": ["User"],
  "name": "Alice V2"
}
BQL · upsert by $filter
// 0 matches -> create
{
  "$op": "upsert",
  "$setKinds": ["User"],
  "$filter": { "email": "[email protected]" },
  "email": "[email protected]",
  "name": "Bob"
}

// 1 match -> update
{
  "$op": "upsert",
  "$setKinds": ["User"],
  "$filter": { "email": "[email protected]" },
  "name": "Bob V2"
}
Valid identity selectors
Use $id for direct identity, or $filter for a 0-or-1 match lookup.
Errors
$id + $filter is invalid. A $filter that matches 2+ units is invalid too.
#

Batch Operations

Pass an array for atomic batch mutations. All succeed or all roll back. You can mix creates, updates, and deletes in the same batch. Use $var to reference units across operations, or nest child units directly inside their parent.

$var references — capture a created unit and link it later in the same batch
BQL · $var batch — create two units and join them
[
  { "$var": "_:user", "$setKinds": ["User"], "name": "Alice" },
  { "$var": "_:acct", "$setKinds": ["Account"], "provider": "github" },
  { "$setKinds": ["UserAccount"], "user": "_:user", "account": "_:acct" }
]
BQL · nested tree — embed child units directly inside the parent field
{
  "$setKinds": ["UserAccount"],
  "user": { "$setKinds": ["User"], "name": "Alice" },
  "account": { "$setKinds": ["Account"], "provider": "github" }
}
Deep nesting (3 levels)
BQL · deep nesting (3 levels)
// User → Tag → Group → Colors, all created atomically
{
  "$setKinds": ["User"],
  "tags": [{
    "$setKinds": ["Tag"],
    "group": {
      "$setKinds": ["Group"],
      "colors": [
        { "$setKinds": ["Color"], "name": "Red" },
        { "$setKinds": ["Color"], "name": "Blue" }
      ]
    }
  }]
}
Mixed operations in one batch
BQL · mixed operations in one batch
// Create + update in the same atomic batch
[
  { "$setKinds": ["User"], "name": "Bob", "status": "new" },
  { "$id": "existing_user_id", "status": "updated" }
]
#

Data Import

POST /units/import is a create-only fast path for seeding datasets. Root items without an explicit $op default to create; an arc element may also carry $op: "link"; any other $op is rejected. The builder topo-sorts $var references and chunks large arrays, so a single payload can carry an entire dataset and still respect parent-before-child ordering.

BQL · public ids on kinds with idField
// Country has idField:"code". Send code as data;
// $id is used to resolve same-import arcs.
POST /units/import
{
  "units": [
    { "$id": "country:mex", "$kinds": ["Country"],
      "code": "MEX", "name": "Mexico" },
    { "$id": "country:bra", "$kinds": ["Country"],
      "code": "BRA", "name": "Brazil" },
    {
      "$id": "sticker:mex-logo", "$kinds": ["Sticker"],
      "code": "MEX1", "label": "Logo", "country": "country:mex"
    }
  ]
}
BQL · export → import (arc topology preserved)
// units.export emits query-like units with $id, $kinds,
// data fields, and raw arc IDs. It omits $op, $var, $iid.
POST /units/import
{
  "units": [
    { "$id": "user:alice", "$kinds": ["User"],
      "name": "Alice", "posts": ["post:hello"] },
    { "$id": "post:hello", "$kinds": ["Post"],
      "title": "Hello", "author": "user:alice" }
  ]
}
$id in import
$id on an imported create object resolves same-import arc references and is stripped before create; send idField values as regular data fields.
In-batch references
Use exported $id values or $var: "_:<name>" on the parent and reference it from children. The builder pre-allocates ULIDs and topo-sorts. Internal ULIDs are never user-chosen.
SSE progress
Add Accept: text/event-stream to POST /units/import for chunked progress events; omit it for one-shot JSON. opts.batchSize tunes server-side chunking (default 5000) — do not split client-side.
#

Batch Size Limits

units.mutate caps a single call at 10,000 operations by default; definitions.mutate caps at 500 operations. Every root and nested mutation node (create, update, upsert, delete, query) counts, plus each explicit arc link / unlink / replace target. A filter-scoped bulk op counts as one regardless of how many units it matches — the cap bounds input batch size, not match count. Nodes are counted after $for / $if expansion.

Breaching the cap returns BATCH_TOO_LARGE, which points data callers at smaller units.mutate calls or at new units in chunks through units.import, and definition callers at smaller definitions.mutate calls. Raise the cap per-call (up to the server hard cap) or per-namespace (mutation.maxBatchSize / definitionMutation.maxBatchSize, raise-only — namespace config cannot lower the server default). maxTotalGeneratedItems (100,000) stays the absolute $for expansion ceiling even when maxBatchSize is raised.

#

Control Flow

$for generates mutation items from a loop; $if includes them conditionally. Control flow is expanded before the engine runs, so the batch stays flat and atomic. Both can nest and mix with regular items.

BQL · $for · array or $range source
// Range source (inclusive)
{
  "$for": { "$in": { "$range": [1, 3] }, "$as": "_:i" },
  "$do": [
    { "$setKinds": ["User"], "name": "user{_:i}", "index": "_:i" }
  ]
}

// Array source
{
  "$for": { "$in": ["rust", "graph", "blitz"], "$as": "_:tag" },
  "$do": [{ "$setKinds": ["Tag"], "name": "_:tag" }]
}
BQL · $if · conditional inclusion
// With optional else branch
{
  "$if": { "$js": "1 > 2" },
  "$then": [{ "$setKinds": ["Status"], "value": "then" }],
  "$else": [{ "$setKinds": ["Status"], "value": "else" }]
}

Write {_:var} inside strings to interpolate the loop variable. Total iterations are bounded by query limits to prevent runaway loops.

#

Graph Operations

Manage connections with $op inside arc fields (see for timestamps). link adds connections, unlink removes them, replace sets them exactly. Direct assignment (DX sugar) is shorthand for replace.

BQL · arc operations
// Link: add connections
{ "$id": "book1", "authors": { "$op": "link", "$id": "user1" } }

// Unlink: remove a connection
{ "$id": "book1", "authors": { "$op": "unlink", "$id": "user1" } }

// Replace: set exact connections
{ "$id": "book1", "authors": { "$op": "replace", "$id": ["user1", "user2"] } }

// DX sugar: direct assignment = replace
{ "$id": "book1", "authors": ["user1", "user2"] }
BQL · link by filter
{
  "$id": "book1",
  "authors": {
    "$op": "link",
    "$kinds": "User",
    "$filter": { "role": "dev" }
  }
}

For linkFields with target: "role", direct endpoint-shaped writes are rejected. Use a projected endpoint update for existing related units, or create the relation tree explicitly.

BQL · tunnel query and projected update
// Query projected endpoints through a tunnel link
{
  "$kinds": "Candidate",
  "$fields": [
    "name",
    {
      "$expand": "interviewers",
      "$fields": ["name", "department"]
    }
  ]
}

// Update the projected endpoints selected by the tunnel
{
  "$id": "candidate_123",
  "interviewers": {
    "$op": "update",
    "$filter": { "department": "Ops" },
    "department": "Platform"
  }
}
BQL · tunnel relation-tree create
{
  "$id": "candidate_123",
  "interviewers": {
    "$setKinds": ["Interview"],
    "date": "2026-04-11T09:00:00Z",
    "interviewer": {
      "$setKinds": ["Interviewer"],
      "name": "Dana",
      "department": "Platform"
    }
  }
}
#

Expressions

Use $js for inline JavaScript expressions or $ts for TypeScript (types are erased before execution; the explicit form is $code: { $lang, $body }). Works on create and update data-field writes (including upsert and scoped arc updates). Create sees sibling fields only; update sees the stored unit plus siblings, topo-ordered. Self-reference reads the pre-write stored value. Declare captures with "$var": "_:name"; read them in expressions with $var.name or $var["name"]. Circular sibling dependencies are rejected. Hooks see the authored expression, not the evaluated value. Test absence with isNull(x); DECIMAL locals are objects — use decimal().

An expression that returns null clears its target, just like a literal null. Runtime failures fail the mutation by default; on updates, onExprError: "skip" leaves only the failing field unwritten and reports a warning. Code-shaped objects inside JSON-operation values or arrays are literal JSON, not executable expressions.

BQL · create · topological evaluation (tax depends on subtotal, total depends on both)
{
  "$setKinds": ["Invoice"],
  "subtotal": 100,
  "tax": { "$js": "subtotal * 0.21" },
  "total": { "$js": "subtotal + tax" }
}
BQL · update · stored fields + self-increment
{
  "$id": "line1",
  "total": { "$js": "price * qty" },
  "count": { "$js": "count + 1" }
}
BQL · string transforms and slugs
{
  "$setKinds": ["Post"],
  "title": "Hello World",
  "slug": { "$js": "title.toLowerCase().replaceAll(' ', '-')" }
}
BQL · typescript expression · types erased before execution
{
  "$setKinds": ["Invoice"],
  "subtotal": 100,
  "total": { "$ts": "const rate: number = 0.21; subtotal * (1 + rate)" }
}
BQL · full workflow · query + $var + $js
// Query an existing user, then create a post using their data
[
  { "$op": "query", "$var": "_:author", "$kinds": "User", "$id": { "User": "user1" } },
  {
    "$setKinds": ["Post"],
    "title": "Hello World",
    "authorName": { "$js": "$var.author.name" },
    "boost": { "$js": "$var.author.karma * 0.1" },
    "greeting": { "$js": "`Hello ${$var.author.name}!`" }
  }
]

$js also works in queries as virtual fields, computed values that exist only in the response, prefixed with %.

BQL · virtual fields in queries
{
  "$kinds": "Order",
  "$fields": [
    "name", "price", "quantity",
    { "%line_total": { "$js": "price * quantity" } },
    { "%tax_label": { "$js": "'Tax: ' + (price * 0.21).toFixed(2)" } }
  ]
}
Typed literals

Force a JSON value into a specific type at write time. The canonical form is the object key with a # prefix — the $ namespace is reserved for BQL operators and selectors.

BQL · canonical #-prefixed object form
{
  "$setKinds": ["Invoice"],
  "created":  { "#datetime": "2024-01-15T10:30:00Z" },
  "dueDate":  { "#date":     "2024-02-15" },
  "openAt":   { "#time":     "09:00:00.000000" },
  "price":    { "#decimal":  "19.99" },
  "eta":      { "#duration": "1h30m" },
  "active":   { "#boolean":  "true" },
  "count":    { "#integer":  "42" },
  "ratio":    { "#float":    "3.14" },
  "owner":    { "#unit":     "01J..." },
  "rawText":  { "#text":     ".name" }
}

#datetime requires a timezone (Z or +02:00). #decimal preserves exact base-10. #duration combines w/d/h/m/s/ms/u and supports negatives. #text escapes a string that would otherwise be parsed as DX sugar (e.g. ".name" inside a native expression). Write casts as objects such as { "#datetime": "2024-01-15T10:30:00Z" }.

Interval values

An INTERVAL field stores a set of ranges. Probe with $has (point membership), $all (interval-set superset), and $any (any overlap). $contains is TEXT substring only.

BQL · interval write + queries
// Write: a Schedule with two ranges. Compact notation is the
// shortest spelling; a member array of nested tuples also works.
{
  "$setKinds": ["Schedule"],
  "officeHours": { "#intervals": "[09:00:00.000000,13:00:00.000000) U [14:00:00.000000,18:00:00.000000)" }
}

// Read: results never echo the "#intervals" cast. The shape follows the
// field's intervalFormat — "tuple" here, so bounds render as pairs:
// { "officeHours": [["09:00:00.000000","13:00:00.000000"], ["14:00:00.000000","18:00:00.000000"]] }

// Schedules whose office hours contain 10:30
{
  "$kinds": "Schedule",
  "$filter": { "officeHours": { "$has": { "#time": "10:30:00.000000" } } }
}

// Schedules whose office hours overlap a meeting window
{
  "$kinds": "Schedule",
  "$filter": {
    "officeHours": {
      "$any": {
        "#intervals": [[{ "#time": "12:30:00.000000" }, { "#time": "15:30:00.000000" }]]
      }
    }
  }
}
Native expressions

$expr is a checked fast lane for selected hot-path functions, named @namespace.fn. Inside an @-call, a string starting with . is DX sugar for a field reference; wrap in #text to keep the literal string. $js remains the broad surface for anything not covered yet.

BQL · native @text.concat virtual field
{
  "$kinds": "User",
  "$fields": [
    "name",
    { "%fullName": { "$expr": { "@text.concat": [".name", " ", ".familyName"] } } }
  ]
}

Responses

All responses follow a consistent structure. The shape of data is predictable based on your query.

#

Response Shape

The data field is an object or null when the selector is provably single: an exact scalar kind-claim map, scalar $iid, or an equality filter on a unique ONE data field (e.g. $filter: { "email": "x" } where email is unique: true). Null equality on a unique field stays MANY. Scalar public $id queries return MANY. Queries driven by $kinds alone, non-unique filters, $id.$in, root $or, or $in return a nonempty array or null, even if the filter matches one unit. $limit: 1 does not change response shape.

BQL · ONE · scalar claim
{
  "data": {
    "$id": { "User": "abc123" },
    "$kinds": ["User"],
    "name": "Alice",
    "email": "[email protected]"
  },
  "outcome": "completed"
}
BQL · MANY · by $kinds
{
  "data": [
    { "$id": { "User": "abc123" }, "$kinds": ["User"], "name": "Alice" },
    { "$id": { "User": "def456" }, "$kinds": ["User"], "name": "Bob" }
  ],
  "outcome": "completed",
  "meta": { "count": 2, "timingMs": 1.23 }
}

Set opts.explain in query or mutation execution options to include a plan in the response. "basic" returns the step list and the units each step outputs (unitsOut); "full" adds the units each step reads (unitsIn) and per-step timing. Useful for tuning.

BQL · opts.explain · plan + timing in the response
{ "query": { "$kinds": "User", "$filter": { "status": "active" } }, "opts": { "explain": "full" } }

// Response shape
{
  "data": [ ... ],
  "outcome": "completed",
  "meta": { "count": 10, "timingMs": 1.8 },
  "explain": {
    "steps": [
      { "operation": "kindScan", "detail": { ... }, "unitsIn": 25000, "unitsOut": 10, "durationUs": 1200 }
    ],
    "totalDurationUs": 1800,
    "planner": { ... }
  }
}
#

Metadata

Units include $id, $iid, and $kinds by default, subject to visibility permissions. $meta is only valid inside $fields. $meta is not a root query key; include it there to get all always-on meta fields. Contextual meta ($score, $history) must be requested explicitly. Timestamp fields use the canonical public keys $createdAt and $updatedAt; both serialize as ISO strings.

$id
Public identity claims keyed by leaf kind; {} when no claim is public
$iid
Internal ULID (22-char base62)
$kinds
All kind names, sorted; an empty list renders null. Omitted when permissions mask `$kinds`.
$version
Mutation counter
$createdAt
ISO-8601 creation timestamp
$updatedAt
ISO-8601 last mutation timestamp
$meta
Shorthand: expands to all of the above
$score
BM25 relevance score, only with $search (request explicitly)
$history
Per-unit event stream (inline via $fields; "$history" = IDs, { $expand: "$history" } = entries)
#

Outcomes and Issues

outcome is the operation state. Completed, committed, and partial outcomes use 200; accepted work uses 202; rejected work normally uses 422 (or 429 for backpressure).

  • issues is one flat, severity-sorted diagnostic list
  • severity is error, warning, or info
  • phase is request, execution, commit, or postCommit

A query whose $filter matched zero units (and that was not an identity / $offset lookup) succeeds with data: null and a warning explaining the empty result. Authored empty arrays, including $in: [], reject. Every issue has a stable code; issue-specific context stays in details.

BQL · empty-result warning (HTTP 200)
{
  "data": null,
  "outcome": "completed",
  "issues": [
    { "code": "FILTER_MATCHED_NOTHING", "severity": "warning", "phase": "execution", "field": "$filter", "message": "filter matched 0 units" }
  ],
  "meta": { "count": 0, "timingMs": 0.21, "issues": { "total": 1, "returned": 1, "omitted": 0, "errors": 0, "warnings": 1, "info": 0 } }
}
BQL · rejected response (HTTP 422)
{
  "data": null,
  "outcome": "rejected",
  "issues": [
    { "code": "UNKNOWN_KIND", "severity": "error", "phase": "execution", "message": "unknown kind 'Userr'. Did you mean 'User'?" }
  ],
  "meta": { "timingMs": 0.12 }
}

Branch on issue.code with the typed catalog exported by @blitzgraph/client-core (SERVER_ISSUE_CODES, with separate error and diagnostic partitions) — the catalog is generated from the server, so unknown codes mean a stale client.

UNKNOWN_KIND
Referenced a kind that is not defined (carries a typo suggestion)
BATCH_TOO_LARGE
Mutation batch exceeded maxBatchSize (split into smaller units.mutate calls, or create new units in chunks with units.import)
AMBIGUOUS_FIELD_ACROSS_KINDS
A written field resolves to conflicting definitions across the unit’s kinds
UNKNOWN_OPTS_KEY
Unknown option key; carries data: { unknown, allowed }
PERMISSION_DENIED
Authenticated token lacks the route permission
SIGN_IN_REQUIRED
Portal admission needs an app-user session
ACCESS_FORBIDDEN
Portal access rule denied the live subject
SECRETS_KEK_UNAVAILABLE
Secret ops require BLITZSTORE_SECRET_ENV_VAULT_KEYRING
NOT_IMPLEMENTED
Reserved options: parallel, limits.maxMemoryBytes, limits.maxRegexComplexity.
SUBSPACE_LIMIT_EXCEEDED
Creating the subspace would exceed the namespace subspace limit

By default, mutations accumulate all errors so you see every problem at once. Set failFast: true in mutation options to stop on the first error instead.

API Reference

Every HTTP route and every typed operation, generated from the API manifest in its one order. Summaries are the manifest text. docs.get operation/<id> returns the input schema and rules of one row.

#

Routes

Each route lists its method, path and id. A plane route such as units.query takes the { query | mutation, opts? } envelope; its $type and $op select an operation below.

definitions.query
POST /definitions/query
Read definitions: kinds, fields, apps, automations and library members, or lint automation candidates.
definitions.mutate
POST /definitions/mutate
Create, update or delete definitions in one atomic batch.
definitions.emailTemplate.preview
POST /definitions/email-templates/preview
Render a saved email template binding, or the built-in template, with sample values.
definitions.emailTemplate.previewDraft
POST /definitions/email-templates/preview-draft
Render an unsaved email template draft with sample values without writing it.
definitions.export
POST /definitions/export
Export the definitions of the selected subspace as one importable bundle.
definitions.import
POST /definitions/import
Import a definitions bundle into the selected subspace in one commit; the result lists each app's portal status and the imported member counts.
definitions.asset.blob.presign
POST /definitions/assets/blob/presign
Start a Library image or font upload session and return where to send the bytes.
definitions.asset.blob.finalize
POST /definitions/assets/blob/finalize
Verify a Library upload and return the sealed marker for an asset create or update.
definitions.asset.blob.abort
POST /definitions/assets/blob/abort
Cancel a Library upload session and discard its staged bytes.
definitions.asset.blob.upload
PUT /definitions/assets/blob/upload/{blobId}
Receive the raw bytes of a local Library upload session.
definitions.asset.bytes
POST /definitions/assets/bytes
Read the bytes of one Library asset with authenticated, uncached access.
units.query
POST /units/query
Read units with BQL: filters, fields, arcs, sorting and pagination.
units.mutate
POST /units/mutate
Create, update, link or delete units with BQL in one atomic batch.
units.live
POST /units/live
Stream the results of a BQL query and their later changes as server-sent events.
units.export
POST /units/export
Export the units of the selected subspace as JSON or as a stream of chunks.
units.import
POST /units/import
Create units in the selected subspace from inline arrays or an export; import never updates an existing unit and is not idempotent.
units.blob.presign
POST /units/blob/presign
Start a file upload session and return where to send the bytes.
units.blob.finalize
POST /units/blob/finalize
Finish an upload session and return the file marker to store on a unit.
units.blob.abort
POST /units/blob/abort
Cancel an upload session and discard its staged bytes.
units.blob.upload
PUT /units/blob/upload/{blobId}
Receive the raw bytes of a local upload session.
units.operations.query
POST /units/operations/query
Run a declared query operation of an app with its portal identity.
units.operations.mutate
POST /units/operations/mutate
Run a declared mutation operation of an app with its portal identity.
installations.query
POST /installations/query
Read app installations: status, resolution, access explanations and diagnostics.
installations.mutate
POST /installations/mutate
Change app installations: delivery receipts, identities, invites and enrollments.
runs.query
POST /runs/query
Read workflow tooling, run records, run events, workflow triggers and transfers.
runs.mutate
POST /runs/mutate
Start workflows and actions, signal or cancel runs, and manage transfers and triggers.
runs.agents.execute
POST /runs/agents/execute
Run one agent turn and stream its events as server-sent events.
runs.agents.model
POST /runs/agents/model
Stream one model completion for an agent turn with a reserved inference budget.
runs.transfer.exportStream
POST /runs/transfer/export-stream
Stream a sealed recovery export of the namespace as one binary bundle.
runs.stream
GET /runs/{iid}/stream
Stream the events of one run as server-sent events until it ends.
runs.artifact.upload
POST /runs/artifacts
Upload a transfer bundle and store it as an artifact for a later import.
runs.artifact.download
GET /runs/artifacts/{artifactId}
Download a stored transfer artifact as one binary bundle.
resources.query
POST /resources/query
Read resources: databases, namespaces, subspaces, configuration, history, env values, modules and agent setups.
resources.mutate
POST /resources/mutate
Change resources: databases, namespaces, subspaces, configuration, env values, modules and agent setups.
resources.module.upload
POST /resources/modules/{name}
Upload a WebAssembly code module under a name for actions to call.
resources.module.download
GET /resources/modules/{name}
Download the bytes of a stored WebAssembly code module.
server.query
POST /server/query
Read server-wide state: KV tables, metrics, host bindings and keys.
server.mutate
POST /server/mutate
Change server-wide state: grants, limits, keys, usage watermarks and blob quarantine.
docs.overview
GET /docs/overview
Read the agent primer: planes, concepts, operators and how to start.
docs.get
GET /docs/{section}/{name}
Read one docs page by section and name: a concept, operator, example, plane, operation, term, schema or error page.
docs.list
GET /docs/list
List the docs pages, optionally of one section or one plane.
docs.search
POST /docs/search
Search the docs pages by text; error pages are read by name only.
docs.batch
POST /docs/batch
Read several docs pages in one request; a missing page is a warning issue.
docs.check
POST /docs/check
Check a value against a named schema root with the acceptor the server runs for it.
public.root
GET /
Redirect a browser to Studio when the server hosts it.
public.whoami
GET /whoami
Describe the credential that authenticated this request.
public.info
GET /info
Describe the server build and its capabilities.
public.health
GET /health
Report whether the server is ready to serve requests.
public.health.live
GET /health/live
Report whether the server process is alive.
hooks.workflow
POST /hooks/{db}/{namespace}/{subspace}/{source}
Receive an external event for a workflow source and admit it as a run.
hooks.workflow.challenge
GET /hooks/{db}/{namespace}/{subspace}/{source}
Answer the verification challenge of an external event source.
#

Operations

Each operation id is <plane>.<type>.<op> and names the route that carries it. A selector, the read grammar and the permission floor are on its docs.get operation/<id> page.

definitions.kind.query
definitions.query
Read kind definitions; a selector narrows the list.
definitions.kind.create
definitions.mutate
Create a kind definition.
definitions.kind.update
definitions.mutate
Change the kind definitions that the selector addresses.
definitions.kind.delete
definitions.mutate
Delete kind definitions by `$did`, by `$filter`, or every kind with `$all`.
definitions.embedder.query
definitions.query
Read embedder definitions; a selector narrows the list.
definitions.embedder.create
definitions.mutate
Create an embedder definition.
definitions.embedder.update
definitions.mutate
Change the embedder definitions that the selector addresses.
definitions.embedder.delete
definitions.mutate
Delete the embedder definitions that the selector addresses.
definitions.field.query
definitions.query
Read data, role and link field definitions together; a selector narrows the list.
definitions.dataField.query
definitions.query
Read data field definitions; a selector narrows the list.
definitions.dataField.create
definitions.mutate
Create a data field definition.
definitions.dataField.update
definitions.mutate
Change the data field definitions that the selector addresses.
definitions.dataField.delete
definitions.mutate
Delete the data field definitions that the selector addresses.
definitions.roleField.query
definitions.query
Read role field definitions; a selector narrows the list.
definitions.roleField.create
definitions.mutate
Create a role field definition.
definitions.roleField.update
definitions.mutate
Change the role field definitions that the selector addresses.
definitions.roleField.delete
definitions.mutate
Delete the role field definitions that the selector addresses.
definitions.linkField.query
definitions.query
Read link field definitions; a selector narrows the list.
definitions.linkField.create
definitions.mutate
Create a link field definition.
definitions.linkField.update
definitions.mutate
Change the link field definitions that the selector addresses.
definitions.linkField.delete
definitions.mutate
Delete the link field definitions that the selector addresses.
definitions.vectorField.query
definitions.query
Reserved: query a vector field definition; vector fields are not implemented yet.
definitions.vectorField.create
definitions.mutate
Reserved: create a vector field definition; vector fields are not implemented yet.
definitions.vectorField.update
definitions.mutate
Reserved: update a vector field definition; vector fields are not implemented yet.
definitions.vectorField.delete
definitions.mutate
Reserved: delete a vector field definition; vector fields are not implemented yet.
definitions.hook.query
definitions.query
Read schema hook definitions; a selector narrows the list.
definitions.hook.create
definitions.mutate
Create a schema hook definition.
definitions.hook.update
definitions.mutate
Change the schema hook definitions that the selector addresses.
definitions.hook.delete
definitions.mutate
Delete the schema hook definitions that the selector addresses.
definitions.action.query
definitions.query
Read action definitions; a selector narrows the list.
definitions.action.lint
definitions.query
Check unsaved action candidates together and return their diagnostics without writing them.
definitions.action.create
definitions.mutate
Create an action definition.
definitions.action.update
definitions.mutate
Change the action definitions that the selector addresses.
definitions.action.delete
definitions.mutate
Delete the action definitions that the selector addresses.
definitions.workflow.query
definitions.query
Read workflow definitions; a selector narrows the list.
definitions.workflow.lint
definitions.query
Check unsaved workflow candidates together as one batch and return their diagnostics, sharers and prerequisites without writing them.
definitions.workflow.create
definitions.mutate
Create a workflow definition.
definitions.workflow.update
definitions.mutate
Change the workflow definitions that the selector addresses.
definitions.workflow.delete
definitions.mutate
Delete the workflow definitions that the selector addresses.
definitions.app.query
definitions.query
Read app definitions; a selector narrows the list.
definitions.app.create
definitions.mutate
Create an app definition.
definitions.app.update
definitions.mutate
Change the app definitions that the selector addresses.
definitions.app.delete
definitions.mutate
Delete the app definitions that the selector addresses.
definitions.component.query
definitions.query
Read component definitions; a selector narrows the list.
definitions.component.create
definitions.mutate
Create a component definition.
definitions.component.update
definitions.mutate
Change the component definitions that the selector addresses.
definitions.component.delete
definitions.mutate
Delete the component definitions that the selector addresses.
definitions.emailTemplate.query
definitions.query
Read email template definitions; a selector narrows the list.
definitions.emailTemplate.create
definitions.mutate
Create an email template definition.
definitions.emailTemplate.update
definitions.mutate
Change the email template definitions that the selector addresses.
definitions.emailTemplate.delete
definitions.mutate
Delete the email template definitions that the selector addresses.
definitions.page.query
definitions.query
Read the page that an app declares at one path.
definitions.layout.query
definitions.query
Read the layout that an app declares at one path.
definitions.queryOp.query
definitions.query
Read query operation definitions; a selector narrows the list.
definitions.queryOp.create
definitions.mutate
Create a query operation definition.
definitions.queryOp.update
definitions.mutate
Change the query operation definitions that the selector addresses.
definitions.queryOp.delete
definitions.mutate
Delete the query operation definitions that the selector addresses.
definitions.mutationOp.query
definitions.query
Read mutation operation definitions; a selector narrows the list.
definitions.mutationOp.create
definitions.mutate
Create a mutation operation definition.
definitions.mutationOp.update
definitions.mutate
Change the mutation operation definitions that the selector addresses.
definitions.mutationOp.delete
definitions.mutate
Delete the mutation operation definitions that the selector addresses.
definitions.workflowOp.query
definitions.query
Read workflow operation definitions; a selector narrows the list.
definitions.workflowOp.create
definitions.mutate
Create a workflow operation definition.
definitions.workflowOp.update
definitions.mutate
Change the workflow operation definitions that the selector addresses.
definitions.workflowOp.delete
definitions.mutate
Delete the workflow operation definitions that the selector addresses.
definitions.operationsModule.query
definitions.query
Read the compiled references of every app operation in the selected subspace.
definitions.designToken.query
definitions.query
Read design token definitions; a selector narrows the list.
definitions.designToken.create
definitions.mutate
Create a design token definition.
definitions.designToken.update
definitions.mutate
Change the design token definitions that the selector addresses.
definitions.designToken.delete
definitions.mutate
Delete the design token definitions that the selector addresses.
definitions.asset.query
definitions.query
Read Library image and font asset definitions; a selector narrows the list.
definitions.asset.create
definitions.mutate
Create a Library asset from a finalized Library upload marker.
definitions.asset.update
definitions.mutate
Change the Library asset definitions that the selector addresses.
definitions.asset.delete
definitions.mutate
Delete the Library asset definitions that the selector addresses; referenced assets need `allowBrokenReferences`.
installations.installation.query
installations.query
Read the installation status of the apps in the selected subspace; `$iid` or `appSlug` narrows the list.
installations.installation.listDiagnostics
installations.query
Read one page of the diagnostics an installed app recorded, newest last.
installations.installation.resolve
installations.query
Resolve the portal URL that serves an installed app, or why it is not served yet.
installations.installation.explain
installations.query
Explain the access decision of an installed app for one page path.
installations.installation.evaluateAccess
installations.query
Evaluate the access verdicts of every page of an installed app.
installations.installation.recompile
installations.mutate
Recompile the installations of the selected subspace from their saved definitions.
installations.identity.query
installations.query
Reserved: read the portal identities of an installation.
installations.identity.update
installations.mutate
Disable a portal identity or change its email address.
installations.invite.query
installations.query
Reserved: read the pending portal invitations of an installation.
installations.invite.create
installations.mutate
Invite a person to a portal and send the invitation email.
installations.enrollment.query
installations.query
Reserved: read the pending holder enrollments of an installation.
installations.enrollment.create
installations.mutate
Enroll a holder in a portal and send the enrollment email.
installations.delivery.reset
installations.mutate
Clear the stored email delivery receipt of an app so the next delivery check starts fresh.
installations.delivery.takeTest
installations.mutate
Take the newest captured test email of an app from the test delivery outbox.
runs.workflow.analyzeBlastRadius
runs.query
Assess the static effects a saved workflow can have, without running it.
runs.workflow.render
runs.query
Render a saved workflow as a state diagram and a step outline.
runs.workflow.simulate
runs.query
Simulate a saved workflow with mocked steps and report every transition, without writing anything.
runs.workflow.test
runs.query
Run the test cases of a saved workflow against mocked steps and report each outcome.
runs.workflow.execute
runs.mutate
Start a run of a saved workflow; the required idempotency key makes a retry return the same run.
runs.action.execute
runs.mutate
Run a saved action once with the given input and return its output and its first log lines; the idempotency key, sent in the body or the Idempotency-Key header, makes a retry return the same run.
runs.action.query
runs.query
Read the retained history of action runs by `$iid` or by action name, newest first unless `$sort` names another order.
runs.run.query
runs.query
Read run records, newest first unless `$sort` names another order; `$filter` and the request selectors narrow the list.
runs.run.count
runs.query
Count the run records that the same `$filter` and request selectors as the list select, by status.
runs.run.inspect
runs.query
Inspect one run record by `$iid`, with its output and the signal it waits for.
runs.run.listEvents
runs.query
Read one page of the events one run recorded, oldest first.
runs.run.traceCascade
runs.query
Read the tree of runs that one run started, directly or through its children.
runs.run.metrics
runs.query
Read the scheduler queue metrics of the database or server.
runs.run.summaries
runs.query
Read the daily run totals per type and status.
runs.run.signal
runs.mutate
Deliver a named signal to a waiting workflow run, by `$iid` or by workflow and correlation key.
runs.run.cancel
runs.mutate
Cancel one run that has not finished.
runs.run.retry
runs.mutate
Queue one failed run to run again.
runs.agentSession.query
runs.query
Read the status of one agent session.
runs.agentSession.create
runs.mutate
Reserved: start an agent session.
runs.agentSession.setModel
runs.mutate
Reserved: change the model of an agent session.
runs.agentSession.interrupt
runs.mutate
Interrupt the running turn of an agent session.
runs.agentSession.signal
runs.mutate
Return a tool result to an agent session that waits for it.
runs.workflowTrigger.query
runs.query
Read the trigger sources of a workflow with their counters and rejected events.
runs.workflowTrigger.restart
runs.mutate
Restart a stopped trigger source of a workflow.
runs.workflowTrigger.replay
runs.mutate
Replay one rejected trigger event of a workflow as a new run.
runs.workflowTrigger.discard
runs.mutate
Discard one rejected trigger event of a workflow without running it.
runs.transfer.query
runs.query
Read the transfer record of the namespace; `$iid` narrows it to one import.
runs.transfer.planExport
runs.query
Estimate what an export of the namespace would contain, without exporting it.
runs.transfer.planImport
runs.query
Check whether an import would start fresh or resume, without importing.
runs.transfer.export
runs.mutate
Export the namespace into a stored transfer artifact.
runs.transfer.import
runs.mutate
Start importing a transfer artifact into the namespace.
runs.transfer.cancel
runs.mutate
Reserved: cancel a namespace transfer in progress.
runs.artifact.list
runs.query
Read the stored transfer artifacts of the database.
runs.artifact.delete
runs.mutate
Delete one stored transfer artifact.
resources.database.query
resources.query
Read the databases of the server; `$rid` narrows the list.
resources.database.queryDefault
resources.query
Read the default database of the server.
resources.database.measureSize
resources.query
Measure the stored bytes of one database per namespace.
resources.database.create
resources.mutate
Create a database on the server.
resources.database.update
resources.mutate
Make one database the default database of the server.
resources.database.delete
resources.mutate
Delete one database and all its namespaces.
resources.namespace.query
resources.query
Read the namespaces of the database; `$rid` narrows the list.
resources.namespace.create
resources.mutate
Create a namespace in the database.
resources.namespace.update
resources.mutate
Make one namespace the database default.
resources.namespace.delete
resources.mutate
Delete one namespace and all its subspaces.
resources.subspace.query
resources.query
Read the subspaces of the namespace; `$rid` narrows the list.
resources.subspace.lifecycleReceipt
resources.query
Read the latest lifecycle receipt of one subspace; data is null when none exists.
resources.subspace.create
resources.mutate
Create a subspace in the namespace.
resources.subspace.declare
resources.mutate
Create the engine-owned system subspace when it is absent.
resources.subspace.update
resources.mutate
Rename one subspace or make it the default subspace.
resources.subspace.delete
resources.mutate
Delete one subspace and everything stored in it.
resources.subspace.wipe
resources.mutate
Delete the units, definitions, schema or portals of one subspace and keep the subspace.
resources.subspace.replace
resources.mutate
Delete the data of one subspace and replace its definitions in one lifecycle operation.
resources.config.query
resources.query
Read the effective configuration of the namespace and its overrides.
resources.config.resolveEffectiveConfig
resources.query
Read the effective configuration of the namespace with the limits it enforces.
resources.config.update
resources.mutate
Change configuration overrides of the namespace.
resources.config.reset
resources.mutate
Remove every configuration override of the namespace.
resources.history.query
resources.query
Read the commit history of a subspace, newest first.
resources.env.query
resources.query
Read the env constants and secret metadata of the namespace; `domain` and `name` narrow the list.
resources.env.declare
resources.mutate
Declare an env constant or secret when it is absent; an existing value is kept.
resources.env.set
resources.mutate
Create or replace the value of an env constant or secret.
resources.env.rotate
resources.mutate
Replace the configured value of an env constant or secret; a name without one answers `ENV_VALUE_NOT_FOUND`.
resources.env.delete
resources.mutate
Delete one env constant or secret.
resources.env.deleteDomain
resources.mutate
Delete every env constant and secret of one domain.
resources.env.rotateKek
resources.mutate
Re-encrypt every env secret of the namespace under a new key-encryption key.
resources.module.query
resources.query
Read the WebAssembly code modules of the namespace; `name` narrows the list.
resources.module.delete
resources.mutate
Delete one WebAssembly code module.
resources.agentSetup.query
resources.query
Read the agent setups of the namespace with their credential custody and bindings.
resources.agentSetup.listModels
resources.query
Read the models that the credential of one agent setup can use.
resources.agentSetup.loginStatus
resources.query
Read the login state of the credential of one agent setup. A poll can store a refreshed credential.
resources.agentSetup.set
resources.mutate
Create or replace one agent setup.
resources.agentSetup.delete
resources.mutate
Delete one agent setup.
resources.agentSetup.bind
resources.mutate
Bind one agent setup to an agent role.
resources.agentSetup.unbind
resources.mutate
Remove the role binding of one agent setup.
resources.agentSetup.seal
resources.mutate
Store a sealed credential for one agent setup.
resources.agentSetup.unseal
resources.mutate
Remove the sealed credential of one agent setup.
resources.agentSetup.startLogin
resources.mutate
Start a device login for the credential of one agent setup.
resources.agentSetup.cancelLogin
resources.mutate
Cancel the device login in progress of one agent setup.
resources.agentSetup.logout
resources.mutate
Sign out the credential of one agent setup.
resources.definition.publish
resources.mutate
Publish definitions that hold secrets, which requires both definitions and resources write.
server.kv.query
server.query
Read the registered KV tables of the server.
server.kv.listEntries
server.query
Read one page of the entries of one registered KV table, with redacted values.
server.metrics.query
server.query
Read the live subscription and portal projection metrics of the server.
server.host.resolve
server.query
Resolve the database, namespace, subspace and app that one portal id serves.
server.key.query
server.query
Read the internal keys of the server; `$rid` narrows the list.
server.key.set
server.mutate
Store an internal key under one id.
server.key.delete
server.mutate
Delete one internal key.
server.grant.query
server.query
Reserved: read the grants of the server.
server.grant.set
server.mutate
Create or replace one grant.
server.grant.revoke
server.mutate
Revoke one grant.
server.limit.query
server.query
Reserved: read the limit overrides of the server.
server.limit.set
server.mutate
Create, replace or clear one limit override.
server.usage.export
server.mutate
Export the usage records after a watermark.
server.usage.acknowledge
server.mutate
Acknowledge exported usage records up to a watermark.
server.blob.quarantine
server.mutate
Record the malware scan verdict of one stored blob.

Need more details? Join the community on Discord.