Skip to content

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 nixosSystem
nest.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
  • modules — list of config contributions from all matching rules + child nodes
  • Return value — the output object (e.g., a nixosSystem or homeManagerConfiguration). Return null to 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.

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, firewall

needs may also be a function for late binding:

nest.trait.lb.needs = traits: [ traits.server ];

The function receives the fully processed trait tree.

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 required

neededBy 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: …) — receives ctx.select directly
  • 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 users
nest.trait.host.synth = select: {
node.userCount = builtins.length (select.children nest.user);
};
# Example: inject users from registry
nest.trait.host.synth = select:
let adminUsers = select nest.admin;
in {
node.children = map (u: {
inherit (u) name sshKeys;
is = [ nest.user nest.admin ];
}) adminUsers;
};
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);
};
};
Contribute Community Sponsor