A Task runs a repository-owned command in its environment. Use a Build when the command produces an artifact whose inputs, dependencies, execution capabilities and output should be managed explicitly. Keep compilation and packaging logic in the script; Blueprint supplies the declared inputs and asks the executor to obtain the result. A Workflow orders tasks and builds. The shipped sandboxed executor uses Nix on x86_64 Linux.

Declare a build§

Inside config, with a Nix package source named nix, scripts/build.roc in the project and the build-scripts environment of Lock the Roc packages scripts use:

Source("assets", "path:./assets"),
Executor("native", Nix("nix")),
Build("app", [
	Execute("native"),
	Use("build-scripts"),
	Inputs(["assets"]),
	Run(["roc-stable", "scripts/build.roc"]),
	Output("dist/app"),
]),

Executor("native", Nix("nix")) declares an executor named native that builds with Nix and takes its bootstrap package set from the source nix; Execute("native") selects it. A Source belongs to Nix without a Resolver.

Run is any argv; nothing about a build requires Roc. Roc takes the first -- of its arguments for itself, so pass a script its own arguments after one: Run(["roc-stable", "scripts/build.roc", "--", "--release"]).

What a build sees and produces§

Run blueprint update, then blueprint build app. The printed store path is resolved by Nix and contains exactly the declared file or directory. Missing outputs fail. Needs(["library"]) adds build dependencies; cycles fail during configuration validation. Sources live under $BLUEPRINT_INPUTS/<name> and needed artifacts under $BLUEPRINT_ARTIFACTS/<name>, both read-only and separate from the writable project copy.

Each build snapshots current project files, including untracked/task-generated files, excluding VCS metadata, caller-generated roots, authority and all local input trees. Blueprint makes that copy itself, as Select a build’s project files describes, and a file is executable in a build only when its owner may execute it. Changing a locked local source requires explicit update. Initial source/output policy rejects symlinks and special files. Only local x86_64 Linux sandboxed execution is verified; tasks/config compilation are not sandboxed. A build’s PATH holds exactly the tools its environment takes from its package sources, and nothing of the machine: no sh, cp, mkdir or python3 unless the environment’s Tools name bash, coreutils or python3. A command that is not there fails with build command not found, or inside the build’s own script when that script calls one. A build does not see the variables Nix’s stdenv exports either. A tool may give what it runs more than the build was given: the Roc compiler’s package puts coreutils and a C compiler on the PATH of a script it runs. Expected host tools cannot be supplied inside this sandbox. Tool origins are checked against the selected executor’s capabilities before effects; support for a fetched program must be established by that executor rather than inferred from a task’s ability to run it. A versioned standalone runner executes builds using only the locked package set. Its bytes are independent of the Blueprint CLI executable. Caller namespace identities are mounted as an authorization witness at execution time, outside the derivation; they do not change the derivation identity. Both mount and network namespaces must differ from the caller before the runner creates a workspace or starts the command. A missing mount, disabled sandbox, invalid witness or shared namespace fails closed. Builds run locally on x86_64 Linux; remote Nix builders are disabled for this isolation contract. See the complete runnable example, real build fixture.

Original files with published hashes§

SourceFile(name, url, sha256) declares an original upstream file, such as a release archive whose publisher supplies a flat SHA-256. The hash is exactly 64 lowercase hexadecimal characters. The URL follows the same public HTTPS rules as fetched programs. Use Inputs([name]) on a build: the unchanged file is available at $BLUEPRINT_INPUTS/<name>. An archive is never unpacked implicitly; name the unpacker in the environment’s Tools and run it in the producer script when needed. Source(name, ref) continues to supply a locked source tree, including archives that Nix unpacks and pins by their NAR hash.

blueprint update fetches and verifies the original bytes against the declared published hash before committing a pin. blueprint update NAME can select the file alone. The authority is a sources entry in Blueprint.lock with provider = "file", the original URL in ref, and its flat sha256:<hex> in digest. There is no additional source lock. URL, name, hash, addition or removal changes require explicit update.

Ordinary builds and generation use the committed digest. They hash cached bytes on every use, fetch a missing file from its locked URL, and repair a corrupt cache by fetching and verifying it again. Wrong upstream bytes fail; ordinary fetches never change the lock. A verified cached file works offline. The source enters Nix as a locked path: Blueprint measures the verified cached file with nix hash path, and the flake reads it with builtins.fetchTree, which checks that NAR hash, then checks the flat SHA-256 again. Its store identity is independent of the caller’s cache location, and the recipe sees the original archive bytes. Unrelated files that no selected build uses are not fetched.

blueprint lock show --json NAME reports digestKind: "file" and the original flatSha256 separately from tree identities, with empty metadata: {}. A NAR hash is not a publisher’s hash of an archive; do not compare them as if they were the same digest. Build provenance includes the selected file source’s name, original URL and flat digest alongside the other locked sources used.

Order tasks and builds in a workflow§

Inside config, referring to existing tasks and builds:

Workflow("ci", [RunTask("check", ["--verbose"]), BuildArtifact("app"), RunWorkflow("verify")]),
Workflow("verify", [RunTask("version", [])]),

blueprint workflow ci plans the complete dependency/capability/layout/lock closure before executing any task. Steps run in order and stop on the first failure. Repeated explicit tasks and builds repeat; every build snapshots current project files and rebuilds its dependency graph from that snapshot. Nix may reuse unchanged inputs, but Blueprint never caches artifact results across tasks. Locked sources are verified again; task edits to them require explicit update. Cycles and excessive depth/expansion fail during configuration validation. See the real workflow fixture.

Select a build’s project files§

Files(["scripts/producer.sh", "dependencies/shared"]) on a Build selects only those project files and directory trees for that build. Paths are relative to the project root; absolute paths, . and .. components are refused. A selected path must exist. The usual generated-root, authority, VCS and locked local-source exclusions still apply. Omit Files to retain the whole-project snapshot; Files([]) gives the command an empty writable workspace.

Blueprint applies the selection itself, once for each build it prepares. It copies the selected files into snapshots/<build> in the workspace, emptied first, keeping each name’s bytes and whether the owner may execute each file, and refuses a symlink or special file among them; excluded entries are never inspected. The Nix executor measures that copy with nix hash path and records where it is and its NAR hash in project-<build>.json beside the flake. The flake reads the copy with builtins.fetchTree, locked to that hash, so evaluation is pure and the flake never reads the project itself. Nix has the copy in its store once the build is prepared, so Blueprint removes it when the build ends, however it ends, and removes any copy an interrupted run left before it stages anything.

Give each component its own single-output build with its own Files list. The builds share their locked package sets and any identical selected trees in the Nix store; an unrelated application edit does not change their snapshots.

Reach a tool’s libraries§

BLUEPRINT_TOOLS is a read-only directory of complete package roots. A plain or configured tool nix#glibc is at $BLUEPRINT_TOOLS/nix#glibc, so a producer can use $BLUEPRINT_TOOLS/nix#glibc/lib/ld-linux-x86-64.so.2 without guessing a store path. A Command("cc", "nix#clang") exposes its underlying package root at $BLUEPRINT_TOOLS/cc. These paths do not add tools to PATH, enable network access or opt into stdenv setup. Quote paths as usual in a shell.

Check reproducibility and record provenance§

blueprint build NAME --rebuild asks Nix to rebuild the existing derivation and compare its output. Nix fails when the output differs. Build it once before asking for this check; a missing prior output is not a reproducibility comparison. Run this for each component in CI.

blueprint build NAME --json prints one typed result as JSON and keeps progress on stderr. Both flags may be combined. Build result schema version 2 contains:

Reject unsupported schema versions and tolerate additive fields. Output locations are not general content checksums. The provider owns cache reuse; a successful build alone does not prove it was a cache hit.

Plan a build and explain changes§

blueprint plan build NAME shows the selected files, dependencies, executor and capabilities without preparing the provider recipe. Add --prepare to capture selected inputs and obtain the exact build_identity. Preparation may fetch pinned inputs and write caches, but does not change pins or execute the requested build or dependency builds. Nix preparation disables import-from-derivation.

blueprint explain build NAME compares the declared/captured input evidence with the last successful local build. Add --prepare to compare the exact current and previous build identities as well. Missing or incompatible baseline evidence is reported as unavailable, not unchanged. Relevant direct and transitive changes are named. A changed build identity means the request changed; it does not mean the output bytes differ.

Both commands first ask the executor’s provider to validate the executor declaration, as blueprint check does.

An unprepared plan has prepared: false and no exact build identity. It cannot establish that a cached artifact corresponds to the current request.

Use narrow Files selections to avoid unrelated project changes affecting a build. The default whole-project snapshot intentionally includes every eligible project file. Pinned dependencies, tools, options, commands and declared build dependencies also participate in preparation. Scripts must declare what they need: Blueprint cannot infer dependencies hidden in arbitrary commands.

The plan and explain JSON§

Both commands accept --json and print one JSON object, the same typed value their human output renders. plan and explain compute the same object; explain differs only in its human output, which lists the changes. The object’s schema_version is 1, independent of build result schema version 2. Reject unsupported schema versions and tolerate additive fields.

FieldMeaning
schema_version1.
nameThe requested build.
systemThe execution system.
providerThe selected executor’s provider, currently "nix".
preparedtrue with --prepare, otherwise false.
build_identityThe exact prepared build identity, compared together with provider. Empty when prepared is false.
outputsThe prepared outputs, each { name, path }. Empty when prepared is false.
required_capabilitiesWhat this request needs: builds; build-files when a build in the closure selects Files; dependency-builds when one has Needs; prepare when prepared.
provider_capabilitiesThe capabilities the executor’s provider advertises; see Capabilities and guarantees.
supported_guaranteesThe guarantees a successful build by that provider gives.
snapshot_digestsha256:<hex> of the snapshot’s S-expression encoding. A build result reports the same value.
snapshotThe captured inputs: name, system, provider and nodes, one node per build in the closure.
baseline"available" when the last successful local build record was read and compared, otherwise "unavailable".
baseline_reasonWhy the baseline is unavailable, such as no successful local build record. Empty when it is available.
build_identity_status"unchanged" or "changed" against the recorded build identity. "unavailable" when not prepared or when the baseline is unavailable.
changesWhat differs from the baseline, each { build, dependency_path, kind, name }. Empty when the baseline is unavailable.

Each snapshot.nodes entry describes one build:

FieldMeaning
nameThe build.
selected_filestrue when the build selects its project files with Files, false for the whole-project snapshot.
commandIts exact argv.
environmentIts environment.
executorThe name of its executor.
outputIts declared output path.
dependenciesThe builds it Needs.
declarationsAn S-expression of the declarations that shape it: the build; its environment with that environment’s tools, commands, configured tools, programs, expected tools and Roc packages; the package sources it uses; Raw data for its executor’s provider; its executor; and its Files selection.
filesEach captured project file and directory, { path, digest }, sorted by path. The digest is Blueprint’s tree digest of that one entry: its kind, path, executable bit and, for a file, its contents.
inputsEach locked input the build uses, { name, provider, content_identity }, sorted by name. A fetched program has provider "blueprint" and its sha256:<hex>.

Each change names the build it concerns, the dependency_path from the requested build to that build (such as ["app", "library"]), its kind and the name of what changed:

kindMeaning; name is
dependency-addedA build joined the closure; the build.
dependency-removedA build left the closure; the build. Its path is the one it had in the baseline.
dependencies-changedThe build’s Needs changed; the build.
declaration-changedIts command, environment, executor, output, file-selection mode or declarations changed; the build.
file-changedA captured file or directory is new or its digest changed; its path.
file-removedA captured file or directory is no longer captured; its path.
input-changedA locked input is new, or its provider or content identity changed; the input.
input-removedA locked input is no longer used; the input.

Capabilities and guarantees§

A capability is an operation an executor supports. blueprint build requires builds and prepare, and checked-rebuild with --rebuild; an executor without one is refused before any effect.

CapabilityMeaning
buildsRun managed builds.
build-filesCapture only the project files a build selects with Files.
dependency-buildsBuild what a build Needs and supply those artifacts to it.
prepareObtain the exact build identity without executing the build.
checked-rebuildRebuild an existing output and compare the result, for --rebuild.

A guarantee is what a successful build by that executor establishes. Listing one in a plan does not mean that planning ran or checked a sandbox.

GuaranteeMeaning
captured-declared-inputsThe command sees the captured project files and declared locked inputs, not the live working tree.
sandboxed-filesystemThe command runs in a mount namespace separate from the caller’s, with its inputs read-only.
sandboxed-networkThe command runs in a network namespace separate from the caller’s.
checked-rebuild-comparisonA checked rebuild fails when the rebuilt output differs from the existing one.

The Nix executor advertises all five capabilities and all four guarantees.

The local baseline§

A successful, validated build atomically advances its versioned local baseline at .blueprint/build-records/<system>/<name>.scm. Failed execution, planning and explanation preserve that baseline. Saving the record is best-effort: when it cannot be written, for example because it would exceed the 8 MiB bound, the build still succeeds, a warning goes to stderr and the baseline does not advance to that build. With no earlier record, explain reports the baseline as unavailable.

These S-expression records are operational evidence, not executable plans or dependency authority; do not commit them. Deleting them removes the comparison baseline, not the provider’s cached artifacts. Arbitrary historical-record comparison is not part of this interface.

Capture selected reproduction evidence§

The Nix executor supplies a read-only JSON file at $BLUEPRINT_INPUTS/blueprint-input-evidence. Its native format 1 records selected package revisions and NAR hashes, original-file SHA-256 pins, other source tree paths, dependency artifact paths, Roc package paths, the store path of the build’s locked copy of its selected project files and tool roots. This is provider-native reproduction material, not another provider-neutral report API or a complete lock export.

A prepared plan, explain or build measures each build’s selected files in that same copy, after Blueprint hashed it for Nix, rather than in the project again, so the evidence is exactly what Nix received even if the project changes meanwhile. An unprepared plan makes no copy and measures the project.

A producer can ship selected files and its producer module alongside this material. A dedicated configuration using the recorded package revision/NAR hash can prepare the same build request without depending on unrelated project pins. Mutable references, the whole Lock and the final build identity do not participate in this evidence input. Final identity and artifact SHA-256 belong in external result or release evidence, avoiding a build identity that depends on itself.

Override packages in a producer§

Input("producer-overrides", "path:./producer-overrides") may name a local flake with an overlays.default output. Select it with Overlays(["producer-overrides"]) in the producer’s environment. For example, that flake can define:

{
  outputs = { self }: {
    overlays.default = final: prev: {
      meson = prev.meson.overrideAttrs (old: {
        version = "1.12.0";
        src = prev.fetchurl {
          url = "https://github.com/mesonbuild/meson/releases/download/1.12.0/meson-1.12.0.tar.gz";
          # Replace with the checksum published for this exact upstream archive.
          hash = "sha256-<published-base64-sha256>";
        };
        # Apply a specific patch here when the upstream test change needs one.
      });
    };
  };
}

The checksum is an intentional placeholder; fill it before building. Blueprint locks the local flake’s tree and refuses edits until blueprint update. The override applies to builds whose environment selects that overlay. The artifact example declares two single-output builds selecting their producer directory and sharing the same pinned package environment.