BAML Studio hosted architecture

Status: Proposal, 2026-09-09. Revised 2026-09-10 for the publisher design. Supersedes the 2026-09-01 scale-out proposal 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 bundle format rebuilt into query data twice). The hosted system is not built.

The profiler records every BAML call inside the customer's application and writes closed files into a local store. The publisher is the in-process background task that ships the profiler's closed files to the customer's S3 bucket. A meta segment is one immutable file in a stream's meta plane; it carries the records that start and end executions. A data segment is one immutable file in a stream's data plane; it carries the call tree deltas, spans, value references and error captures for the executions in flight. A value pack is one object holding many small values from one flush. A large value is a value at or above the large-value threshold, stored as its own content-addressed object. A function table is a content-addressed object that maps function identifiers to their definitions. S3 tells a queue when a meta segment or a value pack arrives. A projector reads the objects and checks them. It sends allowlisted public facts to Boundary's ClickHouse. It appends private values and their indexes to Lance datasets in the customer's bucket. Lance is a columnar storage format with versioned datasets and built-in indexes. Studio, the CLI and agents run plain SQL against one logical catalog. A query coordinator in the customer's cloud plans each query. It runs the public part in ClickHouse and the private part over Lance. It streams rows with one typed outcome.

Nothing in Boundary records individual uploads. S3 is the evidence, the queue carries the work, and everything else can be rebuilt from the bucket.

The public query language is portable read-only SQL with no BAML-specific dialect. DataFusion, an embeddable SQL engine, plans every query. baml query executes the same SQL from the CLI. The catalog hides the ClickHouse and Lance split. This proposal treats function names and revision IDs as public facts.1

Decisions

  1. Sensitive evidence stays in the customer's cloud. Customer S3 is the canonical evidence store.
  2. Boundary operates the multi-tenant control plane: control API, configuration metastore on PlanetScale, projection ingest API, resident query API and ClickHouse Cloud.
  3. ClickHouse stores only allowlisted, non-value public facts: execution and call identifiers, function names, revision IDs, status, timing and availability states such as args_state. The calls_v1 columns 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; the profiler never truncates. Token counts are not recorded today and would need a catalog version bump.
  4. Private values live only in the customer's bucket. Canonically they live in value packs and large-value objects under the evidence prefix. Derived copies live in rebuildable Lance datasets with private typed columns and n-gram indexes, including the values dataset (values_v1) keyed by CID.
  5. There are no per-upload records in Boundary. A successful S3 PUT is the end of the upload.
  6. S3 mirrors the profiler's local stream layout; the projector runs the existing stream reader over an object-store backend. Every object key is a pure function of scope, stream, plane and sequence, or of scope and CID.
  7. Work delivery is S3 event notifications into SQS Standard with a dead-letter queue. Only meta segments and value packs notify. Delivery is at least once. Projection is deterministic and duplicate-safe.
  8. The public query language is portable read-only SQL planned by DataFusion over one versioned logical catalog. Catalog v1 exposes six public relations, threads_v1, call_path_stats_v1, calls_v1, errors_v1, function_definitions_v1 and health_v1, plus the internal processes_v1, store_files_v1 and value_index_v1.
  9. Queries are classified as interactive (public-only, private, mixed) or background. Background jobs expose progress, cancellation and a downloadable result.
  10. Everything outside the bucket is disposable. ClickHouse facts, Lance datasets, indexes, the NVMe cache and queue messages can all be rebuilt or replayed from the segments, packs and CAS objects.

Topology

flowchart LR
  subgraph Runtime[Customer application]
    PROF["Profiler<br/>In-process native library"]
    UP["Publisher<br/>In-process background task"]
  end

  subgraph DataPlane[Customer cloud]
    GW["Gateway<br/>Alien Worker · AWS Lambda"]
    S3[("Evidence bucket<br/>segments, packs, CAS")]
    LANCE[("Lance datasets and indexes<br/>Same bucket, derived prefix")]
    Q[("Projection queue<br/>Alien Queue · SQS Standard + DLQ")]
    PROJ["Projector<br/>Alien Worker · SQS-triggered Lambda"]
    IDX["Index, compaction and sweep worker<br/>Scheduled container"]
    QUERY["Query coordinator<br/>Alien Container · EC2 with NVMe cache"]
  end

  subgraph ControlPlane[Boundary cloud]
    CTRL["Control API<br/>Boundary service"]
    META[("Configuration metastore<br/>PlanetScale")]
    INGEST["Projection ingest API<br/>Boundary service"]
    CQUERY["Resident query API<br/>Boundary service"]
    CH[("Public facts<br/>ClickHouse Cloud")]
  end

  UI["Studio, CLI or agent<br/>Client application"]

  PROF --> UP
  UP -->|authorize flush| GW
  UP -->|presigned create-only PUTs| S3
  S3 -->|meta and pack notifications| Q
  Q --> PROJ
  PROJ -->|read segments, packs, CAS by key| S3
  PROJ -->|public fact batches| INGEST
  INGEST --> CH
  PROJ -->|private rows and index data| LANCE
  IDX -->|index, compact, prune versions, sweep| LANCE
  UI -->|Boundary user token| QUERY
  QUERY -->|query snapshot| CTRL
  QUERY -->|resident subquery| CQUERY
  CQUERY --> CH
  QUERY -->|Lance reads through NVMe cache| LANCE
  CTRL --> META

Alien is the data-plane runtime. Alien Workers run on AWS Lambda. The query coordinator runs as an Alien Container on an EC2 instance with local NVMe, for example the m7gd or i4g class. At least one warm replica runs beside it. Customer services connect to Boundary over outbound HTTPS only. Boundary never connects into the customer cloud. Studio in the browser calls the query coordinator directly over TLS with a Boundary user token.

Data placement

The storage decisions page records the recommended bucket layout, object layout, value pack format, Lance layout and queue settings, with alternatives. The placement below holds for every option on that page.

DataLocationAuthority
Meta segmentsCustomer S3, evidence prefixCanonical evidence
Data segmentsCustomer S3, evidence prefixCanonical evidence
Value packsCustomer S3, evidence prefixCanonical evidence, private value bodies
Large valuesCustomer S3, evidence prefixCanonical evidence, private value bodies
Function tablesCustomer S3, evidence prefixCanonical evidence, source paths and spans
Values dataset (value bodies keyed by CID)Customer S3, derived prefix, LanceDerived, rebuildable from packs and large values
Lance datasets with private typed columns, and their scalar and n-gram indexesCustomer S3, derived prefixDerived, rebuildable from segments
Background job resultsCustomer S3, temporary prefixDisposable, expires by lifecycle rule
NVMe cache of Lance rangesQuery coordinator nodeDisposable cache
Queue messagesCustomer SQSReplaceable work delivery
Execution and call identifiers, function names, revision IDs, status, timing, availability statesBoundary ClickHouseRebuildable public projection
Tenants, projects, environments, tokens, catalog versions, projection schemas, active projection generationBoundary configuration metastoreConfiguration only, no per-object state

ClickHouse stores function names and revision IDs directly. Queries can filter, group and return them without a customer-local lookup. Source code, paths and exact values stay in the customer's bucket.

Three version numbers appear on this page.

VersionMeaningRecorded where
Projection generationThe ClickHouse rebuild epoch for one scope. A rebuild after loss writes a new generation.Configuration metastore
Catalog versionThe version of the logical relation schemas, such as threads_v1.Configuration metastore
Lance dataset versionThe snapshot of private data that a query binds. Every Lance commit creates one.The dataset in the derived prefix

ClickHouse must not contain:

Components

Customer application

ComponentResponsibility
ProfilerRecord every BAML call. Write closed meta segments, data segments, values and function tables into the local store under a disk budget. A file is either complete or absent.
PublisherRun as an in-process background task. Every flush, collect every complete file not yet shipped, compress each object, authorize the flush with the gateway, PUT the objects in order, advance the cursor, mark shipped values and release shipped segments. Keep files within the disk budget when the gateway is unreachable. Read its configuration from the gateway at start and every 5 minutes. The publisher guide describes it in full.

Customer data plane

ComponentResponsibility
GatewayValidate the publisher token, compute each object key from the authenticated scope, check S3 for each object, return a key-bound, size-bound, checksum-bound, create-only presigned PUT URL for each missing object or stored for each present one. Serve the publisher configuration. Serve erasure requests. May front the query coordinator.
Evidence bucketHold segments, value packs and CAS objects under the evidence prefix, Lance datasets and indexes under the derived prefix, and job results under the temporary prefix.
Projection queueReceive S3 object-created notifications for meta segments and value packs, redeliver after the visibility timeout, move exhausted messages to the dead-letter queue.
ProjectorOn a value pack notification, ingest the pack's entries into the values dataset keyed by CID. On a meta segment notification, decode it; for each RootEnded, walk back through earlier meta objects of the stream for RootStarted and EngineStarted, read the data segments in the execution's data range by key, read the function table by CID, resolve value CIDs from the values dataset or the CAS prefix, send public fact batches to Boundary, append private rows and index data to Lance, commit the new dataset version, delete the message. Report pending and leave the message when a value is not yet ingested.
Index, compaction and sweep workerUpdate Lance scalar and n-gram indexes over new fragments, merge small fragments, prune old dataset versions. Run the sweep that closes executions whose stream stopped without RootEnded as abandoned. Never on the upload path.
Query coordinatorAuthenticate the caller, plan SQL with DataFusion, run resident subqueries in Boundary, run private operations over Lance through a Foyer cache on NVMe, stream NDJSON with a typed outcome, run background jobs.

Boundary control plane

ComponentResponsibility
Control APITenants, projects, environments, deployments, cells, user tokens, publisher tokens, workload tokens, catalog versions, projection schema registry, active projection generation per scope.
Configuration metastoreSystem of record for the control API. PlanetScale. It holds no per-object state.
Projection ingest APIValidate scope and registered relation schema, accept Arrow batches, write them to ClickHouse, return only after the batch is visible.
Resident query APIReparse a resident subquery, inject tenant scope, apply budgets, execute it on ClickHouse, return Arrow.
ClickHouseServe public facts: filters, joins, grouping, ordering and rollups.

A resident subquery is the public part of a query plan. The coordinator sends it to Boundary, and Boundary runs it on ClickHouse. Foyer is the cache library the coordinator uses for Lance reads. It keeps hot ranges in memory and on local NVMe. No customer service receives direct metastore or ClickHouse credentials. The endpoint shapes are in the system contract page.

Ingest flow

  1. The profiler writes closed segment files and value files into the local store. A file is either complete or absent.
  2. Every flush interval, the publisher collects every complete file not yet shipped: meta segments, data segments, small values into one value pack, and large values and function tables as single objects.
  3. The publisher compresses each object with zstd and calls the gateway once per flush with the list of objects, their sizes and their checksums.
  4. The gateway validates the publisher token, computes each object key from the authenticated scope, checks S3 for each key, and answers per object: a create-only presigned PUT URL when the object is missing, or stored when it is already there with the same checksum.
  5. The publisher PUTs the function tables, large values and value packs first, then the data segments in sequence order, then the meta segments.
  6. After the meta segments are stored, the publisher advances its cursor, marks the shipped values, and releases the shipped segments to the local store. The flush is complete. Nothing waits for Boundary.
  7. S3 sends an object-created notification to the projection queue for each meta segment and each value pack. Data segments and CAS objects do not notify.
  8. A projector that receives a value pack notification ingests its entries into the values dataset in Lance, keyed by CID, so duplicates collapse.
  9. A projector that receives a meta segment notification decodes it. For each RootEnded record it reads that execution's data segments by key, its function table by CID, and its values from the values dataset or from the CAS prefix.
  10. The projector builds deterministic public fact batches and sends them to the Boundary projection ingest API, which writes them to ClickHouse and answers only when they are visible.
  11. The projector commits the execution's private rows as a new Lance dataset version and deletes the queue message. A failure at any step leaves the message to be redelivered; after the retry limit it moves to the dead-letter queue.
  12. In the background, the sweep closes executions whose stream stopped without RootEnded as abandoned, and the index and compaction worker updates indexes and merges small fragments.

Ingest flow, step by step

One flush travels from the profiler's local store to a queryable projection. Each step lights the components involved and shows the state of the local store, the queue and the queryable data.

Use Next step. ← →

Ingest pathCustomer services open every Boundary connection
Ingest path from the publisher to ClickHouse and Lance The publisher authorizes one flush with the gateway, which checks S3 per object and returns create-only presigned URLs, then PUTs function tables, large values, value packs, data segments and meta segments into the S3 evidence bucket in the customer cloud. S3 notifies an SQS queue with a dead-letter queue for meta segments and value packs. The projector ingests packs into the values dataset, reads data segments, tables and values by key, sends public fact batches to the Boundary projection ingest API, which writes ClickHouse, and commits private rows to Lance datasets in the same bucket. A background worker runs the sweep, updates indexes and compacts the Lance datasets. Customer application Customer cloud Boundary cloud authorize flush PUT objects sweep index + compact notify message read by key Lance commit Arrow batch insert Publisher profiler + local store Gateway token · keys · HEAD · sign Worker sweep · index · compact Evidence bucket segments · packs CAS · Lance Projection queue SQS + dead-letter queue Projector decode · project · commit SQS-triggered Lambda Projection ingest API answers when visible ClickHouse public facts

The upload order in step 5 is the ordering guarantee. A RootEnded record carries the sequence range of the data segments that hold its execution, and those data segments reference values and a function table by CID. Because tables, large values and packs land first, data segments next and meta segments last, a meta object never appears in the bucket before the objects its executions need. Only meta segments and value packs notify because those are the two objects with work behind them: a meta segment may carry RootEnded, and a pack carries values to ingest. Data segments and CAS objects are read on demand by key, so a notification for them would carry no work.

An execution's RootStarted and its engine's EngineStarted are in the same meta segment as its RootEnded or in an earlier one of the same stream. The projector walks back through earlier meta objects by key, in descending sequence, bounded to 2,000 meta objects. Every timestamp in a stream is StreamStarted.zero_unix_ns plus a nanosecond offset, and StreamStarted is record 1 of meta sequence 1, so the projector reads meta sequence 1 by key; only RootStarted and EngineStarted need the bounded backward walk. A walk that exhausts the bound without finding both records is treated like a sequence gap.

Object layout

text
evidence/v1/<tenant>/<project>/<environment>/
  streams/<process_euid>/meta/<seq>.bamlmeta.zst      notifies the projection queue
  streams/<process_euid>/data/<seq>.bamldata.zst      read by key
  streams/<process_euid>/values/<flush_ulid>.bamlpack.zst   notifies the projection queue
  cas/values/<2 hex>/<cid>.bamlvalue.zst              large values, gateway-checked, read by key
  cas/tables/<2 hex>/<cid>.bamltable.zst              function tables, gateway-checked, read by key
  quarantine/<original key>.quarantine                marker written by the projector
derived/v1/<scope>/<dataset>/                         Lance datasets, internal to the data plane
temporary/v1/<scope>/jobs/<job_id>/result.ndjson      background job results

<seq> is 20 decimal digits. <cid> is the 64-hex digest without the bamlv_1_ prefix. No date partition; retention uses object age. Every object is one zstd frame. A segment, large value or table object is the original file compressed. A value pack is a small header, an uncompressed canonical JSON manifest and the raw value files, compressed together. S3 verifies the SHA-256 of the compressed object on PUT. The CID identifies the decompressed body of a value or table.

What each object family feeds (the public and private split is this proposal's; the profiler has no classification):

FamilyClickHouse (public facts)Lance (private)Read when
Meta segmentsthreads_v1 root rows (status, timing, counts, health), health_v1, execution completenessnothingon notification, plus a bounded backward walk for RootStarted and EngineStarted
Data segmentscalls_v1 metadata columns, call_path_stats_v1 from merged CCT deltas, errors_v1 metadata, availability states, value CIDsvalue references per callby key, range [data_first_seq ..= data_last_seq] when a RootEnded arrives
Value packsnothingvalues dataset: cid, codec, body, locatoron notification
Large valuesnothingbody on demand, locatorby key from CID
Tablesfunction_definitions_v1 public columns: fqn, display_name, definition_key, kind, originsource_file, spans, file paths, package, namespaceby key from EngineStarted.function_table_cid

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.

Object and execution states

An object passes through up to four states. None of them is recorded in Boundary. The only publisher state is the local filesystem: a cursor per plane, and a marker next to each shipped value and table.

StateApplies toReached at stepMeaning
written locallyevery object kind1The file is complete in the local store. Its bytes and trailing checksum are fixed.
shippedevery object kind5 and 6The object exists in S3 with a verified checksum. After the flush's meta segments are stored, the publisher advances its cursor, marks values and tables, and releases segments.
notifiedmeta segments and value packs7A message for the object is in the projection queue or being processed. Data segments and CAS objects skip this state.
projectedmeta segments and value packs8 and 11A pack's entries are in the values dataset. A meta segment's RootEnded executions are projected. Data segments and CAS objects are consumed when the execution that references them is projected.

An execution has its own state, computed by the projector. It is pending when its RootEnded has been seen but the execution is not yet projected, for example because a referenced value has not been ingested; the message stays in the queue. It is projected when its public facts are visible in ClickHouse and the Lance version with its private rows is committed. It is abandoned when its stream stopped without RootEnded and the sweep closed it after the limit. 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. private_state is a per-row value the coordinator computes from the Lance version a query binds; rows of a projected execution report private_state = indexed once the coordinator binds that version, and private_state = pending before.

What replaced the commit ledger

The earlier proposals recorded every upload in Boundary and drove projection from that record. This proposal removes that record. S3 durability plus a checksum-verified, create-only PUT already gives the publisher a durable success signal. SQS with a dead-letter queue already gives at-least-once work delivery with a visible failure lane. The experiments also showed that upload metadata could not prove an upload was valid: in the experiment's bundle format, a valid checksum still arrived with a malformed manifest. Validation therefore belongs in the projector, which reads the bytes. The retired mechanisms and the conditions for reconsidering them are on the deferred decisions page.

Query flow

  1. Studio, the CLI or an agent obtains a short-lived Boundary user token scoped to tenant, project and environment.
  2. The client sends SQL to the query coordinator in the customer cloud.
  3. The coordinator validates the token and binds a snapshot: the active ClickHouse projection generation and catalog version from Boundary, and the latest committed Lance dataset version from the bucket.
  4. DataFusion parses and plans the SQL against the logical catalog and classifies the query: public-only, private, mixed, or background (by estimated cost or by caller request).
  5. Public operations go to the Boundary resident query API as a resident subquery. Boundary reparses it, injects scope and budgets, runs it on ClickHouse, returns Arrow rows. Public-only queries end here.
  6. Private operations run in the coordinator over Lance: typed column filters, n-gram text search, numeric ordering, grouping by private fields, and reading a call's values. Public rows prune the private scan (function, time window, status, availability).
  7. The coordinator joins, orders and limits, then streams NDJSON rows followed by exactly one queryOutcome record with the snapshot, generation, Lance version, rows streamed, values evaluated, unavailable-value reasons and any typed error.
  8. A background query 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 for download.

Job states are queued, running, succeeded, failed and cancelled. The endpoints are POST /v1/query for interactive queries, POST /v1/query-jobs to start a job, GET /v1/query-jobs/{job_id} for state and progress, DELETE /v1/query-jobs/{job_id} to cancel, and GET /v1/query-jobs/{job_id}/result for the stored result.

sequenceDiagram
  participant C as Studio, CLI or agent
  participant Q as Query coordinator
  participant B as Boundary
  participant L as Lance in customer S3
  C->>Q: POST /v1/query with SQL and user token
  Q->>B: query snapshot: generation and catalog version
  Q->>L: latest Lance dataset version
  Note over Q: DataFusion plans and classifies the query
  Q->>B: resident subquery for public operations
  B-->>Q: Arrow rows from ClickHouse
  Q->>L: private filters, search and ordering through the NVMe cache
  L-->>Q: private rows
  Q-->>C: NDJSON rows, then one queryOutcome

Public-only queries never read Lance. Private and mixed queries read Lance only within the set that public facts have pruned. The coordinator evaluates private operations before the final ORDER BY and LIMIT when pushing the limit down could change the result. Interactive queries carry a budget. A query whose estimated cost exceeds it is rejected with a typed error that suggests a background job.

Query classes and expectations

The numbers below are single-instance POC measurements over 15,000,004 synthetic calls, not p95 values or SLAs. Cold is one controlled run in which private data came from object storage with no disk-cache bytes. Warm is the worst of three runs in which private data came from the NVMe cache with no object-storage bytes. Targets are proposed, not promised.

ClassRuns inPOC queriesPOC measurementProposed target
Public-onlyClickHouse onlyOpen an execution, list calls, timing, recent failures, health chart146 to 170 msUnder 500 ms
Private: read a call's valuesClickHouse identity lookup, then LanceRead a call's values, retrieve regression evidence1,045 to 1,074 ms cold; 936 to 943 ms warmUnder 2 s cold, 1 s warm
Private and mixed: search, ordering, groupingPublic pruning, then LanceFind by input, search output text, compare revisions, sort by amount, custom dashboard1,331 to 1,606 ms cold; 959 to 1,055 ms warmUnder 2 to 5 s cold, 1 to 2 s warm
BackgroundPublic cohort discovery, then a Lance anti-joinFind missing notifications, an anti-join9,705 ms cold; 2,293 ms warmNever interactive; job with progress, cancel and download

The POC corpus reused 10,000 input fixtures and small outputs. It does not represent production value cardinality or payload sizes. Loss of the NVMe cache changes latency, not correctness.

Correctness rules

Failure behavior

FailureBehavior
Gateway unreachableThe publisher keeps its files within its disk budget and retries the flush. Shipped files are freed before unshipped ones; the oldest unshipped segments are never dropped before shipped ones.
PUT response lostThe publisher authorizes the next flush with the same objects. The gateway finds each object and answers stored.
Publisher crashes mid-flushThe cursor was not advanced. Everything from the cursor re-ships on restart. Duplicates in S3 are harmless because keys are deterministic and the gateway answers stored.
Local checksum failure on a segmentThe publisher skips and reports the file. Its plane blocks until the file validates or a 60 second timeout passes. The projector then sees a sequence gap and marks the affected executions index_corrupt.
Malformed segment or pack with a valid checksumThe projector sends the message straight to the dead-letter queue and writes a quarantine marker next to the object. The bytes are kept. Operators inspect and replay.
Value referenced but not yet ingestedThe projector reports the execution pending and leaves the message. It is retried until the pack lands and the values dataset holds the CID.
Stream stops without RootEndedThe sweep closes the execution as abandoned after the limit. Its public facts record that state.
Duplicate notification or duplicate projector runDeterministic IDs produce one fact set and one set of Lance rows.
Projector crashes mid-executionSQS redelivers after the visibility timeout. The retry completes with the same IDs.
Boundary or ClickHouse unavailableThe message stays in the queue and retries with backoff. The queue is the buffer. The customer application is not affected.
Two projectors commit a Lance version at onceFragments are already written. The commit retries against the newest version.
S3 notification lostThe replay tool finds a meta object whose root ids are missing from ClickHouse or Lance and enqueues it.
ClickHouse lostCreate a new projection generation and replay every meta object and pack with the replay tool.
Lance dataset or indexes lostRebuild by replay. Queries report private_state = pending until rows return.
NVMe node lostThe warm replica serves queries. Results are the same; latency is higher until the cache refills.
Private value missing or denied at query timeThe coordinator returns the rows it has and reports the reason in queryOutcome.

Replay tool

The replay tool is an operator command, not a running service. It lists the meta objects and value packs under the evidence prefix, or reads an S3 Inventory report. It decodes each meta object's RootEnded records and compares the root ids it finds with the root ids projected in ClickHouse and in the Lance datasets. For each pack, it checks that its CIDs are in the values dataset. For each missing item it enqueues a synthetic object-created message in the same shape S3 uses, packs before meta objects. Uses:

POC deployment

Customer AWS account:

Boundary account:

POC acceptance tests

  1. 1. Function names and revision IDs can be obfuscated or mapped customer-side later without moving exact values. ↩