Skip to content

Parametric Aspects

A parametric aspect is an aspect defined as a function whose arguments are pipeline context values — host, user, home, or any custom entity kind. Den introspects the function’s argument pattern and only calls it in contexts where all required arguments are available.

# This aspect only activates in {host} contexts
den.aspects.networking = { host, ... }: {
nixos.networking.hostName = host.name;
};
# This only activates when both {host, user} are present
den.aspects.user-groups = { host, user, ... }: {
nixos.users.users.${user.userName}.extraGroups = [ "wheel" ];
};
# This only activates for standalone {home} contexts
den.aspects.shell-config = { home, ... }: {
homeManager.programs.zsh.enable = true;
};

No wrapper needed — a bare function requiring { host, user } is silently skipped in contexts that only have { host }. The argument shape is the condition. No mkIf, no enable flags.

Static attrsets (non-functions) are always included regardless of context:

den.aspects.firewall = {
nixos.networking.firewall.enable = true;
};

When a parametric aspect at some scope S destructures an entity-kind arg (host, user, home, …), exactly one of three things happens:

  1. In-context → bind once at S. If the kind is already in S’s context (e.g. a { user, … } aspect included at a user scope), the arg binds and the aspect emits once, at S.
  2. Schema-DAG descendant → fan out, emit at S. If the kind is a descendant of S in the entity schema (e.g. a { user, … } aspect included at the host scope — users live under hosts), the aspect fans out once per matching descendant, each emitting class-locally at S. This is how a host-scope { user, … } aspect produces per-user content on the host.
  3. Neither → inert, silently. If the kind is neither in-context nor a descendant of S — including a misplaced arg, or any entity-kind arg at the root/flake scope — the aspect contributes nothing. There is no warning (a warning was considered and deliberately rejected: legitimate fan-out and misplacement are indistinguishable without whole-fleet context, so a warning would fire on correct code).

Aspect-level parametric — the function wraps the whole aspect:

# host is a pipeline arg — the whole aspect is parametric
den.aspects.laptop = { host, ... }: {
nixos.networking.hostName = host.name;
homeManager.programs.git.userName = host.hostName;
};

Class-level context injection — Den args inside a class module:

# host here is pre-applied by wrapClassModule, not parametric dispatch
den.aspects.laptop = {
nixos = { host, config, pkgs, ... }: {
networking.hostName = host.name;
};
};

Both patterns are valid and composable. Use aspect-level parametric when the context drives the entire aspect (which classes to include, what includes to add). Use class-level injection when only a specific class module needs entity data alongside module-system args.

Contribute Community Sponsor