What Nx self-healing CI does and where it sits in the pipeline
Nx self-healing CI is a fix-proposal step that runs after a CI task has already failed. When a task such as build, test or lint exits non-zero, Nx Cloud collects four things: the failing task, its terminal output, the projects affected by the change, and the source files relevant to that task. It passes this context to an AI model, which drafts a code change. The proposal is then shown to a developer, who accepts or rejects it. Nothing is committed to the branch without that decision.
The value comes from how narrow that context is. A generic bot that reads CI logs sees a stream of text and has to infer which package broke, which command produced the error and which files might be involved. Nx already knows all of this. The project graph records which project the task belongs to and what that project depends on. The task model identifies the exact target that ran, its configuration and its declared inputs. So the fix generator starts with a bounded problem, for example "the test target of orders-service failed on these files," and does not have to search the whole repository for a cause.
Treat the feature as a reviewer's assistant. It drafts fixes but does not repair anything autonomously. It shortens the path from a red pipeline to a candidate patch. Deciding whether that patch is correct, and whether it addresses the real cause rather than the symptom, remains the engineer's job.
+--------------------+ +-------------------------------+
| CI task failed | | Nx project graph + task model |
| orders-service | | project: orders-service |
| target: test | | target: test |
| exit code non-zero | | deps + declared inputs |
+---------+----------+ +---------------+---------------+
| |
v v
+--------------------------------------------------------+
| Nx Cloud collects failure context |
| 1. failing task 3. affected projects |
| 2. terminal output 4. relevant source files |
+---------------------------+----------------------------+
| bounded context
v
+--------------------------------------------------------+
| AI model drafts a code change (proposal only) |
+---------------------------+----------------------------+
|
v
+--------------------------------------------------------+
| Developer reviews the proposed diff |
+-------------+------------------------------+-----------+
| |
accept reject
v v
+---------------------------+ +---------------------------+
| Change applied to branch | | Nothing committed; failure|
| by the developer | | investigated manually |
+---------------------------+ +---------------------------+
After a CI task fails, Nx Cloud combines the failure with project-graph and task-model context to draft a fix, which reaches the branch only if a developer accepts it.
How it works, step by step
The mechanism follows the order of the diagram above:
- CI runs tasks through Nx. Your pipeline runs targets (for example
nx affected -t lint test build) with the workspace connected to Nx Cloud. Nx works out which projects the change affects from the project graph, and runs the targets for those projects, optionally spread across machines with Nx Agents. - A task fails. A specific target on a specific project exits non-zero. Nx records which task it was and captures that task's terminal output.
- Context is assembled. Nx Cloud combines the failing task's identity, its log, the affected projects and the source files relevant to that task. The project graph and the target's declared inputs are what make this context bounded, so the model is not handed the whole repository.
- A fix is drafted. The context goes to a hosted AI model, which proposes a diff aimed at making that task pass.
- The proposal is checked and surfaced. The failing task should be re-run against the proposed change, so the reviewer sees whether it actually turns green. The section on verification below explains why a green re-run is evidence of credibility, not of correctness.
- A human decides. The developer reviews the diff and accepts or rejects it. Accepted changes land on the branch and go through normal CI and code review. Rejected ones leave the branch untouched, and the failure is handled by hand.
The quality of each step depends on the one before it. If the task's declared inputs are incomplete or its log does not show the real cause, the model receives misleading context and proposes a fix for the wrong problem. That is why the prerequisites section focuses on deterministic tasks and reliable logs.
Who should use it
The feature fits a specific kind of team:
- Backend teams already on an Nx monorepo connected to Nx Cloud. The feature relies on the Nx project graph and task model. If your CI does not run tasks through Nx, there is no bounded context to draw from.
- Teams with a steady volume of mechanical failures. Shared TypeScript libraries, many services importing from them, and frequent refactors produce a stream of type errors, broken imports and lint violations that are tedious to fix but easy to verify. That is where the time savings come from.
- Teams whose tasks are reasonably hermetic. If targets declare their inputs and fail the same way on every agent, the log a model sees reflects the real cause.
- Teams with review discipline. Self-healing assumes a reviewer who reads the diff critically. Teams that already enforce code owners and required reviews can absorb AI suggestions into that process without weakening it.
- Organizations whose policy allows source and logs to be sent to a hosted AI service. If that is not permitted, the feature is not an option regardless of how useful it would be.
It is a weaker fit for small repositories where failures are rare and quick to fix by hand, for teams whose CI failures are mostly environmental or infrastructure problems, and for codebases under contracts or regulation that keep code inside the organization's boundary.
Which backend failures it should fix and which it should leave alone
That judgment is cheap when the failure is local to the code and the fix can be checked without anything outside the repository. It gets expensive when the failure comes from the world around the code. Self-healing is useful in the first case and risky in the second.
| Failure type | Verifiable locally? | Risk of a plausible but wrong fix |
|---|---|---|
| Lint and formatting violations | Yes | Very low |
| Type errors after a shared library interface change | Yes | Low; the patch may silence the error with a cast instead of updating callers |
| Broken imports after refactors | Yes | Low |
| Outdated snapshots or fixtures | Yes | Medium; regenerating can accept a real regression as the new baseline |
| Simple assertion drift | Yes | Medium; changing the assertion may hide a behaviour bug |
| Flaky integration tests | No | High; retries or loosened waits hide the race |
| Database and migration failures | No | High; schema edits can diverge from production state |
| Environment and secret issues | No | High; defaults or stubs paper over missing config |
| Infrastructure timeouts | No | High; longer timeouts mask capacity problems |
| Cross-service contract and runtime config | No | High; one side gets patched to match a broken peer |
In the first group, the task graph contains everything needed to prove the fix: type-check, lint, and unit tests either pass or fail on the patched code. In the second group, the cause sits in a database, a network, a secret store, or another team's service. A code change that turns the pipeline green there usually moves the failure somewhere harder to see. Leave those to the owner of the underlying system.
When to use it
Use self-healing when all of the following hold:
- The failure is in the first group of the table above. Lint, formatting, type errors after an interface change and broken imports are the clearest cases. A correct fix can be proven by re-running the same task.
- The failing task is deterministic. It declares its inputs, does not depend on network or shared machine state, and fails the same way on every agent.
- The failure happens on a feature branch or pull request. A proposed fix there goes through the normal review and CI path before it reaches a protected branch.
- The fix is local to the repository. The change needed is in source the model can see, not in a database, a secret store or another team's service.
- Your measured acceptance rate for that target is above break-even. The cost estimate below shows how to work that out with your own numbers.
A typical good moment is just after a shared library's interface changes. Many downstream projects fail type-check in predictable ways, and each fix is small and easy to judge.
When to avoid it
Do not rely on self-healing, or turn it off for the target, in these situations:
- The failure comes from outside the code. Flaky integration tests, migration failures, missing environment variables or secrets, infrastructure timeouts and cross-service contract breaks all belong to the second group. A green pipeline there usually means the symptom was hidden.
- The task is non-hermetic. If it reads undeclared environment variables, depends on files left by another task, or calls live services, the log the model sees may not contain the real cause.
- The code is security- or data-sensitive. Authentication, authorization, payments and database migrations should be fixed by their code owners, not by accepting a plausible diff.
- Policy forbids sending code or logs off the CI environment. This rules the feature out entirely, not per target.
- Logs may contain secrets that are not masked. Fix masking first.
- Acceptance on a target is below break-even, or reverts are appearing. Narrow the scope instead of pushing reviewers to try harder.
- A deterministic tool already handles the case. Formatters,
lint --fixand codemods produce the same change every time and are easier to audit.
Verification: why a passing re-run is necessary but not sufficient
A suggested fix is only worth reviewing if the task that failed has been re-run against it. Without that, you are reading a plausible-looking diff with no evidence behind it. So treat the re-run as the minimum bar for credibility, then remember that it only shows the task now exits zero. It does not show the change is correct.
The gap matters most in typed backend code, where the shortest path to green is often to weaken the check rather than fix the cause. In the example below, an OrderService test fails after a currency field is added to a response. One candidate fix casts the result as any and loosens toEqual to toMatchObject. The test passes, and the contract drift goes straight to production. The correct fix adds currency to the shared OrderDto interface, so every consumer is type-checked against the new shape. Both diffs pass the re-run. Only one of them is right.
Reviewer checklist
- Does the diff add
any,as unknown as, non-null assertions (!), or@ts-ignore/@ts-expect-error? - Was an assertion loosened, for example exact match to partial match,
toBetotoBeTruthy, or a removedexpect? - Were tests skipped, deleted, or had their timeouts raised?
- Did the change touch the test instead of the code under test, or the reverse, without a stated reason?
- Does the fix land in the shared type or module where the failure actually started?
The model's output is also non-deterministic. The same failure can produce a correct fix on one run and a suppression on the next. Review each suggestion on its own merits, and don't assume a pattern you approved once will come back the same way.
// shared/order.dto.ts: the contract every consumer compiles against
export interface OrderDto {
id: string;
totalCents: number;
currency: 'USD' | 'EUR'; // CORRECT FIX: add the new field to the shared type
}
// order.service.ts
export class OrderService {
getOrder(id: string): OrderDto {
// SUPPRESSION FIX (reject): leave OrderDto unchanged and silence the compiler
// return { id, totalCents: 4200, currency: 'USD' } as any;
return { id, totalCents: 4200, currency: 'USD' };
}
}
// order.service.spec.ts
describe('OrderService.getOrder', () => {
// SUPPRESSION FIX (reject): cast plus partial match. The re-run passes,
// but the test no longer checks the response shape.
// const order = new OrderService().getOrder('ord_123') as any;
// expect(order).toMatchObject({ id: 'ord_123' });
it('returns the full order contract', () => {
// CORRECT FIX: keep the type annotation and the exact match,
// and assert the new field explicitly.
const order: OrderDto = new OrderService().getOrder('ord_123');
expect(order).toEqual({
id: 'ord_123',
totalCents: 4200,
currency: 'USD',
});
});
});
Two fixes that both pass the re-run for an OrderService test that broke when currency was added: the commented-out suppression casts to any and loosens toEqual to toMatchObject, while the correct fix adds currency to the shared OrderDto interface and keeps the exact assertion.
Prerequisites: deterministic tasks, Nx Agents and reliable logs
Self-healing works from what the failing task left behind. With Nx Agents, the tasks in one CI run are spread across several machines. A failure therefore appears as a single task's terminal output, tied to the agent that ran it. Nothing in that output shows the full workspace state. The fix generator sees that task's log and the relevant source. It cannot see what else happened on the same machine, so the log has to contain the real cause.
Non-hermetic tasks break that assumption. Three common patterns:
- Hidden environment dependency. An integration test reads
DATABASE_URLwithout declaring it, and fails on an agent where it is unset. The suggested fix is likely a default value or a skipped assertion, not a configuration change. - Shared state. A migration task leaves files that a later task depends on, but only when both run on the same agent. The error appears in the wrong project, so the fix lands there too.
- Network calls. A timeout against a live service looks like a code defect. A suggestion that adds retries or raises the timeout only hides the problem.
The fix is to make each task describe what it depends on. In the project configuration, give each target explicit inputs:
- source files under
{projectRoot} - shared config under
{workspaceRoot} - an
enventry for each variable that changes behavior
Then list the target's outputs so cached results can be restored exactly. A task that hits the cache on unchanged inputs and fails the same way on every agent is a task whose failures are worth trying to fix automatically.
{
"name": "orders-api",
"targets": {
"build": {
"executor": "nx:run-commands",
"options": {
"command": "tsc -p apps/orders-api/tsconfig.app.json"
},
"inputs": [
"{projectRoot}/src/**/*",
"{projectRoot}/tsconfig.app.json",
"{workspaceRoot}/tsconfig.base.json"
],
"outputs": ["{workspaceRoot}/dist/apps/orders-api"]
},
"integration-test": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"command": "jest -c apps/orders-api/jest.integration.config.ts"
},
"inputs": [
"{projectRoot}/src/**/*",
"{projectRoot}/test/**/*",
"{projectRoot}/jest.integration.config.ts",
"{workspaceRoot}/jest.preset.js",
{ "env": "DATABASE_URL" }
],
"outputs": ["{workspaceRoot}/coverage/apps/orders-api/integration"]
}
}
}
A project.json for a backend service that declares source files, shared workspace config and the DATABASE_URL variable as inputs, and lists outputs (which must match the tool's output paths), so each task is cacheable and fails the same way on every agent.
How to roll it out without creating review noise
Start where a wrong fix costs least: lint, format and typecheck targets. Their failures are local, their fixes are small, and a reviewer can judge a diff in seconds. Leave test, build and migration targets out of scope until you have data on how the tool performs.
Keep human approval mandatory, and leave branch protection, required reviewers and CODEOWNERS exactly as they are. Treat the AI as one more PR contributor. Its commits pass the same checks and the same review as anyone else's. It should never get bypass rights or a service identity that can push to protected branches. If a suggestion needs special permissions to land, that is a reason to reject it.
What to track
For each suggestion, record four things:
- Made: a suggestion was offered.
- Accepted: a reviewer applied it.
- Rejected: a reviewer declined it.
- Reverted later: an accepted fix was undone after merge.
Reverts are the signal that matters most. A fix that was accepted and later reverted means review didn't catch the problem. Before adding a new target type, set thresholds as a team: a minimum number of suggestions, an acceptance rate you agree is useful, and close to zero reverts over a fixed window.
Review fatigue and ownership
Frequent low-quality suggestions teach people to either rubber-stamp them or ignore them, and both defeat the purpose. If the rejection rate climbs on a target, narrow the scope instead of asking reviewers to try harder.
The engineer who accepts a fix owns it, the same as code they wrote themselves. Keep the AI's authorship visible in commit metadata, for example with a co-author trailer, so git blame and incident reviews show where the change came from.
Impact on cost and engineering time: an illustrative estimate
Illustrative estimate (assumed numbers, not a benchmark)
Assumed inputs for a mid-sized backend monorepo:
- F: 40 CI failures per week
- P: 30% fall into auto-fixable classes (lint, formatting, snapshot drift, type errors), so 12 suggestions per week
- M: 20 minutes saved per accepted fix (the investigate-patch-push loop an engineer skips)
- R: 6 minutes of review per suggestion, whether it is accepted or rejected
- A: acceptance rate
Weekly net time saved = F × P × (A × M − R)
Review cost applies to every suggestion. Savings apply only to accepted ones. This is why acceptance rate determines whether the feature helps at all.
| Scenario | Acceptance (A) | Saved per suggestion (A × M) | Net per suggestion | Net per week (12 suggestions) |
|---|---|---|---|---|
| High acceptance | 70% | 14 min | +8 min | +96 min |
| Break-even | 30% | 6 min | 0 min | 0 min |
| Low acceptance | 20% | 4 min | −2 min | −24 min |
Break-even acceptance is R ÷ M, which is 30% with these inputs. If review takes longer, or the fixes are trivial and save less time, the threshold rises.
The model also understates the cost of low acceptance. A rejected suggestion does not only waste the review minutes. The engineer still has to fix the failure by hand, often after switching context twice.
None of this includes the price of the hosted service. Put the subscription or usage cost on the cost side before deciding whether a positive time figure justifies the spend.
Security, data handling, vendor coupling and alternatives
When self-healing runs, the failing task's source context, logs and stack traces leave your CI environment for a hosted AI service. Backend logs often carry connection strings, tokens echoed by misconfigured scripts, and customer identifiers from test fixtures. Mask secrets at the CI level before enabling the feature, because anything printed can be sent along. Confirm that your organization's policy allows code to leave its boundary at all. Some contracts and regulated codebases forbid it, and opting in can't be undone after the fact.
Restrict who may approve AI-authored changes to authentication, payments and database migrations. A plausible fix that relaxes a permission check or reorders a migration can pass every test and still be wrong. Code owners on those paths should be the only approvers.
The feature also ties you to Nx Cloud. If the service is degraded, suggestions stop arriving, so engineers must still be able to triage failures without them. Pricing changes also flow straight into your cost estimate.
| Approach | Best for | Predictability | Auditability |
|---|---|---|---|
Formatters, lint --fix in CI or pre-commit |
Style and mechanical lint violations | Same input, same output | Rule-based, easy to review |
| Codemods, Nx generators | Planned refactors across many projects | Scripted and repeatable | The script itself is reviewable |
| Stricter pre-push checks | Catching type and test errors before CI | High | Runs locally, no code leaves the machine |
| Human triage | Novel, cross-service or security-sensitive failures | Varies by engineer | Reasoning is recorded in the review |
Deterministic tools beat AI suggestions whenever the fix is mechanical, because they produce the same change on every run and explain it through a rule.
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 self-healing ci, 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.