Traits
Traits are defined under nest.trait. After injectNames, every trait attrset has a __traitName field. The nest proxy exposes them as nest.<traitName>.
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 context (built fromfinalAnnotated)modules— list of contributions from all matching rules + child fragments for this class key- Return value — the output object. Return
nullto produce no output.
Nest collects rule contributions as a list — it does not deep-merge them. The class fn decides how to handle the list. For hosts, pass it directly to nixosSystem. For child entities (users), return a module fragment for the parent.
Multiple class keys per trait:
nest.trait.user.class = { # Pass-through: forward user's own nixos contributions to parent nixos = select: modules: { nixos = modules; }; # Produce a module fragment consumed by the parent host via collectChildFrags user = select: modules: { nixos = [{ users.users.${select.node.name} = lib.mkMerge modules; }]; };};processNode calls firstMatch — the first non-null class fn result wins. The className determines which byClass bucket the output lands in.
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, … }: …) — usescallWithArgs, gets entity arg
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 (available as select.node.derivedAttr later). children entries are injected with:
__path = "${parent.__path}.${child.name}"__parentPath = parent.__path- Fully expanded
is(needs + neededBy 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; };__traitName
Section titled “__traitName”Injected automatically by injectNames. Always present after processing. Used by selectors to match traits by name regardless of attrset identity:
nest.admin.__traitName # "admin"nest.monitoring.__traitName # "monitoring"# Nested:nest.trait.monitoring.node-exporter.__traitName # "monitoring.node-exporter"Trait tree structure
Section titled “Trait tree structure”After injectNames, special keys are preserved as-is:
| Key | Preserved as-is |
|---|---|
class | ✓ |
needs | ✓ |
neededBy | ✓ |
synth | ✓ |
__traitName | ✓ |
| everything else | Gets __traitName injected if it’s an attrset (nested trait) |
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); };};