Traits
Traits are defined under nest.trait. They are accessible as nest.<traitName> in your DOM and rules.
Declares that this trait is an entity trait — nodes with this trait produce output.
# host: receives list of NixOS module contributions, passes directly to nixosSystemnest.trait.host.class.nixos = select: modules: nixpkgs.lib.nixosSystem { system = select.node.system; inherit modules; # NixOS module system merges — nest does not };class is an attrset. Each key is an output class name. The value is a function select: modules: <output>:
select— the node’s select contextmodules— list of config contributions from all matching rules + child nodes- Return value — the output object (e.g., a
nixosSystemorhomeManagerConfiguration). Returnnullto produce no output.
Nest collects rule contributions as a list and passes it whole to the class function. The class function decides how to use it. For nixosSystem, pass it directly as modules — the NixOS module system handles merging.
Multiple class keys per trait:
nest.trait.user.class = { # Forward user's own nixos contributions to parent host nixos = select: modules: { nixos = modules; }; # Produce a users.users.<name> fragment for the parent host user = select: modules: { nixos = [{ users.users.${select.node.name} = lib.mkMerge modules; }]; };};The first non-null class function result determines which byClass bucket the output lands in.
Nested traits
Section titled “Nested traits”Traits can be nested under other traits. The nesting becomes the trait’s path, and it is referenced by that path:
# a `user` trait scoped under `host`nest.trait.host.user.class.user = select: modules: { /* … */ };
# in the DOM / rules, reference it by path:is = nest.host.user;A nested trait’s path also drives bare-name selector matching: a bare word
matches a trait whose path ends in that word — so is = "user" matches both
a top-level user trait and host.user. (A .class selector, by contrast,
matches an output-class name, not a trait path.)
Declares explicit trait dependencies. Any node gaining this trait automatically gains all listed traits (transitively):
nest.trait.server.needs = [ nest.nginx nest.ssh nest.firewall ];nest.trait.web.needs = [ nest.server ]; # web → server → nginx, ssh, firewallneeds may also be a function for late binding:
nest.trait.lb.needs = traits: [ traits.server ];The function receives the fully processed trait tree.
neededBy
Section titled “neededBy”Reverse dependency. The trait declares what selects it — any node matching the selector automatically gains this trait:
nest.trait.monitoring.neededBy = nest.server;# All server nodes gain monitoring — no opt-in requiredneededBy accepts any selector (trait, compound, string, constructor). It is evaluated iteratively until fixpoint — chained neededBy declarations work:
nest.trait.node-exporter.neededBy = nest.monitoring;# server → monitoring → node-exporter (all automatic)Bottom-up synthesis. Runs after trait expansion + neededBy, before rules fire.
Signature:
synth = select: { node = { … }; }# OR (destructuring, receives entity arg too)synth = { select, host, … }: { node = { … }; }- Positional form (
select: …) — receivesctx.selectdirectly - Destructuring form (
{ select, host, … }: …) — gets entity arg injected
Return format:
{ node = { derivedAttr = value; # merged onto the node anotherAttr = value; # additional attrs children = [ # virtual children injected as DOM nodes { name = "alice"; is = [ nest.user ]; sshKeys = [ "…" ]; } ]; };}Everything in node except children is merged onto the node as derived attributes (accessible via select.node in rules). children entries are injected as proper DOM nodes with full trait expansion applied.
# Example: count usersnest.trait.host.synth = select: { node.userCount = builtins.length (select.children nest.user);};
# Example: inject users from registrynest.trait.host.synth = select: let adminUsers = select nest.admin; in { node.children = map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; }) adminUsers; };Complete trait example
Section titled “Complete trait example”nest.trait.server = { needs = [ nest.nginx nest.ssh ]; # dependencies};
nest.trait.monitoring = { neededBy = nest.server; # auto-inject on servers};
nest.trait.host = { class.nixos = select: modules: # entity: produces nixosSystem nixpkgs.lib.nixosSystem { system = select.node.system; inherit modules; # list passed directly — no deep-merge };
synth = select: { # derive user count node.userCount = builtins.length (select.children nest.user); };};