A fleet on one page
yen is a query language for Nix configurations: one file of Nix builtins, no dependencies, no module system,
no fixpoint, and no throw. You describe a fleet as data and behavior. yen answers queries on it: the systems to build,
the errors, the graph of who gives what to whom, and whether your rules hold across machines.
yen has no batteries and no vocabulary. It does not know what a host, a user or NixOS is. You name your own nouns, so it configures NixOS, nix-darwin, home-manager, clusters, terraform or anything else you can model as Nix values.
Documentation · Getting started · Reference fully-featured fleet · vic/vix on yen · Sponsor vic
Four words
Section titled “Four words”- Kinds: your nouns, typed.
yen.kinds.host.fields.addr = lib.types.str; - Scopes: where things live.
spawnsbuilds a tree from your data: a user under each host it is on. - Aspects: behavior, active where a predicate holds: a kind, an instance, another aspect, or any fact.
- Sinks: where elements go. An element goes up to the nearest scope that collects it, and the sink reduces them.
Data is queried, not merged: each read is a query on the files that answer it, so content in many files never overwrites itself, and an error is a record you can read.
A fleet on one page
Section titled “A fleet on one page”Two environments, three NixOS hosts, a user on two of them, postgres placed by a fact (some host names it as its
database), and a rule that holds across machines. Every line runs: these files are a tested template
(plus a hardware.nix stub so NixOS evaluates).
# Kinds are your nouns. `spawns` builds the tree from data. Sinks collect what aspects give.{ yen, lib, inputs, ...}:let inherit (yen.lib) same is maybe;in{ yen.kinds.fleet.spawns.env = _: yen.env; yen.kinds.fleet.sinks.out.reduce = yen.lib.reduce.merge; yen.kinds.env.fields.domain = lib.types.str; yen.kinds.env.spawns.host = s: lib.filterAttrs (_: h: same h.env s) yen.host; yen.kinds.host.fields = { env = yen.kinds.env; db = maybe yen.kinds.host; }; yen.kinds.host.spawns.user = s: lib.filterAttrs (_: u: lib.any (same s) u.on) yen.user; yen.kinds.host.sinks.os.nixos = { os, ... }: { imports = os; }; # os feeds nixos yen.kinds.host.sinks.nixos.out = { host, nixos, ... }: { nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; }; }; yen.kinds.user.fields.on = is lib.isList; yen.flake = { out, ... }: out;}# Instances: plain data, refs between them.{ yen, ... }:{ yen.fleet.main = { }; yen.env.prod.domain = "example.com"; yen.env.lab.domain = "lab.example.com"; yen.host.web = { env = yen.env.prod; db = yen.host.db; }; yen.host.db.env = yen.env.prod; yen.host.box.env = yen.env.lab; yen.user.ana.on = [ yen.host.web yen.host.box ];}# Aspects are active where a predicate holds; each element goes up to the nearest `os` sink.{ yen, lib, ... }:let inherit (yen.lib) same;in{ yen.aspects.names = { at = yen.kinds.host; os = { host, env, ... }: { networking = { hostName = host.name; domain = env.domain; }; }; }; yen.aspects.account = { at = yen.kinds.user; # no `os` sink on a user: the account lands on each host ana is on os = { user, ... }: { users.users.${user.name}.isNormalUser = true; }; }; yen.aspects.postgres = { at = s: lib.any (h: same h.db s) (lib.attrValues yen.host); # a fact: some host uses me as its db os.services.postgresql.enable = true; }; # a rule across machines, checked before deploy: a host's db lives in its environment yen.invariants.db-same-env = { scopes, ... }: map (s: { head = "bad:db-other-env:${s.name}"; pos = [ "ref:db:${s.id}:host:${s.db.name}" ]; }) (lib.filter (s: s.kind == "host" && s.db != null && !(same s.db.env s.env)) scopes);}Ask it. The tree comes from the data:
[ "fleet:main" "fleet:main/env:lab" "fleet:main/env:lab/host:box" "fleet:main/env:lab/host:box/user:ana" "fleet:main/env:prod" "fleet:main/env:prod/host:db" "fleet:main/env:prod/host:web" "fleet:main/env:prod/host:web/user:ana"]Change it safely
Section titled “Change it safely”An agent proposes a host in lab that uses the prod database. yen.lib.stage evaluates the fleet without and with
the change, applies nothing, and refuses it with the rule it breaks:
# A change, as an agent proposes it: a lab host that uses the prod database.{ yen, ... }:{ yen.host.api = { env = yen.env.lab; addr = "10.0.1.2"; db = yen.host.db; };}{ added = [ "fleet:main/env:lab/host:api" ]; ok = false; violations = [ "bad:db-other-env:api" ];}What else it does
Section titled “What else it does”- Graphs, Datalog and µKanren, in pure Nix: deploy waves, well-founded models with proofs and why-not, and values synthesized under constraints (a free port, guest memory that fits). Ask and prove
- Agents as team members: every answer is JSON-safe data. Changes are staged, checked by invariants, limited by grants that only narrow, and written as change programs. An MCP server answers agents. For agents
- Errors with fixes:
code,path,got,want,fix, andnearfor typos. Errors as data - Fast where it counts: one nixpkgs per system across hosts, and trimmed NixOS module lists, with drvPath oracles that prove the systems are the same. Performance
- Every capability of Den, den-hoag and clan, and the patterns of real fleets, each with a check, in the reference fleet. From Den
nix flake init -t github:vic/yen#one-page-fleetThen read the documentation: every page is a tested example. Coming from configuration.nix, a flake or Den?
The migration guides build the same systems before and after.
yen is made by vic, with love, as part of the Dendritic ecosystem. If it helps you, please sponsor vic. Issues and pull requests are welcome: see CONTRIBUTING.md.