What Nx conformance checks that lint cannot
A linter works one file at a time. ESLint parses a source file, walks its syntax tree, and reports problems in that file. That suits questions like "is this promise awaited?" or "is this import allowed?" It does not suit questions about how the workspace as a whole is organized, because no single file contains the answer.
Nx conformance runs against the whole workspace. A conformance rule can read the project graph, the resolved configuration of every project, and the file layout on disk. That lets it check things that only exist at workspace level:
- Every project has an owner. Ownership is a property of the project, not of any source file.
- No library depends on an app. The project graph can record dependencies that do not come from an import statement, and conformance evaluates the graph directly.
- Every service defines the same required targets. For example,
build,testandcontainermight be required. This is a check on project configuration, and lint never parses that.
| Aspect | Lint | Nx conformance |
|---|---|---|
| Unit of analysis | A single file | The whole workspace |
| Input | Source code (AST) | Project graph, project config, file layout |
| Typical question | "Is this code correct?" | "Is this workspace structured the way we agreed?" |
| Typical owner | Feature team | Platform team |
This is why teams use conformance for monorepo governance. Agreements such as "services expose a health-check target" or "every library is tagged with a domain" can be written down as checks that run automatically. Without that, the agreement depends on code reviewers remembering it.
The benefit is largest for platform and backend teams running large Nx workspaces with many contributing teams. In those repositories, standards drift faster than any reviewer can track.
Where conformance fits in Nx Powerpack and what it costs
Conformance is not part of open-source Nx. It ships in Nx Powerpack, the commercially licensed set of add-ons, so adopting it needs a budget line and procurement approval as well as a technical decision. Package names, setup steps and licensing terms have changed over time, and eligibility rules (for example, for small teams or open-source projects) may differ from what you expect. Check the current Nx documentation and license terms before you plan a rollout.
The lock-in cost
The license fee is the visible cost. The larger long-term cost is coupling. Custom conformance rules are written against Nx-specific APIs: the project graph, project configuration and the conformance rule interface. That knowledge is encoded in Nx's model of a workspace. If you later move to a different build system, those rules do not carry over. You would have to rewrite them as scripts, CI checks or another tool's plugins. Plan for this by keeping the policy behind each rule documented separately from its implementation. A rule like "every service project defines a health-check target" should be written down somewhere other than the rule's source file.
When it pays off, and when to skip it
Conformance is worth paying for when you have many teams, many projects and standards that span projects, and a platform team that can't review every change.
Skip it in these cases:
- Small workspaces: a reviewer can still hold the standards in their head.
- Few cross-project standards: there is little for a rule to check.
- Standards already covered elsewhere: the module boundary lint rule handles dependency constraints, and generators create projects correctly from the start.
In those cases, a license adds cost and coupling without catching much that you don't already catch.
How rules are configured, scoped and run
Conformance rules live in workspace configuration (in current releases, a conformance section of nx.json), not in each project. That placement matters. A rule describes a property of the workspace, so it is declared once and the project graph decides where it applies. Each rule entry has three parts:
Illustrative config shape (annotated, not copy-paste ready):
rule: the rule's identifier. This is either a built-in rule from the conformance plugin or a path to a local rule file.options: rule-specific settings, such as a list of required targets or an allowed set of tags. These options let one rule implementation serve several policies.projects: which projects the rule evaluates. Matchers can be project names, globs or tag patterns such astag:type:service. Negations narrow the set further.
Scoping by tag is usually the right default. Tags are already how teams classify projects for module boundaries, so a rule like "every service must expose a serve and docker-build target" can follow type:service without anyone maintaining a project list.
Rules run through a dedicated command, not through lint. nx conformance evaluates and reports locally. A check variant, nx conformance:check, is intended for CI: it exits non-zero on enforced violations. Confirm both command names and every config key against the docs for your installed Nx version, because this surface has changed between releases.
nx.json (conformance.rules) ─┐
├─► resolve project matchers ─► run each rule
project graph (tags, targets)┘ (tags, names) (with options)
│
▼
violations per project + rule
│
▼
exit 0 (pass) / non-zero (fail)
// ILLUSTRATIVE nx.json excerpt (JSONC-style comments for annotation).
// Remove comments before use, and confirm keys and rule names
// against the Nx docs for your installed version.
{
"conformance": {
"rules": [
{
// A local rule file. The path is a placeholder for your own rule.
"rule": "./tools/conformance/require-service-targets.ts",
// Options let one rule implementation serve several policies.
"options": {
"requiredTargets": ["serve", "docker-build"]
},
// Scope by tag so new services are covered automatically.
// The negated entry excludes one project by name.
"projects": ["tag:type:service", "!legacy-billing-service"]
}
]
}
}
// Local run: evaluate and report.
// npx nx conformance
// CI run: exits non-zero on enforced violations.
// npx nx conformance:check
An annotated, illustrative conformance entry in nx.json: one rule with options, scoped by tag with a negated project exclusion. It also shows the local and CI commands. Verify the key and command names for your Nx version.
Rolling out rules in stages with rule status and exemptions
Every new rule should start in report-only mode. Nx conformance gives each rule a status: an evaluated rule reports violations without failing the run, and an enforced rule turns violations into a non-zero exit. Check the exact status names your Nx version accepts. Evaluated mode lets you measure the backlog first: how many projects violate the rule, which teams own them, and whether the rule's assumptions are wrong.
Enforcing too early punishes the wrong people. A team opens a two-line PR and the check fails on a violation that predates their work. They then file blanket exemptions or route around the check, and the rule loses credibility before it has prevented anything.
| Stage | Rule status | Effect on CI | Exit criterion |
|---|---|---|---|
| Measure | evaluated | Reports only | Backlog counted and assigned to owners |
| Burn down | evaluated | Reports only | Each violation fixed or exempted |
| Enforce | enforced | Fails on new violations | Ongoing |
Moving a rule to the enforce stage is a one-field change, for example changing "status": "evaluated" to "status": "enforced" (illustrative). Make that change in its own PR so the decision is visible and easy to revert.
Exemptions should be specific and reviewable. Each entry names the project, gives a reason, and records an owner. An illustrative entry: { "project": "billing-legacy", "reason": "migrating to shared build target", "owner": "payments-platform" }. Check how your Nx version supports per-project exclusions before you settle on a format.
Review exemptions on a fixed schedule. A stale exemption keeps the rule green while the standard it protects erodes. Remove the exemption, or renew it with a new reason.
{
"conformance": {
"rules": [
{
"rule": "@nx/conformance/enforce-project-boundaries",
"status": "enforced",
"projects": [
"*",
{
"matcher": "!billing-legacy",
"explanation": "Owner: payments-platform. Reason: migrating to shared build target. Review by: 2027-01-31."
}
]
}
]
}
}
Illustrative nx.json fragment showing a rule moved from "evaluated" to "enforced", with one project excluded and the owner, reason and review date recorded in the exclusion's explanation; check the status values and project-matcher format your Nx version accepts before you copy it.
Writing a custom rule: required targets for service projects
Start with the decision the rule encodes. Every deployable service must expose a health-check target, so smoke tests and readiness probes run the same way everywhere. It must also expose a container-build target, so the release pipeline can build an image without knowing anything about the project. Lint cannot enforce this. The requirement lives in project configuration and tags, not in source files.
The rule's implementation receives a context object describing the workspace. Through it you get the project graph and each project's resolved configuration, including targets inferred by plugins and not only those written in project.json. That distinction matters. A rule that parses project.json directly will report false violations for targets that a plugin adds.
The rule then works in three steps:
- Filter graph nodes to projects whose tags include your service tag, for example
type:service. - For each one, check that both target names exist in its configuration.
- Return a violation for every missing target, not one per project, so a single run surfaces all the work.
Make each message actionable. Name the project, name the missing target, and state the fix. For example: "Project billing-api is tagged type:service but has no container-build target. Add one, or apply the shared container plugin." Messages like this let a developer resolve the violation without opening the rule's source.
The exact factory function, context fields and violation shape have changed as Powerpack has evolved. Verify them against the current documentation before you copy any example, including the one that follows.
Budget for maintenance from the start. A custom rule is code that depends on Nx internals. It needs unit tests against fixture graphs, and it needs revalidation on every Nx upgrade. Ten rules written by three people who have since moved teams become an internal framework that blocks upgrades and that nobody wants to own. Keep rules few and small, and give each one a named owner.
// Verify the import path, factory and result shape against current Nx docs before use.
import { createConformanceRule } from '@nx/conformance';
const SERVICE_TAG = 'type:service';
const REQUIRED_TARGETS = ['health-check', 'container-build'];
export default createConformanceRule({
name: 'service-required-targets',
category: 'consistency',
description: 'Service projects must define health-check and container-build targets',
implementation: async ({ projectGraph }) => {
const violations = [];
for (const node of Object.values(projectGraph.nodes)) {
// Use resolved config from the graph so plugin-inferred targets count.
const tags = node.data.tags ?? [];
if (!tags.includes(SERVICE_TAG)) continue;
const targets = node.data.targets ?? {};
// One violation per missing target, so a single run shows all the work.
for (const target of REQUIRED_TARGETS) {
if (!targets[target]) {
violations.push({
sourceProject: node.name,
message:
`Project "${node.name}" is tagged "${SERVICE_TAG}" but has no ` +
`"${target}" target. Add one, or apply the shared plugin that provides it.`,
});
}
}
}
return { severity: 'high', details: { violations } };
},
});
A custom conformance rule that checks every type:service project for health-check and container-build targets and reports each missing target with a fix; verify the API shape against current docs.
Running conformance in CI and across many workspaces
Run conformance as its own CI step, not folded into lint or tests. When a pipeline fails, the step name tells the developer what kind of problem they have. A failing conformance check means the workspace violates an agreed standard, which is a different conversation from a broken unit test or a style warning. A separate step also lets you gate merges on it independently and track how often it fails.
Most rules need the whole workspace, not only affected projects. A rule about project boundaries, ownership metadata or required targets can be broken by a change elsewhere, such as a new dependency edge or a renamed tag. Evaluating only affected projects misses those violations. In very large monorepos, whole-workspace evaluation can add noticeable time. Measure it in your own pipeline before deciding whether to run it on every push or only before merge.
Keep failure output readable. Each violation should name the project, the file or setting at fault, the rule that flagged it, and what to change. Put a link to the rule's documentation in the rule's own message. Developers should not have to search your wiki to find out why their build is red.
Across many repositories, publish rules as a versioned internal package that each workspace pins and upgrades on purpose. Announce changes with a short changelog. Ship every new or tightened rule in report-only mode first, then switch it to enforcing after repositories have had time to fix their violations. A single release should never break dozens of pipelines at once.
#!/usr/bin/env bash
# CI script: each stage runs separately, so a red build names the kind of failure.
set -euo pipefail
BASE_REF="${BASE_REF:-origin/main}"
echo "::: stage: lint"
# Lint and tests can be limited to affected projects.
npx nx affected -t lint --base="$BASE_REF"
echo "::: stage: test"
npx nx affected -t test --base="$BASE_REF"
echo "::: stage: conformance"
# Conformance checks the whole workspace, not only affected projects.
# A new dependency edge or a renamed tag in one project can break a
# boundary or ownership rule that applies somewhere else.
# 'conformance:check' only reports. It does not apply fixes, and it
# fails only on rules whose status is "enforced" in nx.json.
# Rules set to "evaluated" (report-only) show their violations but do
# not fail the build. Ship new or tightened shared rules that way first.
start=$SECONDS
status=0
npx nx conformance:check || status=$?
elapsed=$((SECONDS - start))
# Log the duration so the team can measure the whole-workspace cost in
# this pipeline. Use that number to choose between running on every push
# and running only before merge.
echo "conformance duration: ${elapsed}s"
if [ "$status" -ne 0 ]; then
echo "Conformance failed: this workspace violates an agreed standard."
echo "Each violation above names the rule and the project at fault."
exit "$status"
fi
A CI script that runs lint and tests on affected projects, then runs conformance on the whole workspace as a separate stage with its own failure message and a logged duration.
Alternatives, overlap and deciding where a standard belongs
Conformance is one of several places a standard can live. Putting the same rule in two of them gives developers two failures with different messages for one problem. Pick one owner per standard.
| Mechanism | Acts on | Prevents or detects | Best for |
|---|---|---|---|
| Lint rules | Source files | Detects, in the editor | Code-level patterns |
| Module boundary rules | Project imports | Detects | Dependency direction by tag |
| Generators and templates | New projects | Prevents | Making the right structure the default |
| Conformance | Workspace and project config | Detects drift | Standards that span files or projects |
| Code review | Changes | Detects, inconsistently | Judgment calls |
| Documentation | People | Neither | Explaining why |
Conformance detects drift but does not prevent it. A generator gets a project right on day one, and a conformance rule catches what changes later.
Decision checklist
- Is the check visible in one source file? Use a lint rule.
- Is it about which projects may import which? Use module boundary rules.
- Is it about how a project starts? Use a generator, then add a conformance rule only if the setup can drift.
- Does it span configuration across projects? Use conformance.
- Does it need judgment? Leave it to review and document the reasoning.
When each rule is paired with a generator, fixing a violation means rerunning a known command rather than doing manual work.
Illustrative estimate. Assume these inputs: 40 merged changes a week, 1 in 4 with a structural review comment, 10 minutes per comment round-trip. That totals about 100 minutes a week. Against this, assume 2 hours a month to maintain the rules. Under these assumptions, review time saved outweighs maintenance cost by roughly two to one.
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, 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.