AnalogJS has been an Nx monorepo since its very first commit, back in July 2022 — so for nearly four years the build tool has known the full dependency graph between every package and app. CI, though, never used any of that. It was six GitHub Actions jobs running mostly in series, each one checking out the repo and re-installing the world before it could do any real work. The workflow had grown into that job-per-concern shape over those years, and once it worked, nobody had a reason to revisit it.
I'd been meaning to move it onto Nx Agents for a while. What finally made me do it was getting a little shamed by a post from Altan Stalker — a pointed argument that CI pipelines exactly like mine reinvent a build graph the tool already has. Mine did, so I tried something: hand the whole migration to my coding agent from a single prompt and see how far careful analysis — not clever YAML — would get.
The short version is that it got the whole way. The more interesting part was watching why it made the choices it did.
The starting point
The old ci.yml had a job per concern — prettier, lint, build, build-windows, unit, and e2e. Every Linux job opened with the same boilerplate:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: corepack enable
- run: pnpm install --frozen-lockfile --prefer-offline
Then e2e was hand-gated behind build with needs: build. That needs: edge is the heart of what Altan calls out: CI was re-encoding a dependency graph the build tool already knows. And stage gating has a quieter cost — when build fails, e2e is skipped, so you don't even find out whether e2e would have passed until the next push. Six runners, duplicated setup, serial --parallel=1 steps, and dead time between the stages.
One prompt
The prompt I gave my coding agent was deliberately about behavior, not YAML: analyze the existing CI and produce a correct, conservative Nx Agents setup based on the pipeline's real commands, dependencies, secrets, and execution behavior. Then I let it do the analysis itself. A few things it did stood out, because they're the things I'd want a careful human to do:
- It read the real pipeline, not an idealized one —
ci.yml, thepackage.jsonscripts each job actually invoked,nx.jsontargetDefaults,.node-version, and the package-manager pin. - It generated the Nx task graph (
nx run-many -t build test e2e lint --graph) and inspected the edges to prove thattestande2ealready depend onbuild— for example,storybook-angular:test → vite-plugin-angular:build. That evidence is what justified deleting theneeds: buildgate: Nx schedules the ordering, so CI doesn't have to. - It refused to over-consolidate. Each target has a distinct project filter —
lintexcludescontent,platform,router,buildexcludesastro-app,e2eis limited to two projects. A singlerun-manycan't express those, so the commands stayed separate. Correctness over a tidier diff. - It translated constraints, not bottlenecks. The old
--parallel=1limits became per-agent task concurrency rather than global serialization, because the constrained resource — memory, browser ports — is local to a machine, and each agent is its own machine.
That last distinction is the one I most expected it to get wrong, and the one that matters most. An old single-runner --parallel=1 is easy to misread as "this must never run concurrently anywhere." Usually it just means "this machine only has so much RAM."
The result
CI is now a single coordinator job that starts a distributed run and hands the cacheable Nx work to agents:
- name: Start CI run
run: >
pnpm exec nx-cloud start-ci-run
--distribute-on="3 analog-linux"
--require-explicit-completion
--stop-agents-after="e2e"
A new .nx/workflows/agents.yaml defines one analog-linux agent template — Node pinned from .node-version, corepack and pnpm, Playwright and Cypress browsers — so any agent can pick up a lint, build, test, or e2e task. The build-windows smoke build stays a native runner job; it needs a separate OS plane, and the agent left it alone.
The payoff is on the part I changed. On the first run — cold cache, agents starting from scratch — the Linux pipeline dropped from about 10 minutes to 7 and a half, just from letting the graph schedule across agents instead of chaining build → e2e in YAML. It should keep dropping as the Nx Cloud cache warms up.
That left one long pole: the Windows smoke build, still around 12 minutes. It was rebuilding all 20 projects, one at a time, just to verify a single app's output. So I scoped it to that app — nx build blog-app, the six tasks it actually needs — and moved the full Windows build to a nightly run. It dropped from ~12 minutes to under 4. Time to green went from ~12 minutes to about 7 and a half.
Conclusion
Let the build tool own the graph. Nx already knows test depends on build. Restating that in YAML just keeps the same fact in two places, and only one of them is checked.
The rest was separating meaning from shape. Old job boundaries and --parallel numbers tell you how the last setup happened to work, not what the next one has to keep. And the analysis — read the scripts, generate the task graph, decide what not to merge — turned out to be the part worth handing off. Point a tool at the real pipeline instead of a mental model of it, and it does that part well. That's the part to lean on.
If you enjoyed this post, click the ❤️ so other people will see it. Follow AnalogJS and Brandon Roberts on Bluesky, and subscribe to my YouTube Channel for more content!


Top comments (0)