n8n Cloud's scanner — and why @loomcycle/n8n-nodes-loomcycle now ships in two editions.
v3.0.0 of @loomcycle/n8n-nodes-loomcycle shipped today. It's a major version with a deliberate cost: the package now ships in two parallel editions from one repo. The default — @loomcycle/n8n-nodes-loomcycle on npm, main branch — is the Slim edition: 14 nodes, zero runtime dependencies, passes n8n Cloud's community-node scanner. The other — @loomcycle/n8n-nodes-loomcycle-full on npm, full-edition branch — is the Full edition: 18 nodes, includes the four AI-Agent Tool cluster sub-nodes plus SSE-push triggers plus the Wait-for-Completion op, self-hosted only.
We didn't want two packages. The forcing function was n8n Cloud's @n8n/scan-community-package scanner, which bans the exact dependencies our cluster sub-nodes were built on. There was no path to a single package that satisfied both audiences, so we built both. This post is the honest engineering account: what the scanner forbids, what we removed, what we replaced it with, and how the constraint produced a real engineering win in the Chat Model.
The four things n8n Cloud's scanner won't let you ship
Community-node packages submitted for n8n Cloud verification run through @n8n/scan-community-package. The scanner is strict and explicit. Zero violations or no Cloud listing. Four categories of bans matter for an agent-runtime wrapper like ours:
- No
@langchain/core. The package — and every transitive dep — can't import it. n8n Cloud runs community nodes in a sandbox that controls memory and timing; langchain's runtime brings its own promise / scheduling assumptions that don't fit. - No timer primitives.
setTimeout,setInterval,node:timers,globalThis,process— all banned. n8n owns the schedule; community nodes don't. - No
console. All logging goes through n8n's own logging surface. The package can't leak stdout/stderr noise. - No non-
n8n-workflowpeer dependencies. The package can declare exactly one peer dep, and it must ben8n-workflow. Everything else is either bundled or absent.
The first three are about runtime invariants n8n Cloud guarantees to its tenants. The fourth is about supply-chain hygiene — every additional peer dep is a vector. Together they describe a runtime where the community node is a guest, not a host: it does what n8n asks, when n8n asks, with what n8n provides.
For a thin HTTP wrapper that just calls loomcycle's wire API, these constraints are mostly trivial. Our action nodes (Run, Memory, Channel, AgentDef, SkillDef, MCPServerDef, Schedule, Webhook, A2A Agent, A2A Server Card, Hook) never used timers and weren't built on langchain — they bundle @loomcycle/client and POST to loomcycle. Those passed the scanner unmodified. The problem was elsewhere.
What we removed to clear the scanner
Three categories of features couldn't survive the four bans.
The four AI-Agent Tool cluster sub-nodes. LoomCycle Memory Tool, Channel Tool, Sub-Agent Tool, and the crown jewel MCP Server Tool — all built on @langchain/core's StructuredTool base class, which is n8n's documented path for plugging tools into the AI Agent. The langchain dependency is essential: @n8n/ai-node-sdk (the langchain-free alternative) has no tool-supply API yet. There's no fix — the dependency is the value. Removed in v3.0.0.
The Run Wait for Completion op. A single op on the Run node that polled GET /v1/runs/{id} until the run reached a terminal state. It used setTimeout for the backoff. setTimeout is banned. The op is removed; operators get the equivalent behavior via the Run Completed trigger (which fires when the run actually completes) or n8n's native Wait node (which n8n owns the schedule for).
The SSE-push half of both triggers. Run Completed and Channel Message were "SSE primary with polling fallback for proxy-hostile deployments." The SSE primary path used a long-running connection inside the node, which conflicts with n8n's scheduling model. Both triggers are now poll()-based — n8n schedules the tick, the node returns when it has events. Detection latency is now the poll interval (operator-configurable, default 10 s) instead of milliseconds. The dedup helpers (seen-set, cursor persistence in workflow static data) carried over unchanged.
Two adjacent items also moved in the same window. The package's license switched from Apache-2.0 to MIT — n8n-node lint flags any non-MIT license as a conformance error (community-package-json-license-not-default). MIT is the community-node convention. Loomcycle core and @loomcycle/client stay Apache-2.0; only the n8n wrapper changes. And the package's peer dependencies are now exactly { "n8n-workflow": "*" } — every other dep is either bundled (@loomcycle/client at build time, zero runtime dep tree) or absent.
The engineering win the constraint forced
The Chat Model is the load-bearing piece. Loomcycle's whole point — from n8n's perspective — is that the AI Agent node can talk to loomcycle's LLM gateway, which routes across providers with quotas, audit, and OTEL. The Chat Model node is the seam.
Until v2.x, the Chat Model was a BaseChatModel subclass from @langchain/core. Three workarounds lived inside it that the previous post ("What it took to make loomcycle a first-class n8n citizen") walked through in detail: a BindTools override to round-trip tool-call IDs through loomcycle's wire shape, a RunnableBinding dance to keep the underlying _getType property reachable across prototype chains, and a synthetic tool-call-id minted at every wire boundary so n8n's AI Agent could correlate streaming chunks with their parent calls. Together: about 200 lines of compensation code that existed entirely because langchain's BaseChatModel was the surface we had to inherit.
The migration target — @n8n/ai-node-sdk — is n8n's own langchain-free Chat Model API. It exposes generate(messages, options) → Result and stream(messages, options) → AsyncIterable<Chunk> over the SDK's own Message types, plus a supplyModel protocol for the AI Agent node to consume. No BaseChatModel inheritance. No prototype chains. No BindTools override.
The migration deleted every one of the three workarounds. The new Chat Model is straight-line code: take n8n's Message[], translate to loomcycle's wire shape, call /v1/_llm/chat, translate the response back. Tool calls round-trip through the SDK's native IDs — no synthetic minting, no prototype lookups, no RunnableBinding wrapper. The diff was ~50 lines of new code replacing ~200 lines of workarounds.
This is the kind of "we wouldn't have fixed it otherwise" win that's hard to motivate without a forcing function. The langchain-based Chat Model worked. The workarounds were stable. The synthetic-tool-call-id stuff had regression tests. We weren't going to spend a sprint refactoring code that wasn't broken. The scanner banned the dep, and the migration to @n8n/ai-node-sdk turned out to be cleaner than the original — but it took the scanner's "you can't ship this anymore" to make us do it.
Why we ship two packages instead of one diminished one
The four AI-Agent Tool cluster sub-nodes were the single biggest reason an n8n operator would install @loomcycle/n8n-nodes-loomcycle over just calling loomcycle's HTTP API from a generic n8n HTTP Request node. The MCP Server Tool specifically — drop it on a canvas, the substrate auto-registers an MCPServerDef, the AI Agent gets a tool that spawns a loomcycle agent — was the crown-jewel pattern the v1.x package was built around.
Self-hosted n8n operators don't care about the scanner. They run their own n8n instance, install whatever community packages they want, accept whatever dependencies those packages bring. For them, removing the cluster sub-nodes is pure regression. The slim package solves their tool problem worse than the v2.x package did.
n8n Cloud operators do care about the scanner. They can only install packages that passed verification. For them, the v2.x package was never installable — Cloud's scanner had been blocking it. The slim package isn't a regression; it's the first version that's installable at all.
Two audiences, two valid optima, one repo. The split runs across two branches:
| Slim (default) | Full | |
|---|---|---|
| npm | @loomcycle/n8n-nodes-loomcycle | @loomcycle/n8n-nodes-loomcycle-full |
| branch | main | full-edition |
| nodes | 14 | 18 |
| n8n Cloud verified | ✅ yes — passes the scanner | ❌ no — self-hosted only |
| AI-Agent Tool sub-nodes | — (use action nodes or the Chat Model) | ✅ Memory / Channel / Sub-Agent / MCP Server Tool |
| Triggers | poll()-based (n8n schedules) | SSE-push + poll fallback |
| Run "Wait for Completion" op | — (use the trigger / Wait node) | ✅ included |
| Chat Model | @n8n/ai-node-sdk (langchain-free) | langchain BaseChatModel |
| Runtime deps | zero | langchain + ai-node-sdk + transitive |
Both editions track the same loomcycle wire API and credential. They differ only in node surface and Cloud-eligibility. Switching is a single npm install swap — the credential carries over, the workflows that only use action nodes carry over (the cluster sub-nodes that don't exist in the slim edition are the migration cost).
The CI discipline that keeps both editions honest
Two parallel packages from one repo only works if both stay green. The CI now runs three jobs on every push and PR:
- Conformance lint —
@n8n/node-cli linton Node 22. This is the official community-node linter, the same one the n8n team runs on submissions. Pinned to0.32.1for reproducibility; runs vianpxwithnpm cifirst so it resolves the repo's own eslint config instead of fetchingeslint@10(which is flat-config-only and errors out). - Cloud scanner replica —
@n8n/scan-community-package. Catches scanner violations before the n8n Creator Portal does. Zero violations or the build fails. - Regular test + build —
npm testacross all 195 tests,npm run buildvia esbuild to verify the dist bundle is correct and dep-free.
One n8n-Cloud-specific detail surfaced in the dep tree: @n8n/ai-node-sdk transitively requires @langchain/community, which requires ignore@^7.0.5, which conflicts with eslint's [email protected]. npm install resolves this loosely; npm ci rejects the inconsistent tree; the scanner forbids an overrides fix. The lockfile isn't published anyway (the package's files field is ["dist"] — zero-dep tarball, the lockfile is purely CI-side concern), so the fix was to switch CI from npm ci to npm install --no-audit --no-fund. Strict locally; loose in CI where the build product is what matters.
The honest engineering insight is that scanner constraints are constraints with intent. n8n Cloud's bans aren't arbitrary — every banned primitive corresponds to a runtime invariant n8n offers its tenants (memory-safety, predictable scheduling, no leaked stdout, supply-chain hygiene). The constraint forced a Chat Model migration that produced cleaner code. It forced trigger polling to go through n8n's own scheduler instead of our timer. It forced the package to bundle @loomcycle/client instead of declaring it as a peer dep, which turned the dist tarball into a zero-runtime-dep artifact. The slim edition isn't a diminished package; it's a package built to a stricter spec. The full edition is for operators who don't need that spec because they run their own n8n.
What you can do with it today
If you're on n8n Cloud, the slim package is the only one you can install — and v3.0.0 is the first version that the scanner passes, so it's also the first version installable on Cloud at all:
# n8n Cloud or self-hosted — Settings → Community Nodes → Install:
@loomcycle/n8n-nodes-loomcycle
If you're self-hosted and want the AI-Agent Tool cluster sub-nodes (especially the MCP Server Tool dynamic-provisioning pattern), switch to the full edition:
# Self-hosted n8n only — Settings → Community Nodes → Install:
@loomcycle/n8n-nodes-loomcycle-full
Both editions need a reachable loomcycle deployment and a bearer token. Self-hosted n8n can point at localhost; n8n Cloud needs a public HTTPS URL or a tunnel (Cloudflare Tunnel, ngrok) because n8n Cloud makes the outbound call from its own network, not yours.
The n8n Creator Portal submission for Cloud verification is queued. v3.0.0 passes @n8n/scan-community-package with zero violations and n8n-node lint with zero problems. Once n8n's team reviews and approves, the slim package appears in n8n Cloud's verified-community-node list.
Companion reading: What it took to make loomcycle a first-class n8n citizen (the v1.x→v2.x story; the BindTools / RunnableBinding / synthetic-id workarounds the v3.0.0 migration deleted), and Seven frameworks and the row that's missing (the broader integration-ecosystem framing this release fits into).