Users want to write to top-level flake-parts config paths (outside config.flake) from aspects — e.g., config.inputs, config.perSystem.packages, config.treefmt. The flake-parts-modules template demonstrates this is possible via a custom flake-parts entity with scope transition and manual resolution, but it requires ~80 lines of boilerplate that users must replicate.
Host, user, and home entities are built-in. The flake-parts entity should be too.
After this change, a user can:
# Register a custom class
den.classes.flake-inputs.description = "Flake input declarations";
# Route it to a top-level flake-parts path
den.policies.route-inputs = _: [
(den.lib.policy.route {
fromClass = "flake-inputs";
intoClass = "flake-parts";
path = [ "inputs" ];
})
];
den.schema.flake-parts.includes = [ den.policies.route-inputs ];
# Use the class naturally in aspects
den.aspects.igloo.flake-inputs.nixpkgs.url = "github:NixOS/nixpkgs";No manual resolveEntity, no isEntity = true, no scope transition policy, no perSystem.imports injection.
The flake-parts-modules template (templates/flake-parts-modules/) has the full working pattern:
modules/den.nix:108-118—to-flake-partspolicy:resolve.to "flake-parts"fromflake-systemscope, creates per-systemflake-partsentitymodules/den.nix:121—den.schema.flake-system.excludes = [ den.policies.to-packages ]to suppress conflicting built-inmodules/den.nix:123—den.schema.flake-parts.isEntity = truemodules/flake-parts.nix:4— manualden.lib.aspects.resolve "flake-parts" (den.lib.resolveEntity "flake-parts" {})injected intoperSystem.importsmodules/systems.nix—systems = builtins.attrNames den.hostsso flake-parts knows which systems to evaluatemodules/classes/*.nix— custom classes routedintoClass = "flake-parts"withpathandadaptArgs
Extract the template's wiring into den's built-in modules.
File: modules/context/flake-schema.nix (already registers flake, flake-system, default)
Add flake-parts as a schema entry kind with isEntity = true so the pipeline treats it as a full entity with policy dispatch.
New file: modules/policies/flake-parts.nix
Built-in policy to-flake-parts that creates the flake-parts entity scope from flake-system:
den.policies.to-flake-parts = { system, ... }: [
(den.lib.policy.resolve.to "flake-parts" {
flake-parts = {
name = "flake-parts-${system}";
aspect = {};
};
})
];
den.schema.flake-system.includes = [ den.policies.to-flake-parts ];The flake-parts entity carries no aspect content of its own — it only receives routed content from other classes. This differs from host/user/home which have lookupAspect defaults.
New file or extend modules/outputs.nix
Auto-resolve the flake-parts entity and inject into perSystem.imports:
perSystem.imports = [
(den.lib.aspects.resolve "flake-parts" (den.lib.resolveEntity "flake-parts" {}))
];Derive systems from den.hosts so flake-parts knows which systems to evaluate perSystem for:
systems = builtins.attrNames den.hosts;Without this, perSystem has no systems to iterate over. This mirrors the template's systems.nix.
The built-in to-packages, to-devShells, to-apps, to-checks policies in modules/policies/flake.nix route system-level classes into flake.<output>.<system>. When flake-parts is active, these conflict with flake-parts' own output handling.
Auto-exclude the conflicting policies from flake-system when flake-parts is active:
den.schema.flake-system.excludes = [
den.policies.to-packages
# ... other conflicting policies
];This mirrors the template's den.nix:121.
Guard all pieces behind actual flake-parts evaluation context — not just inputs ? flake-parts (which only checks input existence, not that the module system has flake-parts options). Use the existing no-flake-parts pattern from outputs.nix which checks !inputs ? flake-parts, and additionally verify options ? perSystem before injecting into perSystem.imports.
All class routes into flake-parts need adaptArgs to give aspect functions access to perSystem module args (pkgs, self', inputs'). This is NOT built into the entity wiring — each class route provides its own adaptArgs. Document the pattern:
den.lib.policy.route {
fromClass = "my-class";
intoClass = "flake-parts";
path = [ "my-path" ];
adaptArgs = { config, ... }: config.allModuleArgs;
}When this lands, the flake-parts-modules template's files change:
| Template file | Status |
|---|---|
modules/den.nix:108-123 |
Redundant — scope transition + isEntity + excludes now built-in |
modules/flake-parts.nix |
Redundant — resolution injection now built-in |
modules/systems.nix |
Redundant — systems derivation now built-in |
modules/classes/*.nix |
Kept — class registration + routes are user-defined |
modules/perSystem-from-hosts.nix |
Kept — host-to-flake-parts cross-entity routing is template-specific |
Existing template users get no-op duplication (idempotent merge via module system). Migration: remove the redundant files.
| File | Change |
|---|---|
modules/context/flake-schema.nix |
Add flake-parts entry with isEntity = true |
New modules/policies/flake-parts.nix |
Scope transition + conflicting policy suppression |
modules/outputs.nix or new module |
perSystem.imports injection + systems derivation |
templates/flake-parts-modules/ |
Simplify: remove redundant wiring files |
- New CI test: register custom class, route into
flake-parts, verify content lands at expectedperSystempath - New CI test: route into top-level flake-parts path (non-perSystem), verify it lands
- Existing
flake-parts-modulestemplate:nix flake check --override-input den . - Full CI:
nix develop -c just ci