Skip to content

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 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 (built from finalAnnotated)
  • moduleslist of contributions from all matching rules + child fragments for this class key
  • Return value — the output object. Return null to 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, 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, … }: …) — uses callWithArgs, 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 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;
};

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"

After injectNames, special keys are preserved as-is:

KeyPreserved as-is
class
needs
neededBy
synth
__traitName
everything elseGets __traitName injected if it’s an attrset (nested trait)
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