Nix Flake Outputs Schema and Derivation Discoverability
Flakes need a way to tell tools about custom outputs beyond packages and shells.

Nix flakes solve a problem that predates them by years: a repository could hold a Nix build and give no outside observer any way to know what it actually provided. Before flakes, discoverability in Nix ran on convention alone. nix-build assumed default.nix, nix-shell assumed shell.nix, and nothing in the repository itself told a consumer what was actually on offer. A flake replaces that guesswork with structure: a flake.nix file at the root of a filesystem tree, carrying four top-level keys, description, inputs, outputs, and nixConfig, each one structurally defined so that any flake becomes inspectable by tooling rather than by reading source. [nix flake show](https://dev.to/therobustroast/how-ive-been-using-nix-flakes-and-what-you-can-do-with-them-3g8h) run against any flake renders what it provides, packages, dev shells, NixOS modules, without a single line of source code read, the moment the schema lets it do so, and that contract rests on two halves working together. The lockfile pins every input to an exact revision so that the same inputs always produce the same outputs, and the outputs schema defines what those outputs mean and how tools are supposed to interpret them. Everything that follows in this piece, the standard output types, the lockfile's role, the gap in the current schema, and the proposal meant to close it, is a consequence of that founding decision to treat outputs as a contract rather than a hint.
Standard output types and their CLI commands
That system string is load-bearing in the schema: it is how a single flake expresses support for multiple platforms without maintaining separate builds for each one. The standard output types and their CLI entry points, drawn from the NixOS Wiki's output schema, give the reader the vocabulary the rest of this piece depends on. A packages.<system>.<name> attribute is built with nix build .#<name>, and a packages.<system>.default attribute is built with the shorter nix build ., mirroring the old default.nix convention but now addressable by name. A devShells.<system>.<name> attribute is entered with nix develop .#<name>, and devShells.<system>.default is entered with plain nix develop. An apps.<system>.<name> attribute is run with nix run.#<name>, provided it carries type = "app" and a program store path for the CLI to execute. A checks.<system>.<name> attribute is executed as part of nix flake check, which evaluates every check defined across the flake in one pass. Beyond those per-system outputs sit the system-level and composition outputs, nixosConfigurations.<name> for whole machine definitions, nixosModules.<name> for reusable configuration fragments, overlays.<name> for package-set modifications, and templates.<name> for scaffolding new projects. Each of these output types carries an implicit validation built into the tooling: nix flake check evaluates checks, packages, devShells, apps, templates, and nixosConfigurations, and nix flake show renders the full tree, but only for the output types the CLI's C++ layer already knows about. That qualifying clause is the seam the rest of this article follows. The default attribute inside each output type is the schema's direct answer to the old default.nix idiom: a named, predictable slot so that tooling and the human reading nix flake show agree on what "the main thing" actually is, without either side relying on a filename convention that carries no information on its own.
The lockfile: making the outputs schema meaningful rather than aspirational
A schema defines shape, but shape alone guarantees nothing about content. Without pinned inputs, two separate evaluations of the same outputs function can produce entirely different derivations even though the schema describing them is identical. That is the gap flake.lock closes: it records every input at an exact revision, which makes the entire dependency graph deterministic, so the same inputs yield the same derivations and the same store paths every time the flake is evaluated. The schema tells a consumer what shape to expect from packages.x86_64-linux.default; the lockfile guarantees that evaluating that attribute today and evaluating it again next week, on a different machine, produces the same derivation. One of those guarantees without the other is incomplete, and treating the schema as a reproducibility mechanism on its own would overstate what it does.
The distinction matters most when someone tries to make a supply-chain claim on top of it. Flakes provide a repeatable build environment, the same .drv files, rather than bit-for-bit reproducible outputs in the sense the Reproducible Builds project defines the term, and achieving that stronger guarantee requires additional discipline in the build system itself, on top of what flakes alone provide. Anyone leaning on flakes as an audit trail needs to understand this boundary rather than assume the lockfile does more than it does. For teams that do need to communicate output integrity to auditors or downstream consumers, the input-derived hash of a derivation is the unit that can actually be attested to, and environment managers that surface that hash directly give teams a real head start on audit workflows rather than leaving them to reconstruct it by hand.
The hard limit in the current schema: custom outputs are invisible to standard tooling
The standard output types cover a lot of ground, but the list is fixed, and anything outside it is effectively invisible to the tools built around the schema. A flake author building an internal platform that outputs Kubernetes configs, or Home Manager configurations, has no way to communicate that interface to consumers through standard tooling, and the practical result is a retreat to README documentation, which defeats the entire discoverability purpose the schema was built to serve. The gap is structural rather than incidental: output types defined by the community, Home Manager's homeConfigurations being the clearest example, end up second-class relative to the types baked directly into C++, even when they are just as stable and just as widely used in practice. A NixOS Discourse post describing this limitation states it directly: commands like nix flake check and nix flake show have built-in support for a limited set of flake output types, and they know about nixosConfigurations but not homeConfigurations. The asymmetry is a consequence of where the knowledge of valid output types lives: hard-coded into the Nix CLI's C++ implementation, rather than defined somewhere a flake author or a tool maintainer could extend without waiting on a Nix release, not a judgment on Home Manager's design or its reliability. That is the exact seam the flake-schemas proposal was written to address.
Flake schemas: moving output-type knowledge from C++ into Nix functions
The flake-schemas PR, submitted by Eelco Dolstra, proposes a new schemas flake output: an attribute set of Nix functions that enumerate and validate other outputs, replacing the hard-coded C++ logic with user-supplied Nix code. The mechanism works by inversion. Rather than the CLI maintaining an internal list of recognized output types and branching on each one, a flake adds a schemas output whose keys correspond to the output types it wants tooling to understand, and each value is a function that knows how to list, display, and check that particular output type. nix flake show and nix flake check then call those functions generically instead of branching on a fixed set of known type names. The consequence is that tools like the Nix CLI and FlakeHub can display and check any output type generically, homeConfigurations, Dockerfiles, Terraform configs, Kubernetes manifests, without requiring a central authority to decide in advance which types count as legitimate. The proposal also sketches further work: making flake outputs configurable via the CLI itself, which would decouple the schema definition even further from the Nix release cycle and let the set of recognized output types evolve without waiting on a new Nix version.
The proposal draws one boundary explicitly, given how easily the idea could be oversold. Flake schemas are not a type system for Nix. They describe and validate the structure of outputs, but they do not attempt to type-check the Nix expression language itself, a problem that remains separate and considerably larger in scope.
Reading and publishing flakes effectively given today's schema boundaries
Given what the schema guarantees today and where its limits sit, both consuming and authoring flakes benefit from a few concrete habits. The first command to run against any unfamiliar flake is nix flake show, pointed at the flake reference in question: it renders the tree of known outputs along with their system coverage, giving an immediate map of what the flake provides without reading a line of its source. For every standard output type, nix run, nix build, and nix develop are directly addressable using the attribute paths that nix flake show just revealed, so there is no guessing involved and no dependency on a README for basic consumption. That alone represents most of the value the schema was built to deliver.
Authoring a flake well means leaning on the same structure from the other direction. For cross-platform support, the flake-utils library's eachDefaultSystem function iterates automatically over x86_64-linux, aarch64-linux, x86_64-darwin, and aarch64-darwin, which avoids repeating the same output definitions once per platform while still making sure the schema covers every target system a consumer might run on. A dev shell pattern that works well in practice is a devShells.<system>.default attribute paired with a shellHook for setting environment variables and handling project setup, consumable directly with nix develop and automatable further with nix-direnv behind a one-line .envrc containing use flake.
For custom outputs that current tooling still cannot validate, the interim best practice is threefold: include schemas from the flake-schemas flake where one is available, document the output shape explicitly in the flake's description field or in a README, and name attributes so their purpose is self-evident, since even unvalidated tooling will at minimum display the attribute names it finds. None of that is a permanent fix. It is a workaround that keeps a flake usable while the schema itself catches up to what the Nix ecosystem is actually building. A publishing checklist follows naturally from the schema's own structure: a packages.<system>.default for the primary build artifact, a devShells.<system>.default for the development environment, a populated checks set for automated validation, and templates where the project is meant to be scaffolded by others. A consumer running nix flake show against a flake built this way sees a complete, predictable interface on the first command, which is the entire point of treating outputs as a contract rather than as documentation.
Mapping the schema to supply-chain visibility: derivations, closures, and SBOM generation
The same structural property that makes a flake's outputs inspectable by a human running nix flake show makes them inspectable by machines doing supply-chain analysis. Tools like the Nix CLI and FlakeHub that understand the schema can enumerate outputs generically. SBOM tooling can walk an output tree without writing bespoke parsing logic for each flake it encounters. The sbomnix project from TIIUAE operationalizes exactly this capability: sbomnix generates software bills of materials directly from a Nix flake reference or a store path, while its companion tool, provenance, generates SLSA v1.0-compliant provenance attestation files for any Nix flake or derivation, with both CycloneDX and SPDX output formats supported.
The Semwright project shows what this looks like in a production pipeline rather than as a standalone utility. A merged pull request in that project pins nixpkgs to an exact revision, builds release executables through a Nix derivation, generates reproducible per-binary CycloneDX 1.5 SBOMs twice over and compares them byte-for-byte, and produces deterministic, non-admitted certification bundles on both x86_64 and aarch64 architectures. The byte-for-byte comparison step gives the SBOM its evidentiary weight: it demonstrates that the schema's discoverability and the lockfile's determinism, taken together, produce an output tree stable enough to audit twice and get the same answer. A flake reference, read this way, stops functioning as a convenient build address and starts functioning as a provenance record that a security team can walk, hash, and compare against a prior run.
Sources
- Flake schemas: making flake outputs extensible | daily.dev
- Flakes - NixOS Wiki
- Flake schemas: Making flake outputs extensible - Announcements - NixOS Discourse
- Custom flake outputs for checks - Help - NixOS Discourse
- How I've Been Using Nix Flakes (And What You Can Do With Them) - DEV Community
- Show HN: Flake schemas
- GitHub - tiiuae/sbomnix: SBOM, provenance, dependency graph, and vulnerability tools for Nix. · GitHub


