Skip to content

Rules

Rules live in nest.rules as a list of attrsets. Each rule has:

  • is — selector determining which nodes this rule applies to
  • One or more class keys (nixos, user, etc.) — config contributed to matching nodes
nest.rules = [
{
is = nest.host; # selector
nixos = { }; # class key: contributes to nixos class config
}
];

Any selector form:

is = nest.host
is = [ nest.host nest.web ]
is = [ nest.host (nest.has nest.admin) ]
is = nest.attrs { env = "prod"; }
is = "*.web"

Each key besides is is a class key. It contributes config to the matching node’s merged config for that class.

{
is = nest.nginx;
nixos.services.nginx.enable = true;
}

When the value is a function, it is called via callWithArgs with the named args it declares:

{
is = nest.host;
nixos = { host, select, ... }:
{
networking.hostName = host.name;
networking.extraHosts =
lib.concatMapStringsSep "\n"
(p: "${p.addr} ${p.name}")
(select.siblings nest.host);
};
}

Available args:

ArgTypeDescription
selectselect contextSelect API for this node
entity argnodeThe node itself, keyed by entity trait name (host, user, alice, etc.)

Only declare the args you need — unused args are filtered out.

# host entity
nixos = { host, ... }: { networking.hostName = host.name; }
# user entity
user = { user, ... }: { isNormalUser = true; openssh.authorizedKeys.keys = user.sshKeys; }
# any node — just use select
nixos = { select, ... }: { result = select.node.someAttr; }

When multiple rules match a node, their contributions are collected as a list — one entry per matching rule. Nest does not deep-merge them. The class fn receives the full list and passes it to nixosSystem, which lets the NixOS module system handle merging with proper semantics (lib.mkForce, lib.mkDefault, type checking, conflict detection):

nest.rules = [
{ is = nest.host; nixos.networking.hostName = "default"; }
{ is = "#web-1"; nixos.networking.hostName = "web-1"; }
# Both entries go into the modules list passed to nixosSystem.
# NixOS sees two definitions for networking.hostName and merges by option type.
];

Multiple rules targeting different keys compose naturally:

{ is = nest.host; nixos.nix.settings.experimental-features = [ "nix-command" ]; }
{ is = nest.nginx; nixos.services.nginx.enable = true; }
# For a node with both host and nginx:
# nixosSystem receives both attrsets as separate modules — no nest-level merge needed

A rule can carry a synth key — processed identically to trait synth but fired by rule matching:

{
is = [ nest.host (nest.attrs { env = "prod"; }) ];
synth = { select, ... }: {
node.children = map (u: {
inherit (u) name sshKeys;
is = [ nest.user nest.admin ];
}) (select nest.admin);
};
}

The synth key result is stored in __mergedCfg.synth during annotation, then applied by applyRuleSynth after all nodes are annotated.

Rule synth children are re-annotated with all rules — they participate fully in output processing.

Rules are evaluated in list order. Within a single rule, class keys are merged in attrset key order (Nix attrset key order is alphabetical). Across multiple rules, later rules override earlier ones for the same path.

nest.rules = [
# Static config for all hosts
{
is = nest.host;
nixos = {
system.stateVersion = "25.11";
boot.loader.grub.enable = false;
};
}
# Dynamic config using select.node
{
is = nest.host;
nixos = { host, ... }: {
networking.hostName = host.name;
};
}
# Cross-node query via select.siblings
{
is = nest.lb;
nixos = { select, ... }:
let webs = select.siblings nest.web;
in {
services.haproxy.backends = map (w: w.addr) webs;
};
}
# Compound selector: host with admin child
{
is = [ nest.host (nest.has nest.admin) ];
nixos.security.sudo.wheelNeedsPassword = false;
}
# Rule synth: inject children
{
is = [ nest.host (nest.attrs { env = "prod"; }) ];
synth = { select, ... }: {
node.children =
map (u: { inherit (u) name sshKeys; is = [ nest.user nest.admin ]; })
(select nest.admin);
};
}
# User rule: fires on synthesized user children
{
is = nest.user;
user = { user, ... }: {
isNormalUser = true;
openssh.authorizedKeys.keys = user.sshKeys;
};
}
# Admin role adds wheel group
{
is = nest.admin;
user.extraGroups = [ "wheel" ];
}
];
Contribute Community Sponsor