BAML control-plane / data-plane contract

Status: Proposal, 2026-09-09. Revised 2026-09-10 for the publisher design. Supersedes the 2026-09-01 scale-out draft contract and the 2026-09-04 acceptance decision. It incorporates the query proof of concept (15 million synthetic calls, 13 queries) and the S3 storage experiment (2,328 execution roots, 12 upload comparisons, one accepted upload in the experiment's benchmark bundle format rebuilt into query data twice). The hosted system is not built.

Boundary services do not connect to the data plane. Data-plane workloads connect to Boundary over outbound HTTPS and send only registered non-value fields. Profiler and query clients connect to data-plane endpoints. Evidence, source material, arguments, outputs and errors stay in data-plane S3.

Scope

Customers keep evidence and exact values in their data plane. Boundary runs shared configuration and analytical services.

This applies to customer-hosted BYOC and Boundary-hosted Alien data planes.

text
ALLOWED

Profiler publisher ----------------------> Data-plane gateway
Studio browser, CLI or agent -----------> Data-plane query coordinator
Data-plane workloads -- outbound HTTPS -> Boundary workload API
S3 object-created event ---------------> Data-plane projection queue (SQS)

NOT ALLOWED

Boundary control-plane server -----X----> Data plane

Data sent to Boundary

Data kept in the data plane

Values stay in the data plane. Registered relation schemas define which non-value fields Boundary may process.

Contract scope

This document defines workflow behavior, state transitions, failure handling and ownership. The OpenAPI contract defines HTTP paths, methods, headers, bodies and status codes. Resolve conflicts before implementation.

The Storage decisions page records the recommended bucket layout, the mapping from profiler files to objects, the value pack format, the Lance layout and the queue settings, with alternatives. Its examples are design inputs. They do not add fields or endpoints to this contract. The publisher guide describes the publisher's algorithm and the profiler facts it relies on.

Identity and deployment model

tenant_id identifies a Boundary customer organization and is the root authorization boundary. A tenant owns projects, and a project owns environments.

PlanetScale is the configuration metastore. It is the system of record for this hierarchy, deployment assignments, tokens, catalog versions and the active projection generation. It holds no per-object and no per-flush state.

Surfaces and ownership

The contract has two HTTP surfaces and one queue message.

SurfaceCallerOwned byNetwork direction
Public data-plane APIPublisher, Studio, CLI or agentData planeClient to data plane
Workload APIGateway, projector or query coordinatorControl planeOutbound HTTPS from data plane to Boundary
S3 notification message for meta segments and value packsS3, or the replay toolData planeS3 to SQS to projector

The public data-plane API runs in a customer VPC or a Boundary-hosted Alien cell. Studio must reach it from the browser. A BYOC route may be public or use customer-network access such as a VPN or private-access proxy.

The control plane hosts the workload API. Data-plane workloads open every connection to it, and responses use the same connection.

The queue message is the standard S3 event notification JSON. The replay tool emits the same shape. The replay tool is an operator command that lists the meta objects and value packs under the evidence prefix, compares the root ids they carry with the projected root ids in ClickHouse and Lance, and enqueues a message for each object whose executions or values are missing.

Authentication

The contract defines three authentication contexts:

  1. A Boundary user token authorizes Studio, the CLI or an agent to call the query coordinator for one tenant, project and environment. Erasure requires an additional erasure permission on the token.
  2. A publisher token authorizes the publisher to call GET /v1/publisher/config and POST /v1/uploads/authorize at the gateway for one environment. It is issued by the control API and rotated by the customer. A 401 or 403 response means the token is invalid: the publisher stops shipping, keeps its files within the disk budget, and retries the config endpoint later.
  3. A data-plane workload identity uses mTLS and a short-lived workload JWT to call the Boundary workload API as a deployment and cell.

Every request carries a caller-generated X-Baml-Request-Id header. Mutations that create a job, an erasure or a projection erasure also carry an Idempotency-Key header. Repeating a request with the same key returns the same resource.

OpenAPI permission names are provisional and non-exhaustive. Define the final permission set and endpoint matrix during implementation.

Contract invariants

Endpoint directory

Public data-plane API

9
GET/v1/publisher/config

Read the publisher configuration

profiler publisherdata plane
PurposeRead the publisher configuration
Called whenThe publisher starts, or five minutes have passed since it last read the configuration.

Called by the publisher when it starts and every five minutes after that. The gateway validates the publisher token and returns the limits the publisher must apply: whether capture is enabled, the segment and CAS format versions the projector accepts, the flush interval, the object count per authorize call, the large-value threshold, the largest object, the pack entry limit, the shipped-value retention window and the presigned URL lifetime. The response is the same for every publisher in one environment and may be cached until the next refresh.

A 401 or 403 means the publisher token is invalid or does not cover the environment. The publisher stops shipping, keeps its files and retries this call later. A 429 or 503 means back off with jitter and keep the last configuration.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:evidence:upload
Security
  • PublisherToken
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields

No request parameters or body are defined.

Responses, errors & schema fields
StatusDescriptionContentHeaders
200Current publisher configuration for the authenticated environment.application/json · PublisherConfigCache-Control · string · —
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
429Concurrency or rate budget exceeded.application/json · ErrorResponseRetry-After · integer ·
minimum 0
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
capture_enabled boolean yes — When false the publisher ships nothing and keeps its files within the profiler's disk budget until a later configuration turns capture on.
supported_segment_versions array<integer> yes
minItems 1uniqueItems true
Segment header versions the projector accepts. A segment with another version is not shipped and is reported. The frozen value is 2.
supported_cas_versions array<integer> yes
minItems 1uniqueItems true
CAS frame format versions the projector accepts, for values and function tables. The frozen value is 1.
flush_interval_seconds integer yes
minimum 1
Seconds between publisher flushes. A flush also starts when pending bytes exceed pack_max_entry_bytes and on shutdown. The starting value is 5, and 1 on Lambda.
max_objects_per_authorize integer yes
minimum 1
Largest number of objects in one POST /v1/uploads/authorize request. A flush with more objects makes several calls.
large_value_threshold_bytes integer yes
minimum 1
Values at or above this size ship as their own CAS object under cas/values/. Smaller values travel in the flush's value pack.
max_object_bytes integer yes
minimum 1
Largest compressed object the gateway authorizes. A larger object is refused with 413 and reported by the publisher.
pack_max_entry_bytes integer yes
minimum 1
Largest total size of original value bytes in one value pack. A flush with more starts another pack.
shipped_value_retention_seconds integer yes
minimum 0
How long the publisher keeps a shipped value or table file, with its .shipped marker, before removing both. Disk pressure may shorten it.
upload_url_ttl_seconds integer yes
minimum 1
Lifetime of a presigned PUT URL from the moment it is issued. The publisher re-authorizes an object whose URL expired.
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
429 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/v1/uploads/authorize

Authorize the objects of one flush

profiler publisherdata plane
PurposeAuthorize the objects of one flush
Called whenThe publisher has collected and compressed the complete files of one flush and needs create-only S3 targets for them.

Called by the publisher once per flush, with the list of objects the flush wants to write: meta segments, data segments, value packs, large values and function tables, each with its size and the SHA-256 of its zstd-compressed bytes. The gateway validates the publisher token, computes every object key from the authenticated scope, the stream, the plane and the sequence, or from the scope and the CID, and checks S3 for each key. It answers per object, in request order. An absent object gets state upload_required with a create-only presigned PUT URL bound to that key, size and checksum. A present object with the same checksum gets state stored and no URL. A present object with a different checksum gets state conflict. The publisher then PUTs the function tables, large values and value packs first, then the data segments in sequence order, then the meta segments. A successful PUT is the end of the upload for that object.

Retry rules. Authorization is idempotent: a repeated request for the same objects returns fresh URLs for the same keys, or stored for objects that arrived in the meantime. A PUT that fails its create-only precondition means the object already exists; the publisher re-authorizes it and expects stored. A conflict on a segment or pack is a local bug; the publisher reports it and never overwrites. A conflict on a value or table is treated as stored, because the CID already proves the content.

Limits. A request carries at most max_objects_per_authorize objects from the publisher configuration; a larger flush makes several calls. An object larger than max_object_bytes, or a request with more objects than the limit, returns 413. A 409 is returned only when the authenticated scope cannot be mapped to an evidence prefix, for example a token whose environment is not provisioned for uploads; per-object mismatches are reported as state conflict inside a 200 response. A 401 or 403 means the publisher stops shipping and keeps its files. A 429 or 503 means back off with jitter.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:evidence:upload
Security
  • PublisherToken
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
application/jsonAuthorizeUploadRequestrequired
FieldTypeRequiredConstraintsDescription
process_euid ProcessEuid yes — Stream identifier. The 32 lowercase hex digits of the profiler process's euid. Every segment and pack key of the stream contains it.
flush_id FlushId yes — ULID assigned by the publisher to one flush. It names the flush's value pack objects and correlates the authorize call with its PUTs.
objects array<UploadObject> yes
minItems 1
Objects the flush wants to write, in any order. The response lists them in the same order.
Responses, errors & schema fields
StatusDescriptionContentHeaders
200One entry per requested object, in request order, with its key, its state and, when an upload is required, its presigned URL.application/json · AuthorizeUploadResponse—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
409Idempotency, digest or state conflict.application/json · ErrorResponse—
413Declared or streamed body exceeds its limit.application/json · ErrorResponse—
429Concurrency or rate budget exceeded.application/json · ErrorResponseRetry-After · integer ·
minimum 0
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
flush_id FlushId yes — ULID assigned by the publisher to one flush. It names the flush's value pack objects and correlates the authorize call with its PUTs.
objects array<AuthorizedObject> yes
minItems 1
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
409 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
413 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
429 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/v1/query

Execute SQL

Studio browser, CLI or agentdata plane
PurposeExecute SQL
Called whenStudio in the user's browser, the CLI or an agent submits an interactive query.

Called directly by Studio in the user's browser, the CLI or an agent. The query coordinator binds one snapshot: the active projection generation and catalog version from Boundary, and the latest Lance dataset version from the bucket. DataFusion plans the SQL against the logical catalog. Public operations run in Boundary as resident subqueries. Private operations run in the coordinator over Lance.

Each result row is written directly as one JSON object. The final NDJSON line is a query-outcome envelope. A stream ending without an outcome is incomplete. mode must be interactive here; use POST /v1/query-jobs for background work.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:query:read
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
application/jsonExecuteQueryRequestrequired
FieldTypeRequiredConstraintsDescription
sql string yes
minLength 1
Read-only SQL against the logical catalog.
parameters array<QueryParameter> yes —
catalog_version string yes
minLength 1
budget QueryBudget yes —
mode string no
default interactiveenum interactive, background
POST /v1/query accepts only interactive. POST /v1/query-jobs runs the query as background.
environment_id string no — Required when the user token allows more than one environment.
Responses, errors & schema fields
StatusDescriptionContentHeaders
200NDJSON stream. Zero or more direct row objects are followed by exactly one query-outcome envelope.application/x-ndjson · string—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
413Declared or streamed body exceeds its limit.application/json · ErrorResponse—
429Concurrency or rate budget exceeded.application/json · ErrorResponseRetry-After · integer ·
minimum 0
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
413 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
429 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/v1/query-jobs

Start a background query job

Studio browser, CLI or agentdata plane
PurposeStart a background query job
Called whenA caller submits a query with mode background, or an interactive query was too costly.

Same request as POST /v1/query with mode: background. The coordinator binds the snapshot, validates and plans the SQL, and returns a job ID immediately. The job runs the same plan, reports progress, can be cancelled, and writes its result to the temporary prefix in the bucket. When an interactive query is too costly, POST /v1/query returns a typed error whose remedy points the caller here.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:query:read
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Idempotency-Key header string yes
minLength 1maxLength 256
Stable identifier for the logical mutation.
Request & schema fields
application/jsonExecuteQueryRequestrequired
FieldTypeRequiredConstraintsDescription
sql string yes
minLength 1
Read-only SQL against the logical catalog.
parameters array<QueryParameter> yes —
catalog_version string yes
minLength 1
budget QueryBudget yes —
mode string no
default interactiveenum interactive, background
POST /v1/query accepts only interactive. POST /v1/query-jobs runs the query as background.
environment_id string no — Required when the user token allows more than one environment.
Responses, errors & schema fields
StatusDescriptionContentHeaders
202Job accepted in state queued.application/json · QueryJob—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
429Concurrency or rate budget exceeded.application/json · ErrorResponseRetry-After · integer ·
minimum 0
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
202 · application/json
FieldTypeRequiredConstraintsDescription
job_id string yes —
state string yes
enum queued, running, succeeded, failed, cancelled
progress QueryJobProgress yes —
rows_so_far integer yes
minimum 0
snapshot QueryExecutionSnapshot yes — The one generation, catalog version and Lance version bound for the whole query.
result_url string · uri no — Present when state is succeeded. Same as GET /v1/query-jobs/{job_id}/result.
error QueryOutcomeError no — Present when state is failed.
created_at string · date-time yes —
updated_at string · date-time yes —
expires_at string · date-time yes — When the job record and its stored result are removed.
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
429 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
GET/v1/query-jobs/{job_id}

Read a background query job

Studio browser, CLI or agentdata plane
PurposeRead a background query job
Called whenA caller polls a background job.

Returns the job state, progress, rows produced so far, the bound snapshot, and after success a result_url. Jobs and their results expire at expires_at.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:query:read
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
NameInTypeRequiredConstraintsDescription
job_id path string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Current job state.application/json · QueryJob—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
404Resource absent within the authorized scope.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
job_id string yes —
state string yes
enum queued, running, succeeded, failed, cancelled
progress QueryJobProgress yes —
rows_so_far integer yes
minimum 0
snapshot QueryExecutionSnapshot yes — The one generation, catalog version and Lance version bound for the whole query.
result_url string · uri no — Present when state is succeeded. Same as GET /v1/query-jobs/{job_id}/result.
error QueryOutcomeError no — Present when state is failed.
created_at string · date-time yes —
updated_at string · date-time yes —
expires_at string · date-time yes — When the job record and its stored result are removed.
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
404 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
DELETE/v1/query-jobs/{job_id}

Cancel a background query job

Studio browser, CLI or agentdata plane
PurposeCancel a background query job
Called whenA caller stops a background job.

Cancels a queued or running job. A job that already reached a final state is returned unchanged. Cancellation is idempotent.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:query:read
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
NameInTypeRequiredConstraintsDescription
job_id path string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Job state after the cancel request.application/json · QueryJob—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
404Resource absent within the authorized scope.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
job_id string yes —
state string yes
enum queued, running, succeeded, failed, cancelled
progress QueryJobProgress yes —
rows_so_far integer yes
minimum 0
snapshot QueryExecutionSnapshot yes — The one generation, catalog version and Lance version bound for the whole query.
result_url string · uri no — Present when state is succeeded. Same as GET /v1/query-jobs/{job_id}/result.
error QueryOutcomeError no — Present when state is failed.
created_at string · date-time yes —
updated_at string · date-time yes —
expires_at string · date-time yes — When the job record and its stored result are removed.
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
404 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
GET/v1/query-jobs/{job_id}/result

Download a background query result

Studio browser, CLI or agentdata plane
PurposeDownload a background query result
Called whenA background job has succeeded and the caller fetches its result.

Returns the stored result of a succeeded job. The result has the same shape as an interactive response: NDJSON rows followed by one query-outcome envelope. The coordinator either streams the stored file or redirects to a short-lived presigned GET URL for it. A job that has not succeeded returns 409.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:query:read
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
NameInTypeRequiredConstraintsDescription
job_id path string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Stored NDJSON result. Rows are followed by exactly one query-outcome envelope.application/x-ndjson · string—
303Redirect to a short-lived presigned URL for the stored result.—Location · string · uri · —
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
404Resource absent within the authorized scope.application/json · ErrorResponse—
409Idempotency, digest or state conflict.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
404 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
409 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/v1/erasures

Request evidence erasure

CLI or Studio with erasure permissiondata plane
PurposeRequest evidence erasure
Called whenAn operator asks to remove evidence for selected streams, executions or a time range.

Called by an operator with erasure permission. The gateway resolves the selector to streams and to the root IDs of the executions those streams hold, then runs the erasure in order. It deletes the stream's meta segments, data segments and value packs from S3 first, then the executions' facts from ClickHouse through POST /projection-erasures, then their rows from Lance. Erasure is per stream, because a data segment holds groups of many executions of the same process; a selected execution removes the whole stream that contains it. Large values and function tables in the CAS prefix are shared across streams and are removed only when no remaining stream in the scope references them; the reference count over remaining streams is an open implementation decision. Replay cannot restore an erased stream because its source is gone. Repeating a request with the same idempotency key returns the same erasure.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:evidence:erase
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Idempotency-Key header string yes
minLength 1maxLength 256
Stable identifier for the logical mutation.
Request & schema fields
application/jsonErasureRequestrequired
FieldTypeRequiredConstraintsDescription
process_euids array<ProcessEuid> no
minItems 1uniqueItems true
Selects whole streams by process euid.
execution_ids array<RootId> no
minItems 1uniqueItems true
Selects the streams that contain these executions. The whole stream is erased.
time_range TimeRange no — Selects every stream with an execution whose start or end falls in this range, resolved through the public facts.
Responses, errors & schema fields
StatusDescriptionContentHeaders
202Erasure accepted and queued.application/json · Erasure—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
409Idempotency, digest or state conflict.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
202 · application/json
FieldTypeRequiredConstraintsDescription
erasure_id string yes —
state string yes
enum queued, running, succeeded, failed
selector ErasureRequest yes — Exactly one selector must be set.
streams_matched integer yes
minimum 0
Streams the selector resolved to.
streams_deleted integer yes
minimum 0
Streams whose objects, facts and Lance rows are removed so far.
error QueryOutcomeError no — Present when state is failed.
created_at string · date-time yes —
updated_at string · date-time yes —
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
409 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
GET/v1/erasures/{erasure_id}

Read an erasure

CLI or Studio with erasure permissiondata plane
PurposeRead an erasure
Called whenAn operator polls an erasure.

Returns the erasure state and the number of streams matched and deleted so far.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:evidence:erase
Security
  • BoundaryUserJwt
Server
URLDescriptionVariables
https://{dataPlaneHost}{dataPlaneHost} default data-plane.example.com
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
NameInTypeRequiredConstraintsDescription
erasure_id path string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Current erasure state.application/json · Erasure—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
404Resource absent within the authorized scope.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
erasure_id string yes —
state string yes
enum queued, running, succeeded, failed
selector ErasureRequest yes — Exactly one selector must be set.
streams_matched integer yes
minimum 0
Streams the selector resolved to.
streams_deleted integer yes
minimum 0
Streams whose objects, facts and Lance rows are removed so far.
error QueryOutcomeError no — Present when state is failed.
created_at string · date-time yes —
updated_at string · date-time yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
404 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —

Boundary workload API

5
PUT/projection-batches/{batch_id}

Upload a projection batch

data-plane projectorcontrol plane
PurposeUpload a projection batch
Called whenThe projector has read one execution's RootEnded record and its data range and built one public fact batch.

Called by the projector with one deterministic Arrow batch of public facts for one execution, relation and partition. batch_id is derived from the execution's root_id, relation and partition, so a redelivered meta segment projects the same execution into the same batch IDs and the same rows. Boundary checks the scope, the registered relation schema and the content digest, writes the batch to ClickHouse and answers only after the rows are visible.

Idempotent within one generation. A retry with the same batch_id, generation and content_digest returns 200 without writing again. The same batch_id and generation with a different digest returns 409. A rebuild into a new generation reuses the same batch IDs. The Arrow schema must match the registered relation exactly. Value bodies, unregistered string columns and unknown fields are rejected.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:projection:write
Security
  • WorkloadJwt + WorkloadMutualTLS
Server
URLDescriptionVariables
https://api.boundaryml.com/studio-data/v1—
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Content-Encoding header string yes
const zstd
X-Baml-Root-Id header RootId yes — Root ID of the execution the batch was built from.
X-Baml-Project-Id header string yes —
X-Baml-Environment-Id header string yes —
X-Baml-Relation header string yes
minLength 1
Registered relation name, for example calls_v1.
X-Baml-Partition header integer yes
minimum 0
Partition index of this batch within the execution and relation, starting at 0.
X-Baml-Projection-Generation header string yes
minLength 1
Projection generation the batch belongs to. Must be the active generation for the scope.
X-Baml-Content-Digest header Digest yes — SHA-256 digest of the uncompressed Arrow IPC body.
Request & schema fields
NameInTypeRequiredConstraintsDescription
batch_id path string yes
minLength 1maxLength 256pattern ^[A-Za-z0-9._:-]+$
Deterministic batch ID derived from the execution's root ID, relation and partition.
application/vnd.apache.arrow.streamstring · binaryrequired
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Identical retry of a batch that is already visible.application/json · ProjectionBatchResponse—
201New projection batch written and visible.application/json · ProjectionBatchResponse—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
409Idempotency, digest or state conflict.application/json · ErrorResponse—
413Declared or streamed body exceeds its limit.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
batch_id string yes —
root_id RootId yes — Root ID of one execution, as recorded in its RootStarted and RootEnded records. It is the execution ID in the public catalog and the key for every deterministic projection ID.
state string yes
const visible
generation string yes —
row_count integer yes
minimum 0
content_digest Digest yes — SHA-256 digest with a sha256: prefix and 64 lowercase hex digits.
201 · application/json
FieldTypeRequiredConstraintsDescription
batch_id string yes —
root_id RootId yes — Root ID of one execution, as recorded in its RootStarted and RootEnded records. It is the execution ID in the public catalog and the key for every deterministic projection ID.
state string yes
const visible
generation string yes —
row_count integer yes
minimum 0
content_digest Digest yes — SHA-256 digest with a sha256: prefix and 64 lowercase hex digits.
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
409 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
413 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/projection-erasures

Delete projected facts for executions

data-plane gatewaycontrol plane
PurposeDelete projected facts for executions
Called whenAn erasure has removed the selected streams' objects from S3.

Called by the gateway during an erasure, after the streams' objects are deleted from S3. Boundary deletes every fact row whose root_id is listed, in the given generation and the authenticated scope, and answers after the delete is visible. Idempotent: executions with no remaining facts count as deleted.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:projection:erase
Security
  • WorkloadJwt + WorkloadMutualTLS
Server
URLDescriptionVariables
https://api.boundaryml.com/studio-data/v1—
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Idempotency-Key header string yes
minLength 1maxLength 256
Stable identifier for the logical mutation.
Request & schema fields
application/jsonProjectionErasureRequestrequired
FieldTypeRequiredConstraintsDescription
root_ids array<RootId> yes
minItems 1maxItems 1000uniqueItems true
Executions whose facts are deleted. The gateway sends the root IDs of every execution in the erased streams.
generation string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Facts deleted and the delete is visible.application/json · ProjectionErasureResponse—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
state string yes
const deleted
generation string yes —
root_ids array<RootId> yes —
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
GET/projection-schemas/{catalog_version}

Read registered projection schemas

data-plane projector or query coordinatorcontrol plane
PurposeRead registered projection schemas
Called whenA projector or coordinator starts, or the catalog version changes.

Returns the registered relation schemas for one catalog version. The projector produces batches that match these schemas exactly. The response is static per catalog version and may be cached.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:projection:read
Security
  • WorkloadJwt + WorkloadMutualTLS
Server
URLDescriptionVariables
https://api.boundaryml.com/studio-data/v1—
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
NameInTypeRequiredConstraintsDescription
catalog_version path string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Registered relation schemas for the catalog version.application/json · ProjectionSchemaSet—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
404Resource absent within the authorized scope.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
catalog_version string yes —
relations array<RelationSchema> yes
minItems 1
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
404 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/query-snapshots

Create a query snapshot

data-plane query coordinatorcontrol plane
PurposeCreate a query snapshot
Called whenA query starts and the coordinator has no cached snapshot for its scope.

Called by the query coordinator when a query starts. The snapshot binds the authenticated tenant, project and environment scope to one catalog version and the active projection generation. The call is cheap and the coordinator caches the result until expires_at. The Lance dataset version is bound by the coordinator from the bucket, not by Boundary.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:resident-query:execute
Security
  • WorkloadJwt + WorkloadMutualTLS
Server
URLDescriptionVariables
https://api.boundaryml.com/studio-data/v1—
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
application/jsonCreateQuerySnapshotRequestrequired
FieldTypeRequiredConstraintsDescription
scope TenantScope yes —
catalog_version string yes
minLength 1
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Snapshot bound to the active generation.application/json · QuerySnapshot—
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
200 · application/json
FieldTypeRequiredConstraintsDescription
snapshot_id string yes —
scope TenantScope yes —
catalog_version string yes —
generation string yes — Active projection generation for the scope.
expires_at string · date-time yes —
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
POST/resident-queries

Execute a ClickHouse-resident subquery

data-plane query coordinatorcontrol plane
PurposeExecute a ClickHouse-resident subquery
Called whenThe plan contains a ClickHouse-resident part.

Called by the query coordinator for the public part of a plan. This is not raw ClickHouse SQL. Boundary parses the subquery again, permits only resident relations and operators, injects the authenticated scope, applies the budget and compiles ClickHouse SQL. The response is an Arrow IPC stream of public facts. It never contains value bodies. The generation that served the query is returned in a response header and in the Arrow schema metadata.

Full OpenAPI operation
Authentication & headers
Permissionprovisional:resident-query:execute
Security
  • WorkloadJwt + WorkloadMutualTLS
Server
URLDescriptionVariables
https://api.boundaryml.com/studio-data/v1—
Headers
NameInTypeRequiredConstraintsDescription
X-Baml-Request-Id header string yes
minLength 1
Caller-generated request ID. UUIDv7 is recommended.
Request & schema fields
application/jsonResidentQueryRequestrequired
FieldTypeRequiredConstraintsDescription
snapshot_id string no —
generation string no —
catalog_version string no —
sql string yes
minLength 1
parameters array<QueryParameter> yes —
budget ResidentQueryBudget yes —
Responses, errors & schema fields
StatusDescriptionContentHeaders
200Arrow stream containing the resident result.application/vnd.apache.arrow.stream · string · binaryX-Baml-Snapshot-Id · string · —
X-Baml-Projection-Generation · string · —
400Invalid request.application/json · ErrorResponse—
401Missing or invalid identity.application/json · ErrorResponse—
403Valid identity without required scope.application/json · ErrorResponse—
413Declared or streamed body exceeds its limit.application/json · ErrorResponse—
429Concurrency or rate budget exceeded.application/json · ErrorResponseRetry-After · integer ·
minimum 0
503Retryable dependency failure.application/json · ErrorResponseRetry-After · integer ·
minimum 0
400 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
401 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
403 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
413 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
429 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —
503 · application/json
FieldTypeRequiredConstraintsDescription
error Error yes —

Queue messages

1
AWS SQS Standard with a dead-letter queues3ObjectCreated

Meta segment or value pack arrived in the evidence bucket

Amazon S3 event notifications or the replay tooldata-plane projector
PurposeMeta segment or value pack arrived in the evidence bucket
Called whenS3 stores a new meta segment or value pack, or the replay tool enqueues a missing one.

S3 sends this notification to the projection queue when an object with the suffix .bamlmeta.zst or .bamlpack.zst is created under the evidence prefix. For a value pack the projector ingests its entries into the values dataset in Lance, keyed by CID, so duplicates collapse. For a meta segment the projector decodes it and, for each RootEnded record, reads that execution's data segments by key, its function table by CID and its values from the values dataset or the CAS prefix, projects public facts to Boundary and private rows to Lance, then deletes the message. A value that is not yet ingested leaves the message for a retry. Delivery is at least once; a duplicate produces the same batch IDs and the same rows. A failed run leaves the message to be redelivered after the visibility timeout and, after the retry limit, moves it to the dead-letter queue. The replay tool emits a message of the same shape for meta segments and packs that are in the bucket but whose executions or values are not in ClickHouse or Lance.

Full message contract
transportAWS SQS Standard with a dead-letter queue
deliveryat-least-once
producerAmazon S3 event notifications or the replay tool
consumerdata-plane projector
interactionone-way
Payload & schema fields
PayloadS3ObjectCreatedNotification
FieldTypeRequiredConstraintsDescription
Records array<S3EventRecord> yes
minItems 1

Evidence ingestion

Storage model

Three systems answer three different questions:

SystemQuestion answered
Data-plane S3 evidence prefixDoes this file exist?
Projection queue (SQS)Which meta segments and packs still need processing?
Boundary ClickHouse and data-plane LanceWhat is queryable?

S3 is the canonical evidence store and mirrors the profiler's local stream layout. The queue carries work, not state. ClickHouse and Lance are derived and rebuildable. No system outside the bucket records that an individual upload happened.

Object kinds

A stream is one process's file sequence, identified by a 32-hex process euid. A segment is one immutable file in a stream plane, meta or data. The publisher ships five kinds of object. The kind value is the one used in the authorize request.

kindKey under evidence/v1/<tenant>/<project>/<environment>/IdentityNotifies the queue
meta_segmentstreams/<process_euid>/meta/<seq>.bamlmeta.zststream and sequenceyes
data_segmentstreams/<process_euid>/data/<seq>.bamldata.zststream and sequenceno, read by key
value_packstreams/<process_euid>/values/<flush_ulid>.bamlpack.zststream and flushyes
valuecas/values/<2 hex>/<cid>.bamlvalue.zstCID, codec 1no, read by key
tablecas/tables/<2 hex>/<cid>.bamltable.zstCID, codec 2no, read by key

<seq> is 20 decimal digits. <cid> is the 64-hex digest without the bamlv_1_ prefix. A value pack holds the small values of one flush. A large value is a value at or above the large-value threshold and is stored as its own object. A function table is a CAS object with codec 2. Every object, packs included, is one zstd frame of its original bytes.

Upload sequence

The step numbers match the ingest flow on the architecture page. A flush is one publisher cycle.

sequenceDiagram
  participant U as Publisher
  participant G as Gateway
  participant S3 as S3
  participant Q as SQS
  participant P as Projector
  participant B as Boundary ingest API
  participant CH as ClickHouse
  participant L as Lance

  U->>U: 1-2. Profiler closes files; publisher collects the flush
  U->>G: 3. POST /v1/uploads/authorize with every object
  G->>S3: 4. HEAD each key
  G-->>U: 4. Per object: create-only presigned PUT URL, or stored
  U->>S3: 5. PUT tables, large values, packs, then data, then meta
  S3-->>U: 5. Stored durably
  U->>U: 6. Advance cursor, mark values, release segments
  S3->>Q: 7. Object-created for each meta segment and pack
  Q->>P: 8. Deliver pack message
  P->>L: 8. Ingest entries into the values dataset by CID
  Q->>P: 9. Deliver meta message
  P->>S3: 9. Read data range, table and values by key
  P->>B: 10. PUT /projection-batches/{batch_id}
  B->>CH: 10. Insert facts
  B-->>P: 10. Batch visible
  P->>L: 11. Commit new dataset version
  P->>Q: 11. Delete message
  Note over P,L: 12. Sweep closes abandoned executions; index and compaction run

The publisher runs a flush every flush_interval_seconds, earlier when pending pack bytes exceed pack_max_entry_bytes, and on shutdown. Nothing waits for Boundary. The flush is complete when the meta segments are stored.

Publisher configuration

GET /v1/publisher/config is called at start and every five minutes, and the response is cacheable. It returns capture_enabled, supported_segment_versions ([2]), supported_cas_versions ([1]), flush_interval_seconds (5), max_objects_per_authorize (100), large_value_threshold_bytes (32 KiB), max_object_bytes (256 MiB), pack_max_entry_bytes (8 MiB), shipped_value_retention_seconds (3,600) and upload_url_ttl_seconds (900). The values in parentheses are starting values. Changing one changes publisher behavior without a runtime release. The OpenAPI contract describes every field.

Authorize request and response

One POST /v1/uploads/authorize per flush lists every object the flush wants to write, at most max_objects_per_authorize per call. The request carries process_euid, flush_id and objects. Every entry carries kind, size_bytes and checksum_sha256 of the zstd object. Segments add sequence, packs add entry_count, and values and tables add cid and codec.

The gateway validates the publisher token, computes each key from the authenticated scope and the entry, and checks S3 for each key. The response repeats flush_id and objects in the same order. Every entry carries kind, its sequence or cid, its key and a state:

stateMeaningPublisher action
upload_requiredNo object exists under the keyPUT to upload_url with every header in required_headers before expires_at
storedAn object with the same key and checksum existsTreat the object as shipped; no URL is returned
conflictAn object with the same key and a different checksum existsSegments and packs: report a bug and never overwrite. Values and tables: treat as stored, because the CID already proves the content

required_headers carries If-None-Match: *, x-amz-checksum-sha256 and Content-Length, so the URL is bound to that key, size and checksum. A 401 or 403 stops shipping. A 413 means one object exceeds max_object_bytes or the request lists more than max_objects_per_authorize objects; the publisher splits the request or drops the oversize object. A 429 or 503 means back off with jitter.

Object and execution states

Each object passes through four states per plane. None of them is recorded in Boundary.

StateMeaningApplies to
written locallyThe profiler closed the file. It is complete or absent, never partialall kinds
shippedThe object exists in S3 with a verified checksum, and the flush that carried it has completedall kinds
notifiedA message for the object is in the projection queue or being processedmeta segments and value packs
projectedThe executions the object closes are projected, or its values are in the values datasetmeta segments and value packs

An execution has three states, computed from the stream and the query stores. It is pending once its RootEnded has been seen but the execution is not yet projected, including while a value it references is not yet ingested. Before RootEnded arrives the execution is not known to the cloud at all; the catalog reports it as running only for local reads. It is projected when its public fact batches are visible in ClickHouse and its private rows are committed in Lance. It is abandoned when its stream stopped without RootEnded and the sweep closed it.

The coordinator computes private_state from the bound Lance version. A public fact whose private rows are not in the bound Lance version carries private_state = pending; once they are, it carries private_state = indexed. Background index and compaction work does not change object or execution state.

Idempotency

Upload retries:

Projection batches:

Projection contract

The projector receives an S3 notification, not an evidence body. The message names the bucket, object.key, object.size, object.eTag and object.versionId. The key suffix selects the handling.

On a value pack (.bamlpack.zst): the projector decompresses the pack, reads the manifest and ingests each entry into the values dataset in Lance, keyed by CID with codec, body and locator. Duplicates collapse. No public fact leaves the data plane.

On a meta segment (.bamlmeta.zst): the projector decodes the segment. For each RootEnded record it:

  1. reads meta sequence 1 of the stream by key, because StreamStarted is record 1 of meta sequence 1 and every timestamp in the stream is StreamStarted.zero_unix_ns plus a nanosecond offset; then walks back through earlier meta objects of the same stream, by key in descending sequence, to find the execution's RootStarted and the EngineStarted for its engine. Only those two records need the walk. The walk is bounded to 2,000 meta objects. A walk that exhausts the bound without finding both records is treated like a sequence gap and marks the execution index_corrupt;
  2. reads data segments [data_first_seq ..= data_last_seq] of the stream by key, the range carried by RootEnded, and skips groups that belong to other roots;
  3. reads the function table by the CID in EngineStarted.function_table_cid;
  4. resolves each value CID from the values dataset, else from the cas/values/ prefix by key, else reports the execution pending and leaves the message for a retry;
  5. verifies that every key lies under the evidence prefix of its own deployment scope, that every file's trailing checksum matches, that the segment versions are supported, and that the active projection generation from Boundary is the one it will write to. An S3 checksum alone does not qualify an object; the storage experiment showed that it cannot prove complete dependencies, which here are the data range, the table and the values;
  6. emits ClickHouse batches and Lance rows with deterministic IDs keyed by the root id, and deletes the message once every batch is visible and the Lance version is committed.

The projector produces two outputs per execution:

OutputContentDestination
Public fact batcheszstd Arrow IPC in the registered relation schema for the six public relations of catalog v1: threads_v1, call_path_stats_v1, calls_v1, errors_v1, function_definitions_v1 and health_v1 (the internal processes_v1, store_files_v1 and value_index_v1 are not public)PUT /projection-batches/{batch_id} on Boundary
Private fragmentsPrivate typed columns, value references, source paths and spans, and n-gram index dataLance datasets under the derived prefix in the bucket

The availability states are written as facts with the catalog's values. In calls_v1, args_state, output_state and error_state take exactly available, not_captured, not_applicable or lost:<reason>, with reasons such as value_too_large and queue_full; a value is whole or lost with a reason, never truncated. In threads_v1, status takes succeeded, failed, cancelled, panicked, abandoned, or running for a live stream; value_state takes none, partial or complete; and index_state is the meta-plane integrity state, complete, index_corrupt, root_started_lost or no_root_ended. program_id is random per engine today, so call_path_id values are not comparable across processes and function_definitions_v1 grows by one row set per engine start until the planned content-hash program id lands.

Each batch request carries its identity in headers:

HeaderContent
X-Baml-Root-IdRoot id of the execution the batch was built from
X-Baml-Project-Id and X-Baml-Environment-IdScope, checked against the workload identity
X-Baml-RelationRegistered relation name
X-Baml-PartitionPartition index within the execution and relation, from 0
X-Baml-Projection-GenerationActive generation for the scope
X-Baml-Content-DigestSHA-256 of the uncompressed Arrow body
Content-EncodingAlways zstd

Boundary accepts only the registered relation schema. The projector reads the schema for its catalog version from GET /projection-schemas/{catalog_version}. Value-bearing fields, source material, unknown fields and unapproved string columns are rejected.

The Lance commit is the last step. It appends the new fragments and commits a new dataset version. Two projectors committing at once produce a conflict. The loser retries the commit against the newest version. Fragments are never rewritten on retry.

Executions whose stream stops without RootEnded are closed by the sweep. The sweep is the scheduled job that lists the stream prefixes under the evidence prefix, finds the streams whose newest meta object is older than the silence limit, and marks their executions with a RootStarted and no matching RootEnded as abandoned with the facts it has. The sweep interval and the silence limit are open implementation decisions below. An interior sequence gap in a plane is corruption: the projector marks every execution whose data range crosses the gap index_corrupt.

An object that fails verification is quarantined. The projector sends the message straight to the dead-letter queue and writes a quarantine marker under quarantine/<original key>.quarantine. The object bytes are kept. Operators inspect and replay after a fix.

Failure handling

S3, SQS, ClickHouse and Lance do not share a transaction. Determinism and redelivery repair every incomplete transition.

FailureDurable stateRecovery
Gateway unreachableFiles in the local storePublisher retries with backoff; the disk budget applies and the oldest unshipped segments are never dropped before shipped ones
PUT failsObject absentPublisher retries that object; the plane cursor does not advance past a failed sequence
PUT response lostObject may exist in S3Re-authorize; stored confirms the upload, upload_required means PUT again
Crash mid-flushCursor not advancedEverything in the flush re-ships; every landed object returns stored
Disk pressureFiles in the local storeThe profiler stops recording at its budget; the publisher frees shipped files first
Publisher token rotatedFiles in the local store401; the publisher stops shipping, reloads config and retries with the new token
Local checksum failure on a fileFile skipped and reportedThe plane blocks until the file validates or a 60 s timeout passes; the projector then sees a gap and marks affected executions index_corrupt
Re-authorize finds a different checksum under a segment or pack keyOriginal object in S3Gateway returns conflict; publisher reports a local bug and never overwrites
Lost S3 notificationObject in S3, no messageThe replay tool enqueues a synthetic message
Value referenced but not yet ingestedMeta message in the queueThe execution is pending; the message is retried until the pack lands
Stream stops without RootEndedRootStarted shipped, no RootEndedThe sweep marks the execution abandoned after its limit
Malformed object with a valid checksumObject plus quarantine markerMessage goes to the dead-letter queue; operators inspect and replay
Boundary or ClickHouse unavailableMessage in the queueRetries with backoff after the visibility timeout; the queue is the buffer
Projector crashes mid-execution or loses a batch responseSome batches visible, no Lance commitRedelivery repeats the same batch_id and content_digest and appends the same rows
Lance commit conflictFragments written, version not committedRetry the commit against the newest version
Retry limit reachedMessage in the dead-letter queueFix the cause, then replay the dead-letter queue
ClickHouse or Lance data lostObjects in S3Replay every meta object and pack; ClickHouse is rebuilt into a new projection generation
Coordinator node or NVMe cache lostNothing lostWarm replica serves queries; results are the same, latency is higher

Query execution

Query sequence

sequenceDiagram
  participant U as Studio, CLI or agent
  participant Q as Query coordinator
  participant B as Boundary workload API
  participant CH as ClickHouse
  participant N as NVMe cache
  participant L as Lance datasets

  U->>Q: POST /v1/query with user token
  Note over Q,B: Coordinator initiates every HTTPS connection
  Q->>B: POST /query-snapshots
  B-->>Q: snapshot_id, catalog_version, generation, expires_at
  Q->>L: Read latest committed version
  Q->>Q: Plan with DataFusion, classify query
  Q->>B: POST /resident-queries
  B->>B: Reparse, inject scope, apply budget
  B->>CH: Execute resident subquery
  CH-->>B: Public rows
  B-->>Q: Arrow with generation in metadata
  Q->>N: Read private ranges through the cache
  N->>L: Fetch ranges missing from the cache
  Q-->>U: NDJSON rows then one queryOutcome

Studio requests originate in the user's browser. The coordinator must support TLS, Boundary user tokens and CORS for the Studio origin.

Snapshot semantics

The coordinator binds one snapshot per query. The snapshot holds the tenant, project and environment, the catalog version, the active ClickHouse projection generation, the latest committed Lance dataset version and an expiry time for the Boundary part. The snapshot_id, generation and expires_at come from POST /query-snapshots. That call is cheap and the coordinator caches its result until expires_at. The Lance version is read from the bucket.

All resident subqueries and all Lance reads for one query use the same snapshot. A public fact whose private rows are not in the bound Lance version is reported with private_state = pending, never as NULL or a non-match.

Resident subquery semantics

The coordinator sends only the public portion of the plan to POST /resident-queries. The request names either the snapshot_id or the generation with its catalog_version. Boundary reparses the SQL, permits registered relations and operations, injects authenticated tenant scope and applies query budgets. It does not run caller-provided ClickHouse SQL. The response is an Arrow IPC stream. It carries the generation in the X-Baml-Projection-Generation header and in the Arrow schema metadata. Private literals never appear in a resident subquery. A predicate such as args['invoice']['id'] = 'INV-1042' is evaluated in the coordinator.

Private execution over Lance

Private operations run in the coordinator over the bound Lance version: typed column filters, n-gram text search, numeric ordering, grouping by private fields and reading a call's values. A call's value is read from the values dataset by CID, or from the CAS prefix by key when it is a large value. Public rows prune the private scan by function, time window, status and availability state. Lance reads go through a Foyer cache on local NVMe. The cache is disposable. Losing it changes latency, not results.

Query classes and background jobs

The planner classifies each query. The numbers below are proof-of-concept measurements on one instance with 15 million synthetic calls. Cold is one controlled run with no disk cache. Warm is the worst of three runs from NVMe. They are targets, not SLAs.

ClassReadsTargetPOC measurement
Public-onlyClickHouse onlyUnder 500 ms146 to 170 ms
Private, read a call's valuesPublic identity lookup, then LanceUnder 2 s cold, 1 s warm1,074 ms cold, 943 ms warm
Private or mixed search and groupingPublic pruning, then LanceUnder 2 to 5 s cold, 1 to 2 s warm1,331 to 1,606 ms cold, 959 to 1,055 ms warm
BackgroundSame plan, no interactive limitNever interactiveAnti-join 9,705 ms cold, 2,293 ms warm

Every query request carries a budget with wall time, result rows, result bytes and private bytes read. A query becomes background in two ways. The caller sends mode: background to POST /v1/query-jobs, or the planner's cost estimate exceeds the interactive budget. In the second case POST /v1/query returns an outcome with a typed error whose remedy points the caller to POST /v1/query-jobs.

A background job is accepted with status 202 and a job in state queued. It runs the same plan under the same snapshot rules. GET /v1/query-jobs/{job_id} reports state, progress, rows_so_far, the snapshot and expires_at. Once succeeded it also reports result_url. Once failed it reports error. DELETE /v1/query-jobs/{job_id} cancels a queued or running job. A job in a final state is returned unchanged.

The job writes its result as NDJSON with a final queryOutcome to the temporary prefix in the bucket. GET /v1/query-jobs/{job_id}/result redirects to or streams that file. It returns 409 while the job has not succeeded. The job record and its result are removed at expires_at.

Streaming outcome

The public query response contains zero or more JSON row objects, one per NDJSON line, followed by exactly one {"queryOutcome": ...} record. Rows are not wrapped in a row property. The outcome carries:

FieldContent
queryCompletedWhether execution reached its natural end
resultStatecomplete, incomplete, failed, budget_exhausted or cancelled
snapshotcatalogVersion, generation and lanceVersion, plus snapshotId when known
rowsStreamedRows written before the outcome
valueEvaluationsPrivate values attempted, available and unavailable, with unavailable counts byReason
privateStateRows counted as indexed and as pending
errorTyped error with code, message, retryable and an optional remedy

An empty result is never produced by skipping unavailable evidence. If the stream ends before the outcome record, the result is incomplete. Once response bytes have been consumed, a retry starts a new public query and snapshot.

Erasure

POST /v1/erasures accepts exactly one selector: process_euids, execution_ids or a time_range. It returns an erasure with erasure_id and state. GET /v1/erasures/{erasure_id} reports the state, streams_matched and streams_deleted. The erasure unit is the stream, identified by its process euid, because a data segment holds groups of several executions and cannot be split. A selected execution erases the whole stream that contains it. The gateway runs the erasure in this order:

  1. Delete the stream's objects from S3, including prior versions: its meta segments, data segments and value packs.
  2. Call POST /projection-erasures with root_ids, the root ids of every execution in the erased streams, and generation. Boundary deletes the facts idempotently.
  3. Commit a new Lance version without the rows for those executions.

The stream is removed first, so a later replay cannot resurrect it. Erasure of execution_ids or a time_range resolves to process euids and root ids through the public facts before step 1. Large values and function tables are shared across streams and are removed only when no remaining stream in the scope references them. That needs a reference count over the remaining streams, which is an open implementation decision below.

State machines

Object state per plane, computed from the local store, S3, the queue and the query stores:

text
written locally -> shipped -> notified -> projected        meta segments and value packs
written locally -> shipped                                 data segments, large values, tables

Execution state:

text
pending -> projected
       \-> abandoned

Projector progress for one queue message. These are steps, not stored states:

text
received -> verified -> facts visible -> Lance version committed -> message deleted
                    \-> pending (message retried)
                    \-> quarantined (dead-letter queue)

Background job state:

text
queued -> running -> succeeded
                 \-> failed
                 \-> cancelled

Erasure state:

text
queued -> running -> succeeded
                 \-> failed

Data formats

InterfaceFormatReason
Control operationsHTTPS JSONSmall, inspectable metadata
Segment, large value and table uploadOne S3 object per file, one zstd frame of the original bytesOriginal bytes preserved; the profiler's reader runs unchanged over the bucket
Value packBAMLPCK1 header, uncompressed canonical JSON manifest, then the raw value files; the whole pack is one zstd frameMany small values in one PUT; one value readable alone by offset
Projection batchzstd-compressed Arrow IPC streamTyped, efficient columnar batches
Resident query responseArrow IPC streamTyped bulk transfer to the coordinator
Public query responseNDJSON streamIncremental rows plus an explicit final outcome
Projection workS3 event notification JSON on SQS, for the .bamlmeta.zst and .bamlpack.zst suffixesAt-least-once delivery with no custom message schema
Private dataLance datasets under the derived prefix, including the values dataset keyed by CIDVersioned, rebuildable, indexable columnar storage
Background resultNDJSON file with a final outcome under the temporary prefixSame reader as the interactive stream

Errors and retries

ConditionCaller behavior
Invalid request or unsupported versionDo not retry without changing the request
Authentication or authorization failureRefresh identity or correct scope before retrying; the publisher stops shipping and retries config later
Create-only PUT precondition failureRe-authorize that object; accept stored; report conflict on a segment or pack, never overwrite
Object exceeds max_object_bytesReport; the object is not shipped
Rate, budget or concurrency limitRetry after server guidance with exponential backoff and jitter, or submit a job
Dependency unavailableRetry idempotently with exponential backoff and jitter
Mutation timeout with unknown resultRetry with the same identifiers or read resource state
Query stream ends without an outcomeTreat the result as incomplete and start a new query

Logs contain identifiers and workflow states. They do not contain presigned URLs, SQL value literals, evidence bodies or private values.

Parallel implementation plan

Control plane

Data plane

Shared integration artifacts

Open implementation decisions

OpenAPI examples do not resolve these decisions. Storage-format choices such as the mapping from files to objects, the pack format, object keys and Lance fragment size are recorded on the Storage decisions page.

DecisionMust be resolved before
Final permissions and endpoint-to-permission matrixAuthentication integration
Publisher-token format, issuance and rotationGateway integration
Shipped-value retention window and its disk-pressure overridePublisher integration
Walk-back bound for RootStarted and EngineStartedProjector integration
Sweep interval and silence limit for closing executions as abandonedProjector integration
Reference count over remaining streams for erasing shared large values and function tablesErasure integration
Workload-token issuance and downscoping in shared Alien cellsMulti-tenant hosted integration
mTLS certificate provisioning and rotationControl-plane deployment
Registered Arrow schemas and compatibility rulesProjection integration
Queue visibility timeout, maximum receives and dead-letter retentionProjector load testing
Lance index types, compaction cadence and version retentionQuery performance testing
Interactive query budgets, job limits and result-file expiryQuery performance testing
Hosted S3 tenant isolation using buckets, prefixes or scoped rolesHosted multi-tenant launch
Final error-code registryClient SDK stabilization
Service limits and production SLOsProduction readiness review

Contract change process

  1. Semantic changes start in this document.
  2. Wire changes are made in the OpenAPI contract in the same change.
  3. Both teams review changes that affect their caller or implementation responsibilities.
  4. Generated types and shared fixtures are updated with the contract.
  5. Contract tests must pass before either implementation depends on the new version.

Breaking changes require a new HTTP, catalog, projection schema or message version, depending on the affected surface.

The 2026-09-09 revision moves the unimplemented OpenAPI draft from 0.2.0-poc to 0.3.0-proposal. It removes the upload commit and projection dispatch endpoints (/v1/uploads/{authorization_id}/commit, /evidence/commits, /evidence/commits/{commit_id}, /projection-dispatches/pending, /projection-dispatches/{commit_id} and /projections/commits/{commit_id}/complete) and adds query jobs, erasures and projection schemas. The retired mechanisms are explained on the Deferred decisions page. The 2026-09-10 revision keeps 0.3.0-proposal: POST /v1/uploads/authorize now lists every object of a flush and answers per object, GET /v1/publisher/config is added, and the old draft's per-upload identifier, profiler_schema_version, authorization_id and max_size_bytes fields are gone; time_range survives only as an erasure selector. Draft-generated clients must regenerate types. This is not a migration of a deployed API.

  1. 1. Function names and revision IDs are treated as public facts in this proposal. Obfuscating or mapping them customer-side remains a possible later change and would not alter the contract shape. ↩