[bldr]docs
gRPC API

Build controller

The contract between a node and the build controller it dispatches a build to.

External build controller service.

A build controller is a separate process that the node connects to via a Unix socket. The node calls GetInfo once at startup to learn the controller's name (which must match the workspace's build_controller: value), then calls Build once per generation (including once per watch-mode iteration).

Service builder.controller.v1.BuildController, 4 rpcs.

GetInfo(GetInfoRequest) -> GetInfoResponse

Return controller metadata so the node can register it under the right name.

Request: GetInfoRequest

message GetInfoRequest {
  // no fields
}

Response: GetInfoResponse

message GetInfoResponse {
  optional string name = 1;
  optional string version = 2;
  repeated string capabilities = 3;
}
Field
nameController name — must match the workspace's build_controller: value (e.g. "default" → build_controller: default).
versionInformational version string, e.g. "0.1.0".
capabilitiesOptional features this controller implements, as stable names (e.g. "build.targets"). The node reads them to decide what it may ask of a controller it did not ship with — a workspace pins its controller image independently of the daemon, so the two versions drift by design.

Build(BuildRequest) -> stream BuildControllerEvent

Execute one build generation. Streams events (progress, task-tree nodes, and a terminal output or error event) back to the node.

Request: BuildRequest

message BuildRequest {
  optional string input_cid = 1;
  repeated string args = 2;
  optional string owner_kind = 3;
  optional string owner_id = 4;
  repeated string node_capabilities = 5;
  optional map<string, string> option_overrides = 6;
}
Field
input_cidCID (string form) of the BuildInput DAG-CBOR block.
argsThe build's selector vector, composed by the node from the build's typed fields: &lt;member> --only &lt;item> for a single-item build, --build-member &lt;m> per selected member, --no-targets, --no-cache, and the targets spec as --targets-mode &lt;mode> + repeated --target-filter &lt;f>. A string list rather than typed fields because a workspace pins its controller image independently of the daemon: a vector an older image already parses survives the hop verbatim, where a new field it has not learned would be silently dropped. Nothing a user typed travels here.
owner_kindOwner (kind + id) that owns this build's invocations, so pods the controller spawns via pod.dag link back to the owning build/run/deployment and are cleaned up when it is removed. Empty when unset.
owner_id 
node_capabilitiesOptional features this node implements, as stable names (e.g. "pod.cache-volumes"). Build code reads them through Bldr.nodeCapabilities() and asks only for what the node understands.
option_overridesPer-build overrides of the workspace's options: map (bldr build -o key=value). Each value is text to parse as YAML, as bldr ws set does. The controller merges them over the BuildInput's options before the build code runs, so input.options is the effective map. A controller that understands this advertises the build.options capability; the node refuses to dispatch overrides to one that does not, rather than let them vanish.

Response: BuildControllerEvent

One event streamed from the controller back to the node during a build. The stream ends after exactly one terminal event (output or error).

message BuildControllerEvent {
  oneof event {
    ControllerTaskEvent task = 2;
    ControllerOutputEvent output = 3;
    ControllerErrorEvent error = 4;
    ControllerTargetsEvent targets = 5;
    ControllerDiagnosticsEvent diagnostics = 6;
  }
}
Field
taskA progress-tree node update (non-terminal, repeatable).
outputBuild succeeded — output_cid is the artifact (terminal).
errorBuild failed (terminal).
targetsWhat this workspace's build code declares it can produce (non-terminal, repeatable).
diagnosticsWhat the build's tooling said about the source: type errors, lints (non-terminal, repeatable).

StreamLanguageServerProtocol(stream ControllerLspMessage) -> stream ControllerLspMessage

Serve LSP for the workspace's build code — .bldr.ts and .bldr/**/*.ts.

Those files are TypeScript, but not TypeScript a general language server can make sense of: their imports resolve through the build runtime's own rules (bare member specifiers, .bldr/ as an implicit source root, a generated ambient module per member), and the types they are checked against are generated from this daemon's function registry. The controller is the only thing that knows all of that — it already type-checks these files on every build, with exactly the virtual filesystem and paths map a language service needs. So the node routes build-code LSP here rather than reimplementing the resolution.

Its own RPC rather than messages multiplexed into Build: an editor session is long-lived and has no build, while a Build stream belongs to one generation and ends with it. Sharing one would tie the lifetime of a developer's editor to the lifetime of a build.

Request: ControllerLspMessage

One message of an LSP session with the controller.

The first client message must be the header; every later one carries a JSON-RPC payload. The payload is deliberately just a string: the node is a conduit here, not a participant — it routes and rewrites paths, and giving it a typed view of LSP would mean teaching it the protocol twice.

message ControllerLspMessage {
  oneof message {
    ControllerLspHeader header = 1;
    string jsonrpc = 2;
  }
}
Field
headerSession header. Client's first message, exactly once.
jsonrpcA JSON-RPC request, response or notification.

Response: ControllerLspMessage

The same message as ControllerLspMessage.

ExecuteGraph(stream GraphNodeMessage) -> stream GraphControllerMessage

Declare one build generation as a graph and answer for its inputs.

The controller declares nodes; the node executes them. The controller's only other part is to say, for a node whose dependencies have resolved, what that node's input is. Scheduling, caching, node state and failure belong to the node (specs/concepts/build-graph.yml).

The node's first message is the start; the controller answers with the root graph. From then on the node sends one PrepareInput per node that has an input mapper and the controller answers each with one InputPrepared. The node ends the generation by closing its stream.

A controller that implements this advertises the build.graph capability; the node uses Build for one that does not.

Request: GraphNodeMessage

One message from the node to the controller during ExecuteGraph.

message GraphNodeMessage {
  oneof message {
    BuildRequest start = 1;
    PrepareInput prepare_input = 2;
  }
}
Field
startThe generation to declare. The node's first message, exactly once.
prepare_inputAsk for one node's input.

Response: GraphControllerMessage

One message from the controller to the node during ExecuteGraph.

message GraphControllerMessage {
  oneof message {
    ControllerGraph graph = 1;
    InputPrepared input_prepared = 2;
    ControllerDiagnosticsEvent diagnostics = 3;
    ControllerErrorEvent error = 4;
  }
}
Field
graphThe root graph. The controller's first graph message, exactly once.
input_preparedThe answer to one PrepareInput.
diagnosticsWhat the build's tooling said about the source (repeatable).
errorThe graph could not be declared. Terminal.

Types used above

ControllerTaskEvent

A snapshot of one node in the controller's progress tree. Consumers key by id; link children via parent; render state. Each event is a full snapshot of that node (idempotent to replay).

message ControllerTaskEvent {
  optional uint64 id = 1;
  optional string label = 3;
  optional ControllerTaskState state = 4;
  optional map<string, string> labels = 10;
  repeated uint64 waiting_for = 11;
  oneof _parent {
    uint64 parent = 2;
  }
  oneof _output {
    string output = 5;
  }
  oneof _progress_update {
    ControllerTaskProgress progress_update = 6;
  }
  oneof _log_stream_id {
    string log_stream_id = 7;
  }
  oneof _log_cid {
    string log_cid = 8;
  }
  oneof _pod_id {
    string pod_id = 9;
  }
  oneof _input {
    string input = 12;
  }
  oneof _wait {
    ControllerTaskWait wait = 13;
  }
}
Field
idUnique id within this build's task forest.
parentAbsent on root nodes; set to the parent's id for child nodes.
labelHuman-readable label for this node.
stateCurrent lifecycle state.
outputTerminal output value (result CID or error message), present when state is COMPLETED or FAILED.
progress_updateQuantitative progress, present when state is RUNNING.
log_stream_idA live log stream id capturing this node's command output, if it opened one (the daemon surfaces it so bldr logs follow &lt;id> / the UI can tail it). Set on the RUNNING re-emit once the stream opens.
log_cidThe sealed log stream's final Blob CID, carried on the terminal emit.
pod_idThe id of the pod this node ran, if any. Set on the RUNNING re-emit once the pod starts; the daemon surfaces it so the UI can link to the Pods page.
labelsFree-form key/value metadata for this node. language names a syntax for rendering the label (e.g. "sh" for a shell command), so a consumer need not guess from the text.
waiting_forIds of the tasks this one is waiting on, when queued. Lets a consumer say what a pending node is blocked behind instead of just "not started".
inputThe invocation's input CID, once one is attached to this node.
waitWhat the node is held on, present when state is WAITING or WAITING_FOR_RESOURCES.

ControllerTaskProgress

message ControllerTaskProgress {
  optional uint64 current = 1;
  optional uint64 total = 2;
}

ControllerTaskWait

Why a node is not making progress. Mirrors builder.TaskWaiting and builder.TaskWaitingForResources so the node's translation is a copy.

message ControllerTaskWait {
  optional string reason = 1;
  optional string resource = 2;
  optional uint64 needed = 3;
  optional uint64 available = 4;
}
Field
reasonHuman-readable, e.g. "waiting for an execution slot (all 8 in use)".
resourceWhich resource admission is short of: "cpu" | "memory" | "slots", or "queue" when the execution is waiting behind earlier ones. Empty for WAITING, which is not an admission wait.
neededWhat admission needs and what is free, in the resource's own unit (millicores, bytes, slots; for queue, the executions ahead and 0). Zero for WAITING.
available 

ControllerOutputEvent

message ControllerOutputEvent {
  optional string output_cid = 1;
}
Field
output_cidCID (string form) of the build's output artifact.

ControllerErrorEvent

message ControllerErrorEvent {
  optional string message = 1;
}

ControllerTargetsEvent

The targets a workspace's build code declares, and how much to trust the list.

Sent as its own event rather than folded into the terminal output because it is known long before the build finishes — the code declares everything by the time its members have been imported — and because a build that fails still has a useful answer to "what could this workspace build". A list that only arrived with a successful output would be missing exactly when it is most wanted.

message ControllerTargetsEvent {
  optional TargetListStatus status = 1;
  repeated BuildTarget targets = 2;
  optional string error = 3;
}
Field
statusHow current the list is.
targetsEvery declared target. Empty while generating, and on failure.
errorWhy the list could not be produced. Set only with STATUS_FAILED.

BuildTarget

One thing a workspace can build, run, deploy or test.

message BuildTarget {
  optional string name = 1;
  optional TargetKind kind = 2;
  optional map<string, string> labels = 3;
}
Field
nameIts full path: &lt;member>/&lt;scope path>/&lt;name> — e.g. bldr/daemon/tests/unit. A target's name is where it was declared.
kindWhat it is, which decides what can be done with it.
labelsFree-form labels the build attached, for the targets that carry any.

ControllerDiagnosticsEvent

Diagnostics the build's tooling produced, as they are found.

Its own repeatable event rather than something attached to the terminal result, because the value here is timeliness: a typecheck that runs for two minutes should report its first error at three seconds, and a build that fails is exactly the one whose diagnostics matter most — a payload that only rode a successful output would be missing whenever it was wanted. The node appends each event to what it already has; an event is never the complete set, and a controller that has nothing new to say sends nothing.

message ControllerDiagnosticsEvent {
  repeated CodeDiagnostic diagnostics = 1;
}
Field
diagnosticsThe diagnostics found since the previous such event on this stream. Batching is the controller's choice: one event per tool invocation costs less than one per finding, and neither changes what the node does.

CodeDiagnostic

One thing a build's tooling said about one place in the source.

Mirrored field-for-field from builder.CodeDiagnostic rather than imported, exactly as BuildTarget and TargetKind above are: this file is the whole contract a controller binary compiles against, and making it depend on the node's own API proto would drag the entire client surface along with it. The node translates one to the other verbatim.

Every positional field is optional in the sense that 0 means "not reported". A tool that knows only the file must still be able to say so — a diagnostic with no line is worth strictly more than no diagnostic.

message CodeDiagnostic {
  optional string member = 1;
  optional string path = 2;
  optional uint32 line = 3;
  optional uint32 column = 4;
  optional uint32 end_line = 5;
  optional uint32 end_column = 6;
  optional DiagnosticLevel level = 7;
  optional string title = 8;
  optional string description = 9;
  optional string snippet = 10;
  optional uint32 snippet_start_line = 11;
  optional string code = 12;
  optional string tool = 13;
  optional string target = 14;
  repeated RelatedDiagnostic related = 15;
  repeated DiagnosticFix fixes = 16;
  optional map<string, string> labels = 17;
  optional string root = 18;
  optional uint64 offset = 19;
  optional uint64 end_offset = 20;
}
Field
memberWorkspace member the file belongs to, e.g. bldr/ui. Empty when the controller could not attribute it to one.
pathPath within the member, relative and without a leading slash, e.g. src/App.tsx. When member is empty this is whatever path the tool gave, verbatim, since anything else would be an invention.
line1-based. 0 means the tool did not report a position.
column 
end_line1-based, inclusive end of the span. 0 for a point diagnostic.
end_column 
level 
titleOne line, no trailing newline — what a list shows.
descriptionThe full text, including whatever notes and hints the tool attached. Empty when it adds nothing to title.
snippetSource lines around the diagnostic, verbatim and newline-separated, so a consumer can render context without fetching the file — which it may not be able to do at all. Empty when the controller chose not to include one.
snippet_start_line1-based line number of snippet's first line. 0 when there is no snippet.
codeThe tool's own code, e.g. TS2322 or clippy::needless_borrow. Empty when the tool has no such concept.
toolWhat produced it, e.g. tsc, clippy, rustc. Names a tool, not a version.
targetThe build target it came out of, when the controller knows — bldr/ui/check. Empty otherwise.
relatedMirrors of builder.CodeDiagnostic's later fields, so a controller that has them can pass them through rather than flattening them away. See that message for what each means; the node forwards them untouched.
fixes 
labels 
root 
offset 
end_offset 

RelatedDiagnostic

Mirrors builder.RelatedDiagnostic.

message RelatedDiagnostic {
  optional string message = 1;
  optional DiagnosticSpan location = 2;
}

DiagnosticSpan

Mirrors builder.DiagnosticSpan.

message DiagnosticSpan {
  optional string member = 1;
  optional string path = 2;
  optional uint32 line = 3;
  optional uint32 column = 4;
  optional uint32 end_line = 5;
  optional uint32 end_column = 6;
  optional uint64 offset = 7;
  optional uint64 end_offset = 8;
  optional string root = 9;
}

DiagnosticFix

Mirrors builder.DiagnosticFix.

message DiagnosticFix {
  optional string message = 1;
  optional string applicability = 2;
}

ControllerLspHeader

What an LSP session over build code needs before it can answer anything.

The controller resolves a workspace's members from a BuildInput — that is how it learns which members exist, where their .bldr/ sources are, and what paths map their imports resolve through. A build carries one; an editor session does not, so the node supplies the workspace's current input here.

It is not a build. Nothing is executed, no pods start: the input is used only to index the build code the language service then answers about. When the workspace changes on disk the node syncs a new input and sends a fresh header, which is what keeps an editing session current.

message ControllerLspHeader {
  optional string input_cid = 1;
}
Field
input_cidThe workspace's current BuildInput CID.

PrepareInput

Call a node's input mapper with the outputs of its dependencies.

message PrepareInput {
  optional uint64 call_id = 1;
  optional string node_id = 2;
  optional uint64 mapper_id = 3;
  optional map<string, string> dependency_outputs = 4;
}
Field
call_idNames this call; the answer carries it back.
node_idThe node whose input is wanted.
mapper_idThe mapper the node declared.
dependency_outputsOutput CID (string form) of each dependency, by dependency node id.

ControllerGraph

A set of nodes declared together, and the targets that point at them.

message ControllerGraph {
  repeated GraphNode nodes = 1;
  repeated GraphTarget targets = 2;
}

GraphNode

One declared node: everything about it except its input.

message GraphNode {
  optional string id = 1;
  optional string function = 2;
  optional string label = 3;
  optional map<string, string> labels = 4;
  repeated string dependencies = 5;
  oneof input {
    uint64 mapper_id = 6;
    ConstantBlock constant_input = 7;
  }
  oneof _target_prefix {
    string target_prefix = 8;
  }
}
Field
idUnique in the build. A subgraph's nodes are scoped under their boundary node's id with a /.
functionThe node function to invoke, or execute-graph for a node whose mapper returns a subgraph.
labelHuman-readable label.
labelsFree-form key/value metadata.
dependenciesIds of the nodes whose outputs this node needs.
mapper_idAn input mapper in this controller's isolate. The id means nothing outside that isolate and is never stored or hashed.
constant_inputAn input known at declaration.
target_prefixSet on an execute-graph node whose subgraph declares targets: every target it declares has this prefix.

ConstantBlock

A block the controller authored, for the node to store: a JSON value in the form of InputPrepared, and the type to mint its CID with (a DataType label such as "deployment"; empty for generic DAG-CBOR).

message ConstantBlock {
  optional string json = 1;
  optional string data_type = 2;
}

GraphTarget

A named pointer at a node.

message GraphTarget {
  optional BuildTarget target = 1;
  optional string select = 3;
  oneof artifact {
    string node_id = 2;
    ConstantBlock constant = 4;
  }
}
Field
targetName, kind and labels, as on every target.
node_idThe node whose output is this target's artifact.
selectA dag-path expression (docs/dag-paths.md) selecting the artifact inside the node's output block, e.g. .cid for a function that answers &#123; cid: &lt;link> &#125;. Empty: the output block itself.
constantA block authored by the build code: a deployment, a runnable, a pipeline. The target's artifact is the stored block.

InputPrepared

What an input mapper produced.

message InputPrepared {
  optional uint64 call_id = 1;
  oneof result {
    string input_json = 2;
    ControllerSubgraph subgraph = 3;
    string error = 4;
  }
}
Field
call_idThe PrepareInput this answers.
input_jsonThe input, as a JSON value the node turns into blocks. A CID link is &#123; "/": "&lt;cid>" &#125; and a byte string &#123; "/": &#123; "bytes": [...] &#125; &#125;.
subgraphFor an execute-graph node: the subgraph to execute.
errorThe mapper threw. The message carries the JavaScript error and stack.

ControllerSubgraph

The subgraph an execute-graph node's mapper returned.

message ControllerSubgraph {
  optional ControllerGraph graph = 1;
  optional string result_node_id = 2;
}
Field
graph 
result_node_idThe node whose output becomes the execute-graph node's output.

ControllerTaskState

enum ControllerTaskState {
  CONTROLLER_TASK_RUNNING = 0;
  CONTROLLER_TASK_COMPLETED = 1;
  CONTROLLER_TASK_FAILED = 2;
  CONTROLLER_TASK_QUEUED = 3;
  CONTROLLER_TASK_CANCELLED = 4;
  CONTROLLER_TASK_WAITING = 5;
  CONTROLLER_TASK_WAITING_FOR_RESOURCES = 6;
}
Value
CONTROLLER_TASK_RUNNING 
CONTROLLER_TASK_COMPLETED 
CONTROLLER_TASK_FAILED 
CONTROLLER_TASK_QUEUEDNode opened before its work exists: it shows in the tree straight away, as queued, instead of appearing only once it becomes runnable.
CONTROLLER_TASK_CANCELLEDNever ran, or never finished, because the build failed elsewhere. Not a failure of its own — it has no error to report.
CONTROLLER_TASK_WAITINGStarted, and blocked on something outside itself (a lock another task holds, a rate limit). wait.reason says what. Non-terminal.
CONTROLLER_TASK_WAITING_FOR_RESOURCESHeld at the node's admission gate: the host has no capacity to start it yet. wait carries the shortfall. Non-terminal; RUNNING follows. A node that predates this value reads it as RUNNING, which is what it showed for a held task anyway.

TargetListStatus

How much to trust the target list.

enum TargetListStatus {
  TARGET_LIST_STATUS_UNSPECIFIED = 0;
  TARGET_LIST_STATUS_GENERATING = 1;
  TARGET_LIST_STATUS_FAILED = 2;
  TARGET_LIST_STATUS_DONE = 3;
  TARGET_LIST_STATUS_STALE = 4;
}
Value
TARGET_LIST_STATUS_UNSPECIFIED 
TARGET_LIST_STATUS_GENERATINGThe controller is working on it. Nothing to show yet, but something is coming — distinct from a workspace that genuinely declares nothing.
TARGET_LIST_STATUS_FAILEDThe list could not be produced: the build code did not type-check, or failed while declaring. error says why. This is a list failure, not necessarily a build failure.
TARGET_LIST_STATUS_DONEFresh, and describes the build code as it currently stands.
TARGET_LIST_STATUS_STALEA watch build whose inputs have moved since the list was produced. The contents are still the last good answer — worth showing, worth marking, and not worth blanking the page over.

TargetKind

What a target is. Decides which verb applies to it: an output is exported, a test is run for its verdict, a runnable is started, a deployment invoked.

enum TargetKind {
  TARGET_KIND_UNSPECIFIED = 0;
  TARGET_KIND_OUTPUT_DIRECTORY = 1;
  TARGET_KIND_CONTAINER_IMAGE = 2;
  TARGET_KIND_TEST = 3;
  TARGET_KIND_RUNNABLE = 4;
  TARGET_KIND_DEPLOYMENT = 5;
  TARGET_KIND_COVERAGE = 6;
  TARGET_KIND_METRICS = 7;
  TARGET_KIND_DIAGNOSTICS = 8;
  TARGET_KIND_PIPELINE = 9;
}
Value
TARGET_KIND_UNSPECIFIED 
TARGET_KIND_OUTPUT_DIRECTORYA filesystem tree (scope.addOutputDirectory).
TARGET_KIND_CONTAINER_IMAGEA container image (scope.addImage).
TARGET_KIND_TESTA test (scope.addTest) — building it is running it.
TARGET_KIND_RUNNABLEA runnable (scope.addRunnable), including language servers.
TARGET_KIND_DEPLOYMENTA deployment (scope.addDeployment).
TARGET_KIND_COVERAGEA coverage report (scope.addCoverage). Its own kind rather than an output directory because what a client does with it differs: there is a percentage to show and files to rank by it, neither of which a file tree offers.
TARGET_KIND_METRICSA metrics blob (scope.addMetrics): line-protocol rows queried via GetBuildMetrics. Same value as builder.proto's TargetKind — the daemon forwards the integer untouched.
TARGET_KIND_DIAGNOSTICSA diagnostics report (scope.addDiagnostics): the messages a tool emitted about code. Same value as builder.proto's TargetKind — the daemon forwards the integer untouched.
TARGET_KIND_PIPELINEA pipeline definition (scope.addPipelineTarget): a declarative pipeline authored by build code. Same value as builder.proto's TargetKind — the daemon forwards the integer untouched.

DiagnosticLevel

Mirrors builder.DiagnosticLevel, numerically identical so the node's translation is a copy and never a lookup table that can drift.

enum DiagnosticLevel {
  DIAGNOSTIC_LEVEL_UNSPECIFIED = 0;
  DIAGNOSTIC_LEVEL_ERROR = 1;
  DIAGNOSTIC_LEVEL_WARNING = 2;
  DIAGNOSTIC_LEVEL_INFO = 3;
}

On this page