What Nx conformance rules are and how they differ from lint
Lint rules examine one source file at a time. A linter parses a file, walks its syntax tree, and reports problems such as an unused import or a banned API call. That scope works well for code style and local correctness. It cannot answer questions about the workspace as a whole, because no single file holds that information.
Nx conformance rules work at the workspace level. A rule receives the project graph, each project's resolved configuration, and the file structure, and checks them against a policy. That lets you express rules that only make sense across projects. For example:
- Ownership metadata: every project must declare an owning team, so an alert or a breaking change has someone to route to.
- Dependency direction: projects tagged as domain logic must not depend on projects tagged as application shells. This is checked against the graph, not against individual import statements.
- Required project fields: every project must define a
testtarget, a tag set from an approved list, or a specific project type.
A failure here is a governance problem, not a style problem. Conformance is the place where an organization encodes rules that previously lived in a wiki page or in a reviewer's memory.
Conformance ships as part of Nx Powerpack, which is a commercial offering and not part of core open-source Nx. Licensing terms and package names have changed over time, so check the official Nx documentation for the current details before you plan a rollout.
This guide is for platform and backend engineers who run monorepos that several teams share.
LINT (single file) CONFORMANCE (whole workspace)
+--------------+ +---------+ +---------+ +---------+
| one source | | Project | | Project | | File |
| file | | graph | | config | | layout |
+--------------+ +---------+ +---------+ +---------+
| | | |
v v v v
+--------------+ +---------------------------------+
| Parse to AST | | Conformance rules (policy) |
+--------------+ | - every project has an owner |
| | - no domain -> app-shell deps |
v | - required targets and tags |
+--------------+ +---------------------------------+
| Lint rules | |
| unused import| v
| banned API | +---------------------------------+
+--------------+ | Governance violation (workspace)|
| +---------------------------------+
v
+--------------+
| Style/local |
+--------------+
Lint rules check one source file's syntax tree for local problems, while conformance rules check the project graph, project configuration and file layout together to enforce workspace governance policies.
Who should use conformance rules
Conformance is aimed at the people responsible for a shared workspace, not at individual feature work:
- Platform and developer-experience teams that own the monorepo's tooling and CI. They write, scope and maintain the rules.
- Architects and staff engineers who define dependency direction, layering and ownership policy, and want it checked by a machine instead of argued in each review.
- Organizations with many teams in one Nx workspace, where no single reviewer sees every change and drift goes unnoticed until it causes an incident or blocks a deploy.
Product engineers on those teams are the consumers. They see conformance as a CI check with a message telling them what broke and how to fix it. They should not need to read rule source code to get a pull request merged.
If you are a single team in a small repository, or you have not yet adopted tags and project metadata, you are probably not the target user yet. Start with tags and module boundaries and revisit conformance when the workspace grows.
How conformance works
At a high level, a conformance run goes through these stages:
- Nx builds the project graph. The graph comes from project configuration files, package manifests, Nx plugins that infer projects and targets, and analysis of imports between projects. This is the same graph that
nx affectedandnx graphuse. - The conformance command reads the rule list from
nx.json. Each entry names a rule implementation (built-in or a local path), an optional project scope, a status and rule-specific options. - Each rule is resolved and scoped. The implementation is loaded, and the scope filter decides which projects it evaluates.
- Each rule inspects the workspace and returns violations. A rule receives the graph and workspace context, applies its policy, and returns a list of violations, each with a message and the affected project or file.
- Results are aggregated and the status decides the outcome. Violations from report-only rules are shown but do not fail the run. Violations from enforced rules make the command exit non-zero, which fails the CI job.
Two consequences follow from this design. First, conformance results are only as accurate as the project graph, so missing tags or misconfigured project roots show up as false positives or missed violations. Second, because rules see the whole graph, they can catch problems that a per-file linter cannot, such as a dependency direction broken by a change in a different project. The exact command name, option names and status values depend on your Nx version, so confirm them in the documentation for the version you have installed.
Choosing between conformance, module boundaries and code review
Nx gives you three governance tools, and each covers different ground. Tags combined with the module boundary lint rule control which projects may depend on which. Conformance rules check policies that apply across the whole workspace. Code review handles anything that needs human judgment.
| Concern | Best tool | Why |
|---|---|---|
| "Feature libs must not import app code" | Tags + module boundaries | Enforced per import, in the editor and in lint |
| "Every project declares an owner" | Conformance | Checks workspace metadata, not source files |
| "Is this abstraction worth it?" | Code review | Needs context a rule cannot encode |
When to use conformance
Conformance is worth adopting when several teams share one workspace and no single reviewer sees every change. It also helps when the architecture is drifting despite tags, or when project ownership is missing or stale. In each case, the rule records an agreement that people would otherwise forget or argue about again.
Timing matters too. Good moments to introduce it are when a workspace is about to absorb more teams or repositories, after an incident traced to unowned or wrongly coupled code, or when the same review comment about structure keeps recurring across pull requests.
When not to use it
Avoid it in small, single-team repos, where a conversation is cheaper than a rule. Also avoid it for policies that come down to taste, because a rule built on opinion gets suppressed instead of followed. And don't adopt it if no one will own the rules. Unowned rules turn into noise and end up disabled.
Alternatives
- Tags and the module boundary lint rule. The
@nx/enforce-module-boundariesrule from the open-source Nx ESLint plugin enforces dependency constraints between tagged projects. It covers dependency direction but not ownership or required configuration fields. - Custom scripts over the exported graph. If you cannot or will not license Powerpack, export the project graph as JSON (for example with
nx graph --file=graph.json) and write a script that fails CI when it finds a violation. This works, but you give up integrated reporting and consistent output, and you have to maintain the script yourself. - Generators. Nx generators can scaffold new projects with the required tags, targets and owner fields from the start. This prevents new violations but does nothing about existing projects or later edits.
- Code owner files on your Git host. A code owners file routes reviews to the right team based on paths. It assigns reviewers but does not check that metadata in the workspace is present or correct.
- Code review alone. It handles judgment calls well but does not scale as the only line of defence for mechanical policies across many teams.
How rules are configured in nx.json
Conformance keeps its configuration in nx.json, next to the rest of your workspace settings, not in a separate tool config. Rule changes therefore go through the same review as any other change to shared workspace behavior, and the file's history shows who tightened or relaxed a standard and when.
Each entry in the rules list has three parts:
- A reference to a rule implementation. This is either a built-in rule or a path to a custom rule in your repository. The entry only points at the logic, so several entries can use the same implementation with different options.
- A project scope. You can limit a rule to a subset of projects, for example only backend services or only projects with a certain tag. This lets you apply a rule to the area it is meant for without rewriting it.
- A status. One status reports violations without failing the run. Another fails the run when a violation is found. Most rollouts depend on this setting: a new rule starts in report-only mode, you watch what it finds, and you switch it to failing once the backlog is cleared.
The accompanying config example is an illustrative sketch of this structure, not a copy-paste template. The configuration surface may change between releases. Before you commit anything, check the exact option names, rule identifiers and accepted status values against the documentation for the version installed in your workspace.
// nx.json (excerpt). ILLUSTRATIVE SKETCH: verify option names, rule identifiers
// and status values against the docs for your installed Nx version.
{
"conformance": {
"rules": [
{
// Built-in rule, enforced everywhere: violations fail the run
"rule": "@nx/conformance/enforce-project-boundaries",
"status": "enforced"
},
{
// Custom rule from this repo, scoped to backend projects only.
// Report-only while the existing backlog is cleaned up.
"rule": "./tools/conformance/require-health-check",
"projects": ["tag:type:backend"],
"status": "evaluated",
"options": { "endpoint": "/healthz" }
},
{
// Same implementation, different scope and options, already enforced
"rule": "./tools/conformance/require-health-check",
"projects": ["payments-api"],
"status": "enforced",
"options": { "endpoint": "/internal/health" }
}
]
}
}
Illustrative sketch of conformance rules in nx.json, showing a rule reference, a project scope and a report-only versus failing status; confirm exact names and values in your version's docs.
Built-in rules and writing a custom rule
The built-in rules cover policies that most large workspaces end up writing anyway. Broadly, they check project boundaries: which projects may depend on which, often driven by tags. They check ownership: whether every project has a declared owner, so that a violation has someone to route to. They also check structural consistency, such as whether projects follow agreed layouts or metadata conventions. Treat these as categories. The specific rules available depend on your version.
Writing a custom rule
A custom rule is a function with a narrow contract. It receives the project graph and workspace context, such as project configurations, tags and file locations. It returns a list of violations. Each violation carries a human-readable message and identifies the affected project or file, so CI output points at something actionable. The sketch that follows is illustrative. Check the exact types and registration API against your installed version.
The function is the easy part. The real cost is everything around it:
- Tests. Build small fixture graphs that cover both passing and failing cases. Without them, a refactor of the rule silently weakens it.
- False positives. Each one teaches engineers to ignore the rule, which erodes trust in the whole rule set.
- Upgrade breakage. Custom rules depend on Nx's internal graph shape and plugin APIs, and either can change between releases. Someone has to own the rule and re-verify it on every upgrade.
Accuracy limits
Graph-based checks are only as good as the graph. Dynamic imports with computed paths may never appear as edges, so real violations go unnoticed. Generated code can introduce dependencies that nobody wrote by hand. A misconfigured project, such as one with a wrong root or missing tags, produces false alarms or hides real problems. Fix the graph before you blame the rule.
// Illustrative sketch: the rule logic only. Wrap it in whatever
// registration API your installed Nx version provides.
import type { ProjectGraph } from '@nx/devkit';
interface Violation {
message: string;
sourceProject: string; // the project CI output should point at
}
// Policy: shared libraries must not depend on feature projects.
export function sharedMustNotDependOnFeature(graph: ProjectGraph): Violation[] {
const violations: Violation[] = [];
const tagsOf = (name: string) => graph.nodes[name]?.data.tags ?? [];
for (const [source, deps] of Object.entries(graph.dependencies)) {
if (!tagsOf(source).includes('scope:shared')) continue;
for (const dep of deps) {
// External packages (npm:...) are not in graph.nodes, so they are skipped.
if (!graph.nodes[dep.target]) continue;
if (tagsOf(dep.target).includes('scope:feature')) {
violations.push({
message: `${source} (scope:shared) depends on ${dep.target} (scope:feature).`,
sourceProject: source,
});
}
}
}
// Accuracy limit: only edges present in the graph are checked. A dynamic
// import with a computed path, or a project with missing tags, slips through.
return violations;
}
An illustrative custom rule reads the project graph, compares tags on each dependency edge, and returns violations that each include a message and the affected project. The graph types come from @nx/devkit, and the rule registration API is left out because it varies by version.
Picking a first rule set and rolling it out in phases
Once the graph is trustworthy, pick two or three policies that a machine can judge without debate: every project declares an owning team, or no application imports another application's internals. Each produces a yes/no answer and maps to a real cost, such as unowned code that nobody patches or coupling that blocks independent deploys. Leave style preferences like naming conventions or folder layout to formatters and linters. Encoding taste as governance invites arguments about the rule instead of fixes.
Roll out in four steps:
- Run the rules in report-only mode so violations are visible but nothing fails.
- Record the existing violation count per project and per team.
- Assign cleanup to the owning teams, using the tags and ownership metadata you already have.
- Switch to enforced once the backlog is near zero, or exempt the remainder explicitly.
Illustrative estimate: assume 300 violations across 6 teams, with each team fixing 10 per week alongside normal work. An even split would take about 5 weeks. If one team owns 150 violations, that team needs 15 weeks. The largest single backlog sets the enforcement date, not the total. You can either help that team or enforce for the other teams first.
| Approach | Effect on unrelated PRs | Team reaction |
|---|---|---|
| Big-bang enforcement | Fail on violations that predate the PR | Resentment, pressure to disable the rule |
| Phased rollout | Unaffected until enforcement | Cleanup planned like any other work |
Big-bang enforcement fails an engineer's one-line fix because of a violation they never wrote. That trains people to see conformance as an obstacle and pushes them to bypass it.
Running conformance in CI and handling exceptions
Conformance should run on pull requests as its own CI step, next to lint, test and build, so a violation shows up as a named failing check and not inside a lint log.
Every change or only relevant ones
Running conformance on every pull request is simplest and catches violations that cut across projects, such as a dependency rule broken by a change elsewhere. Running it only on affected projects is cheaper, but a rule that inspects the whole workspace can miss problems when it sees only part of it. A reasonable default is to run cheap, workspace-wide rules on every change and scope expensive rules to affected projects.
Illustrative sketch: after dependencies are installed, add a step that runs nx conformance:check and fails the job on violations. Confirm the exact command and its options in the Nx documentation for your version.
Actionable failures
Every failure message should state three things: what rule was broken and where, which team owns the rule, and how to fix it, either with a concrete change or a link to internal guidance. If a message only says "violation in libs/payments", the engineer has to go find the rule's author, and the pull request stalls.
Exceptions
Exclude specific projects from a rule; do not disable the rule outright. Next to each exclusion, record why it exists, which team owns it and when it will be reviewed. Review the list every quarter. Without a review date, an exclusion added as a temporary workaround tends to stay in place for good.
#!/usr/bin/env bash
set -euo pipefail
BASE_SHA="${BASE_SHA:-origin/main}"
HEAD_SHA="${HEAD_SHA:-HEAD}"
npm ci
npx nx affected -t lint test build --base="$BASE_SHA" --head="$HEAD_SHA"
echo "--- conformance ---"
if ! npx nx conformance:check; then
# Point engineers to ownership and fix guidance, so the PR does not stall.
echo "Conformance failed. Each rule's owner and fix guidance are listed in" >&2
echo "this repository's conformance documentation." >&2
exit 1
fi
An illustrative, vendor-neutral CI step that runs lint, test and build on affected projects, then runs conformance workspace-wide as its own failing check with a pointer to rule owners; verify the conformance command against the Nx documentation for your version.
Ownership of the rules, cost and engineering time
Treat the rule set as a product with an owner. A platform team maintains it, and every rule change goes through a pull request with the same review as code, because a new rule can break every team's pipeline at once. Give teams a documented channel, such as an issue template or a standing agenda slot, to propose a rule, contest one, or ask for one to be retired. A rule that nobody can challenge loses legitimacy quickly.
Costs
- License and vendor exposure. Conformance ships with Nx's commercial tier (Powerpack), not the open-source core. Your enforcement layer now depends on a license renewal and on the vendor's roadmap. Check the current terms before you build on it.
- CI time. Each run adds a step to the pipeline. Measure it the same way you measure any other check.
- Cognitive load. Engineers must learn what each rule means and why it exists.
- Custom rule maintenance. Custom rules are code. They need tests, an owner, and updates when Nx internals change.
Savings
Architecture debates move out of individual reviews and into one rule discussion. Boundary and ownership drift surfaces at commit time instead of during an incident. Violations are tied to project owners, so each fix goes to the right team.
Limits
- Rule sprawl. Too many rules teach engineers to request exceptions by reflex.
- Meaningless metadata values. Metadata rules check that a field exists, not that its value is correct. An owner field set to a generic value such as
unassignedpasses unless the rule also checks the value against a list of real teams. - Platform bottleneck. Central ownership can make the platform team a queue that every change waits in.
- Autonomy versus architectural integrity. Conformance does not settle this tension. It only enforces whatever balance your organization has already agreed on through its own process.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to nx conformance rules, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.