| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 1 | 25 | 46 | 1% |
| Commands | 0 | 23 | 26 | 0% |
| Section tags | 10 | 2 | 2 | 71% |
What each file covers
Sections
1 shared · 25 only in A · 46 only in B- − Project Overview
- − General Guidelines
- − Agent Skills and Claude Code Plugin
- − Essential Commands
- − Fresh checkout / agent setup
- − Building
- − Testing
- − Code Quality
- − Architecture Overview
- − Package Structure
- − Technology Stack
- − Key Architectural Patterns
- − Key Development Patterns
- − Workflow Traversal Utilities
- − TypeScript Best Practices
- − Error Handling
- − Persistence layer & the TypeORM boundary
- − Frontend Development
- − Testing Guidelines
- − Common Development Tasks
- − Design Principles
- − Security Must Not Degrade the Building Experience
- − Security Fix Hygiene
- − Customer Confidentiality
- − Github Guidelines
- + Commands
- + Run tests locally
- + Run with container capabilities (requires pnpm build:docker first)
- + Lint and typecheck
- + Test Maintenance (Janitor)
- + Golden Rules
- + When to Use
- + Architecture Rules
- + Analyze entire codebase
- + Analyze specific file
- + Run specific rule
- + Auto-fix (dead-code only)
- + List all rules (short)
- + Show detailed rule info (for AI agents)
- + Discover test specs (for orchestration)
- + Distribute specs across shards
- + Baseline (Incremental Cleanup)
- + Create baseline - snapshots current violations
- + Commit the baseline
- + Update baseline after fixing violations (manual commit required)
- + Incremental Cleanup Strategy
- + Show ALL violations (ignoring baseline) for cleanup work
- + Find easiest files to fix (lowest violation count)
- + TCR with max diff size (skip if changes are too large)
- + TCR Workflow (Test && Commit || Revert)
- + Dry run - see what would happen
- + Execute - actually commit/revert
- + With guardrails - skip if diff too large
- + After Writing New Tests
- + Entry Points
- + Test Isolation
- + Unique Identifiers
- + Dynamic User Creation
- + Isolated Browser Contexts
- + Code Style
- + Architecture
- + Debugging
- + Test Migration & Refactoring
- + Reference Files
- + Isolated API Contexts
- + Identity-Based Assertions
- + Worker Isolation (Fresh Database)
- + Data Setup
- + Feature Enablement
- + Feature Flag Overrides
- + Shard Rebalancing
- AGENTS.md
Commands
0 shared · 23 only in A · 26 only in B- − pnpm agent:setup
- − pnpm agent:setup install
- − pnpm agent:setup --json
- − pnpm build > build.log 2>&1
- − pnpm --filter @n8n/telemetry catalog
- − pnpm build
- − pnpm reset
- − pnpm reset --full
- − pnpm test
- − pnpm test:affected
- − pnpm test <test-file>
- − pnpm lint
- − pnpm typecheck
- − node-dev
- − pnpm dev:ai
- − eslint-disable
- − vitest
- − pnpm --filter=n8n-playwright test:local
- − pnpm dev
- − pnpm --filter n8n-containers services --services postgres,redis,mailpit,proxy
- − node-1234-improve-request-handling
- − node-1234-fix-ddos-vulnerability
- − gh pr create --draft
- + pnpm --filter=n8n-playwright test:local <file-path>
- + pnpm --filter=n8n-playwright test:local tests/e2e/credentials/crud.spec.ts
- + pnpm --filter=n8n-playwright test:container:sqlite --grep @capability:email
- + pnpm --filter=n8n-playwright lint
- + pnpm --filter=n8n-playwright typecheck
- + pnpm janitor
- + pnpm janitor --file=pages/CanvasPage.ts --verbose
- + pnpm janitor --rule=dead-code
- + pnpm janitor:fix --rule=dead-code
- + pnpm janitor --list
- + pnpm janitor rules --json
- + pnpm janitor discover
- + pnpm janitor orchestrate --shards=14
- + pnpm janitor baseline
- + git add .janitor-baseline.json && git commit -m "chore: add janitor baseline"
- + git add .janitor-baseline.json && git commit -m "chore: update baseline after cleanup"
- + pnpm janitor --ignore-baseline --json
- + pnpm janitor --ignore-baseline --json 2>/dev/null | jq '.fileReports | sort_by(.violationCount) | .[:5]'
- + pnpm janitor tcr --max-diff-lines=500 --execute -m="chore: cleanup"
- + pnpm janitor tcr --verbose
- + pnpm janitor tcr --execute -m="chore: remove dead code"
- + pnpm janitor tcr --execute --max-diff-lines=500 -m="chore: cleanup"
- + pnpm janitor --file=tests/my-new-test.spec.ts --verbose
- + pnpm janitor tcr --execute
- + pnpm janitor tcr --execute -m="chore: ..."
- + git commit
Section tags
10 shared · 2 only in A · 2 only in B- − types
- − security
- + database
- + api
- setup
- build
- test
- lint-format
- code-style
- architecture
- testing-strategy
- git-pr
- do-not
- agent-behaviour
Line diff
n8n-io/n8n · AGENTS.md
@@ −1 @@
1# AGENTS.md
2
3This file provides guidance on how to work with the n8n repository.
4
5## Project Overview
6
7n8n is a workflow automation platform written in TypeScript, using a monorepo
8structure managed by pnpm workspaces. It consists of a Node.js backend, Vue.js
9frontend, and extensible node-based workflow engine.
10
11## General Guidelines
12
13- Always use pnpm
14- **Secrets on the command line:** if a developer opted into anonymous dev
15 metrics (`scripts/dev-metrics`), pnpm command arguments are recorded. Arguments
16 of secret-carrying words (`config`, `login`, `publish`, `token`) — whether a
17 subcommand or baked into a flag — are dropped, and the home dir is stripped from
18 paths, but other args are sent as-is — so never put secrets in a command. Pass
19 sensitive values via environment variables, which are never captured.
20- When adding comments, keep them concise and to the point - explain the "why"
21 in a line or two; don't be overly verbose. Comments should be scoped and
22 relevant to the surrounding code, not just to the current task
23- We use Linear as a ticket tracking system
24- We use Posthog for feature flags
25- To find registered telemetry events (names, descriptions, properties), run
26 `pnpm --filter @n8n/telemetry catalog` (`--json` for structured output). The
27 registry is being adopted incrementally, so search call sites if the catalog
28 has no match. The `n8n:telemetry` skill covers adding or changing events
29- When starting to work on a new ticket – create a new branch from fresh
30 master with the name specified in Linear ticket
31- When creating a new branch for a ticket in Linear - use the branch name
32 suggested by Linear, **unless it is a security fix** (see Security Fix
33 Hygiene below)
34- Use mermaid diagrams in MD files when you need to visualise something
35- **Developing v3 features:** land normal feature work on `master` behind an
36 opt-in flag; introduce breaking changes only on the `3.x` branch. See
37 [.github/DEVELOPING_V3.md](.github/DEVELOPING_V3.md).
38
39## Agent Skills and Claude Code Plugin
40
41n8n shared skills live in `.agents/skills/`. Claude Code consumes them through
42symlinks in `.claude/plugins/n8n/skills/`; OpenCode reads `.agents/skills/`
43directly. Harness-specific overrides remain real directories in the harness
44path, such as `.opencode/skills/setup-mcps/`. See
45[skills README](.agents/skills/AGENTS.md) for editing and sync guidance.
46
47n8n-specific Claude Code commands and agents live in `.claude/plugins/n8n/` and
48are namespaced under `n8n:`. Use `n8n:` prefix when invoking them (e.g.
49`/n8n:create-pr`, `/n8n:plan`, `n8n:developer` agent). See
50[plugin README](.claude/plugins/n8n/README.md) for structure and details.
51
52## Essential Commands
53
54### Fresh checkout / agent setup
55
56For a fresh checkout (cat-bot, a new hire, any agent verifying the repo
57builds), prefer `pnpm agent:setup` over running install + build + tests by
58hand. It chains them in one process, caps per-process memory and turbo
59concurrency so a 6GB box doesn't OOM, streams all output to
60`.agent-setup/<step>.log` (gitignored), and surfaces only a one-line summary
61per step plus the tail of the failing log. A machine-readable
62`.agent-setup/summary.json` is always written so a backgrounded run is
63readable in a single shot — no polling, no scrolling logs.
64
65```bash
66pnpm agent:setup # install → build → test (full suite)
67pnpm agent:setup install # one step at a time
68pnpm agent:setup --json # JSON summary on stdout (for scripts/agents)
69```
70
71### Building
72Use `pnpm build` to build all packages. ALWAYS redirect the output of the
73build command to a file:
74
75```bash
76pnpm build > build.log 2>&1
77```
78
79You can inspect the last few lines of the build log file to check for errors:
80```bash
81tail -n 20 build.log
82```
83
84If build outputs or the turbo cache are stale (e.g. after switching branches
85or worktrees) but dependencies haven't changed, use `pnpm reset` (lightweight
86by default) for a fast recovery: it cleans build outputs and force-rebuilds
87(keeping `node_modules` and untracked files). If that doesn't fix your issue,
88use `pnpm reset --full`, which also wipes untracked files and reinstalls
89dependencies.
90
91### Testing
92- `pnpm test` - Run all tests
93- `pnpm test:affected` - Runs tests based on what has changed since the last
94 commit
95
96Running a particular test file requires going to the directory of that test
97and running: `pnpm test <test-file>`.
98
99When changing directories, use `pushd` to navigate into the directory and
100`popd` to return to the previous directory. When in doubt, use `pwd` to check
101your current directory.
102
103### Code Quality
104- `pnpm lint` - Lint code
105- `pnpm typecheck` - Run type checks
106
107Always run lint and typecheck before committing code to ensure quality.
108Execute these commands from within the specific package directory you're
109working on (e.g., `cd packages/cli && pnpm lint`). Run the full repository
110check only when preparing the final PR. When your changes affect type
111definitions, interfaces in `@n8n/api-types`, or cross-package dependencies,
112build the system before running lint and typecheck.
113
114## Architecture Overview
115
116**Monorepo Structure:** pnpm workspaces with Turbo build orchestration
117
118### Package Structure
119
120The monorepo is organized into these key packages:
121
122- **`packages/@n8n/api-types`**: Shared TypeScript interfaces between frontend and backend
123- **`packages/workflow`**: Core workflow interfaces and types
124- **`packages/core`**: Workflow execution engine
125- **`packages/cli`**: Express server, REST API, and CLI commands
126- **`packages/frontend/editor-ui`**: Vue 3 frontend application
127- **`packages/frontend/@n8n/i18n`**: Internationalization for UI text
128- **`packages/nodes-base`**: Built-in nodes for integrations
129- **`packages/@n8n/nodes-langchain`**: AI/LangChain nodes
130- **`packages/@n8n/instance-ai`**: "AI Assistant" in the UI, "Instance AI" in code — AI assistant backend. See its `CLAUDE.md` for architecture docs.
131- **`@n8n/design-system`**: Vue component library for UI consistency
132- **`@n8n/config`**: Centralized configuration management
133
134## Technology Stack
135
136- **Frontend:** Vue 3 + TypeScript + Vite + Pinia + Storybook UI Library
137- **Backend:** Node.js + TypeScript + Express + TypeORM
138- **Testing:** Vitest (unit) + Playwright (E2E)
139- **Database:** TypeORM with SQLite/PostgreSQL support
140- **Code Quality:** Biome (for formatting) + ESLint + lefthook git hooks
141
142### Key Architectural Patterns
143
1441. **Dependency Injection**: Uses `@n8n/di` for IoC container
1452. **Controller-Service-Repository**: Backend follows MVC-like pattern
1463. **Event-Driven**: Internal event bus for decoupled communication
1474. **Context-Based Execution**: Different contexts for different node types
1485. **State Management**: Frontend uses Pinia stores
1496. **Design System**: Reusable components and design tokens are centralized in
150 `@n8n/design-system`, where all pure Vue components should be placed to
151 ensure consistency and reusability
152
153## Key Development Patterns
154
155- Each package has isolated build configuration and can be developed independently
156- Hot reload works across the full stack during development
157- Node development uses dedicated `node-dev` CLI tool
158- Workflow tests are JSON-based for integration testing
159- AI features have dedicated development workflow (`pnpm dev:ai`)
160
161### Workflow Traversal Utilities
162
163The `n8n-workflow` package exports graph traversal utilities from
164`packages/workflow/src/common/`. Use these instead of custom traversal logic.
165
166**Key concept:** `workflow.connections` is indexed by **source node**.
167To find parent nodes, use `mapConnectionsByDestination()` to invert it first.
168
169```typescript
170import { getParentNodes, getChildNodes, mapConnectionsByDestination } from 'n8n-workflow';
171
172// Finding parent nodes (predecessors) - requires inverted connections
173const connectionsByDestination = mapConnectionsByDestination(workflow.connections);
174const parents = getParentNodes(connectionsByDestination, 'NodeName', 'main', 1);
175
176// Finding child nodes (successors) - uses connections directly
177const children = getChildNodes(workflow.connections, 'NodeName', 'main', 1);
178```
179
180### TypeScript Best Practices
181- **NEVER use `any` type** - use proper types or `unknown`
182- **Avoid type casting with `as`** - use type guards or type predicates instead (except in test code where `as` is acceptable)
183- **Define shared interfaces in `@n8n/api-types`** package for FE/BE communication
184- **Lazy-load heavy modules** — if a module is only used in a specific code
185 path (not every request), use `await import()` at point of use instead of
186 top-level `import`. Applies especially to native modules and large parsers.
187
188### Error Handling
189- Don't use the deprecated `ApplicationError` class anywhere — it's a
190 compatibility shim kept only so community nodes keep resolving. Use one of
191 these instead, picking by cause:
192 - `UserError` — the user caused it (invalid input, unauthorized action,
193 business-rule violation).
194 - `OperationalError` — a transient, expected issue (network request failing,
195 DB query timing out) that should be handled gracefully.
196 - `UnexpectedError` — a bug in the code (logic mistake, unhandled case,
197 failed assertion) that developers need to fix.
198- Import from appropriate error classes in each package
199
200### Persistence layer & the TypeORM boundary
201
202TypeORM (`@n8n/typeorm`) must stay in the **persistence layer** — the `@n8n/db`
203package or a backend module's own `database/` folder (entity/repository files).
204Business logic — services, controllers, handlers, commands, factories — must not
205import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). In
206`packages/cli` this is enforced by the `misplaced-n8n-typeorm-import` lint rule;
207a new import (or an inline `eslint-disable` of the rule) fails CI.
208
209- **Pattern:** when a query needs operators (`In`, `IsNull`, `LessThan`,
210 `FindOptionsWhere`, …), put it behind a **use-case-named repository method**
211 that takes plain parameters and returns domain-shaped values — not a generic
212 `find(options)` passthrough.
213- **Transactions:** transaction orchestration belongs in the persistence layer.
214 Don't reach for `.manager` / `.manager.transaction(...)` or
215 `createQueryBuilder(...)` in business logic. Use the sanctioned primitive in
216 `@n8n/db`: inject the abstract `TransactionRunner` and wrap the unit of work in
217 `txRunner.run(ctx, async (ctx) => …)`. The callback receives an
218 `OperationContext` carrying the active transaction; thread that `ctx` into the
219 repository methods you call. `run` **requires** a context — pass an empty `{}`
220 at the operation entry point, and reuse the one you were handed everywhere
221 below it (a context that already carries a transaction is joined, not nested).
222 Repositories extend `BaseRepository` and resolve the right `EntityManager` with
223 `this.managerFor(ctx)`; the `Transaction` handle is opaque and never exposes a
224 driver type to business logic. See `oauth-token.service.ts` +
225 `oauth-*-token.repository.ts` for a worked example.
226- **Anti-patterns reviewers reject** — they hide the dependency instead of
227 removing it:
228 - String-matching TypeORM errors, e.g. `error.name === 'QueryFailedError'`.
229 - Relabeling the import from `@n8n/typeorm` to `@n8n/db` to silence the rule
230 (`@n8n/db` re-exports several operators/types, but this relabels the
231 dependency rather than removing it).
232 - Pushing `.manager` / `createQueryBuilder` into business logic to avoid an
233 operator import — trades a visible leak for an invisible one.
234
235### Frontend Development
236- Refer to `packages/frontend/AGENTS.md`
237- **All UI text must use i18n** - add translations to `@n8n/i18n` package
238- **Use CSS variables directly** - never hardcode spacing as px values
239- **data-testid must be a single value** (no spaces or multiple values)
240- Always use `design-system-rules` skill in reviews
241
242### Testing Guidelines
243- **Always work from within the package directory** when running tests
244- **Mock all external dependencies** in unit tests
245- **Prefer reusing hoisted shared `mock<T>(...)` fixtures** when a typed mock is immutable and used across tests. This rule exists to avoid massive test slowdowns from repeatedly creating nested proxy mocks while preserving the type contract. Avoid replacing these with `as unknown as T` helpers for entities like `User`.
246- **Confirm test cases with user** before writing unit tests
247- **Typecheck is critical before committing** - always run `pnpm typecheck`
248- **When modifying pinia stores**, check for unused computed properties
249- **For Vitest packages that use `@n8n/di` decorators**, use `createVitestConfigWithDecorators` from `@n8n/vitest-config/node-decorators`. It enables SWC `decoratorMetadata` (esbuild doesn't emit it) and externalizes workspace packages that register services (`@n8n/di`, `@n8n/config`, `@n8n/constants`, `n8n-workflow`) so a single DI `Container` instance is shared across the runtime. Loading them through Vitest's pipeline alongside their CJS dist produces two `Container`s and `Container.get(...)` returns `undefined`.
250
251What we use for testing and writing tests:
252- For testing nodes and other backend components, we use Vitest for unit tests. Examples can be found in `packages/nodes-base/nodes/**/*test*`.
253- We use `nock` for server mocking
254- For frontend we use `vitest`
255- For E2E tests we use Playwright. Run with `pnpm --filter=n8n-playwright test:local`.
256 See `packages/testing/playwright/README.md` for details.
257- **To iterate on a feature without docker rebuilds**, boot service containers
258 and run `pnpm dev` locally — `pnpm --filter n8n-containers services --services postgres,redis,mailpit,proxy`
259 then `pnpm dev`. See [Develop against running containers](packages/testing/playwright/README.md#develop-against-running-containers-avoid-docker-rebuilds).
260- **For Playwright test maintenance/cleanup**, see `packages/testing/playwright/AGENTS.md` (includes janitor tool for static analysis, dead code removal, architecture enforcement, and TCR workflows).
261
262### Common Development Tasks
263
264When implementing features:
2651. Define API types in `packages/@n8n/api-types`
2662. Implement backend logic in `packages/cli` module, follow
267 `scripts/backend-module/backend-module-guide.md`
2683. Add API endpoints via controllers
2694. Update frontend in `packages/frontend/editor-ui` with i18n support
2705. Write tests with proper mocks
2716. Run `pnpm typecheck` to verify types
272
273## Design Principles
274
275### Security Must Not Degrade the Building Experience
276
277Security improvements, whether driven by enterprise requirements or internal
278standards, must NEVER add friction to the common-case building experience. When
279designing security-related features (defaults, behaviors, flows, error
280handling), apply these checks:
281
282- **No friction for the common case:** A community builder's workflow should
283 remain intuitive. Security should be invisible when it can be.
284- **Migration and upgrade paths:** Existing users must have a clear,
285 non-disruptive path forward when defaults or behaviors change.
286- **Security layers on top, not in competition:** Great UX and strong security
287 are not trade-offs. They're both required. If a design forces a choice
288 between them, the design needs more work.
289
290### Security Fix Hygiene
291
292**This is a public repository.** When working on security fixes, never expose
293the attack vector or vulnerability type in any public-facing artifact. Attackers
294monitor open-source repos for signals like branch names, commit messages, PR
295titles, test descriptions, and Linear URLs.
296
297**Rules for security fixes:**
298
299- **Branch names:** Do NOT use the Linear-suggested branch name if it reveals
300 the vulnerability. Rename to describe the fix neutrally
301 (e.g. `node-1234-improve-request-handling`, not
302 `node-1234-fix-ddos-vulnerability`).
303- **Commit messages:** Describe what the code now does, not the threat it
304 prevents (e.g. `fix: add payload size validation`, not
305 `fix: prevent denial of service`).
306- **Test descriptions:** Use neutral, functional language
307 (e.g. `'should sanitize query parameters'`, not
308 `'should prevent SQL injection'`).
309- **Code comments:** Do not describe the attack scenario in comments.
310- **Linear references:** Never include the URL slug
311 (e.g. `.../N8N-1234/fix-ssrf-vulnerability`).
312
313### Customer Confidentiality
314
315**This is a public repository.** Never mention customer names in any
316public-facing artifact — not all customers have agreed to be named publicly,
317and naming them can reveal security-relevant details about their setup.
318
319This applies to PR titles and descriptions, branch names, commit messages,
320code, code comments, test names and test data, and fixtures. When implementing
321a customer request, describe the use case neutrally (e.g. "a customer with a
322large multi-main setup", not the company name) and use generic placeholder
323names (e.g. `Acme Corp`) in tests and examples.
324
325## Github Guidelines
326- When creating a PR, use the conventions in
327 `.github/pull_request_template.md` and
328 `.github/pull_request_title_conventions.md`.
329- Use `gh pr create --draft` to create draft PRs.
330- If there is a corresponding Linear ticket, reference it in the PR
331 description using `https://linear.app/n8n/issue/[TICKET-ID]`. Do not
332 create a Linear ticket on your own — ask first.
333- always link to the github issue if mentioned in the linear ticket.
334
n8n-io/n8n · packages/testing/playwright/AGENTS.md
@@ +1 @@
1# AGENTS.md
2
3## Commands
4
5```bash
6# Run tests locally
7pnpm --filter=n8n-playwright test:local <file-path>
8pnpm --filter=n8n-playwright test:local tests/e2e/credentials/crud.spec.ts
9
10# Run with container capabilities (requires pnpm build:docker first)
11pnpm --filter=n8n-playwright test:container:sqlite --grep @capability:email
12
13# Lint and typecheck
14pnpm --filter=n8n-playwright lint
15pnpm --filter=n8n-playwright typecheck
16```
17
18Always trim output: `--reporter=list 2>&1 | tail -50`
19
20## Test Maintenance (Janitor)
21
22Static analysis for Playwright test architecture. Catches problems before they spread.
23
24> **CRITICAL: Always use TCR for code changes.**
25> When janitor identifies violations and you fix them, use `pnpm janitor tcr --execute` to safely commit. Never manually commit janitor-related fixes - TCR ensures tests pass before the commit lands.
26
27### Golden Rules
28
291. **Analysis only?** Run `pnpm janitor` (no TCR needed)
302. **Making code changes?** Use TCR: `pnpm janitor tcr --execute -m="chore: ..."`
313. **Never** manually `git commit` janitor-related fixes - always go through TCR
324. **Never** modify `.janitor-baseline.json` via TCR - baseline updates must be done manually
33
34### When to Use
35
36| User Says | Intent | Approach |
37|-----------|--------|----------|
38| "Clean up the test codebase" | Incremental cleanup | Create baseline first, then use `--max-diff-lines=500` for small PRs. |
39| "Start tracking violations" | Enable incremental cleanup | Run `janitor baseline` to snapshot current state, commit `.janitor-baseline.json`. |
40| "Add a test for X" | New test following patterns | After writing, run janitor to verify architecture compliance. |
41| "Fix architecture drift" | Enforce layered architecture | Run `selector-purity` and `no-page-in-flow` rules. |
42| "Find dead code" | Remove unused methods | Run `dead-code` rule with `--fix --write` for auto-removal. |
43| "Find copy-paste code" | Detect duplicates | Run `duplicate-logic` rule to find structural duplicates. |
44| "This file is messy" | Targeted cleanup | Analyze specific file, fix issues, TCR to safely commit. |
45| "Refactor this page object" | Safe refactoring | Use TCR - changes commit if tests pass, revert if they fail. |
46| "What tests would break?" | Impact analysis | Run `impact` command before changing shared code. |
47| "Prepare for PR" | Pre-commit check | Run janitor on changed files to catch violations early. |
48
49### Architecture Rules
50
51The janitor enforces a layered architecture:
52
53```
54Tests → Flows/Composables → Page Objects → Components → Playwright API
55```
56
57| Rule | What It Catches |
58|------|-----------------|
59| `selector-purity` | Raw locators in tests/flows: `page.getByTestId()`, `someLocator.locator()` |
60| `no-page-in-flow` | Flows accessing `page` directly (should use page objects) |
61| `boundary-protection` | Pages importing other pages (creates coupling) |
62| `scope-lockdown` | Unscoped locators that escape their container |
63| `dead-code` | Unused public methods in page objects |
64| `deduplication` | Same selector defined in multiple files |
65| `duplicate-logic` | Copy-pasted code across tests/pages (AST fingerprinting) |
66| `no-raw-editor-navigation` | Raw `page.goto()` to a `/workflow/` editor route in tests (use `n8n.start.*` so the canvas loader is awaited) |
67| `valid-owner-annotation` | A spec with no team owner, or an owner not in the canonical list (`CANONICAL_OWNERS` in the rule, mirroring Notion "Ownership v2") |
68
69### Commands
70
71```bash
72# Analyze entire codebase
73pnpm janitor
74
75# Analyze specific file
76pnpm janitor --file=pages/CanvasPage.ts --verbose
77
78# Run specific rule
79pnpm janitor --rule=dead-code
80
81# Auto-fix (dead-code only)
82pnpm janitor:fix --rule=dead-code
83
84# List all rules (short)
85pnpm janitor --list
86
87# Show detailed rule info (for AI agents)
88pnpm janitor rules --json
89
90# Discover test specs (for orchestration)
91pnpm janitor discover
92
93# Distribute specs across shards
94pnpm janitor orchestrate --shards=14
95```
96
97### Baseline (Incremental Cleanup)
98
99For codebases with existing violations, create a baseline to enable incremental cleanup:
100
101```bash
102# Create baseline - snapshots current violations
103pnpm janitor baseline
104
105# Commit the baseline
106git add .janitor-baseline.json && git commit -m "chore: add janitor baseline"
107```
108
109Once baseline exists, janitor and TCR **only fail on NEW violations**. Pre-existing violations are tracked but don't block work.
110
111> **Safeguard:** TCR blocks commits that modify `.janitor-baseline.json`. This prevents accidentally "fixing" violations by updating the baseline instead of the actual code. Baseline updates must be done manually after fixing violations.
112
113```bash
114# Update baseline after fixing violations (manual commit required)
115pnpm janitor baseline
116git add .janitor-baseline.json && git commit -m "chore: update baseline after cleanup"
117```
118
119### Incremental Cleanup Strategy
120
121For large cleanups, keep diffs small and reviewable:
122
123```bash
124# Show ALL violations (ignoring baseline) for cleanup work
125pnpm janitor --ignore-baseline --json
126
127# Find easiest files to fix (lowest violation count)
128pnpm janitor --ignore-baseline --json 2>/dev/null | jq '.fileReports | sort_by(.violationCount) | .[:5]'
129
130# TCR with max diff size (skip if changes are too large)
131pnpm janitor tcr --max-diff-lines=500 --execute -m="chore: cleanup"
132```
133
134**AI Cleanup Workflow:**
1351. Use `--ignore-baseline` to see all violations (not just new ones)
1362. Pick small fixes from the list
1373. Fix violations, then TCR to safely commit
1384. After fixing, run `pnpm janitor baseline` to update the baseline
139
140### TCR Workflow (Test && Commit || Revert)
141
142Safe refactoring loop: make changes, run affected tests, auto-commit or auto-revert.
143
144```bash
145# Dry run - see what would happen
146pnpm janitor tcr --verbose
147
148# Execute - actually commit/revert
149pnpm janitor tcr --execute -m="chore: remove dead code"
150
151# With guardrails - skip if diff too large
152pnpm janitor tcr --execute --max-diff-lines=500 -m="chore: cleanup"
153```
154
155### After Writing New Tests
156
157Always run janitor after adding or modifying tests to catch architecture violations early:
158
159```bash
160pnpm janitor --file=tests/my-new-test.spec.ts --verbose
161```
162
163See `packages/testing/janitor/README.md` for full documentation.
164
165## Entry Points
166
167All tests should start with `n8n.start.*` methods. See `composables/TestEntryComposer.ts`.
168
169| Method | Use Case |
170|--------|----------|
171| `fromHome()` | Start from home page |
172| `fromBlankCanvas()` | New workflow from scratch |
173| `fromNewProjectBlankCanvas()` | Project-scoped workflow (returns projectId) |
174| `fromNewProject()` | Project-scoped test, no canvas (returns projectId) |
175| `fromImportedWorkflow(file)` | Test pre-built workflow JSON |
176| `withUser(user)` | Isolated browser context per user |
177| `withProjectFeatures()` | Enable sharing/folders/permissions |
178
179## Test Isolation
180
181Tests run in parallel. Design tests to be fully isolated so they don't interfere with each other.
182
183### Unique Identifiers
184
185Use `nanoid` for unique test data:
186
187```typescript
188const credentialName = `Test Credential ${nanoid()}`;
189const workflow = await api.workflows.createWorkflow({
190 name: `Test Workflow ${nanoid()}`,
191});
192```
193
194### Dynamic User Creation
195
196Create users dynamically via the public API:
197
198```typescript
199const member = await api.publicApi.createUser({
200 email: `member-${nanoid()}@test.com`,
201 firstName: 'Test',
202 lastName: 'Member',
203});
204```
205
206### Isolated Browser Contexts
207
208For UI tests requiring multiple users, create isolated browser contexts:
209
210```typescript
211// 1. Create users via public API
212const member1 = await api.publicApi.createUser({ role: 'global:member' });
213const member2 = await api.publicApi.createUser({ role: 'global:member' });
214
215// 2. Get isolated browser contexts
216const member1Page = await n8n.start.withUser(member1);
217const member2Page = await n8n.start.withUser(member2);
218
219// 3. Each operates independently (no session bleeding)
220await member1Page.navigate.toWorkflows();
221await member2Page.navigate.toCredentials();
222```
223
224**Reference:** `tests/e2e/building-blocks/user-service.spec.ts`
225
226| Pattern | Why | Use Instead |
227|---------|-----|-------------|
228| `test.describe.serial` | Creates test dependencies | Parallel tests with isolated setup |
229| Fresh DB per file | Tests need isolated container | `test.use({ capability: { env: { TEST_ISOLATION: 'name' } } })` |
230| Fresh DB per test | Tests modify shared state | `@db:reset` tag on describe (container-only, combined with `test.use()`) |
231| `n8n.api.signin()` | Session bleeding | `n8n.start.withUser()` |
232| `Date.now()` for IDs | Race conditions | `nanoid()` |
233| `waitForTimeout()` | Flaky | `waitForResponse()`, `toBeVisible()` |
234| `.toHaveCount(N)` | Brittle | Named element assertions |
235| Raw `page.goto()` | Bypasses setup | `n8n.navigate.*` methods |
236
237## Code Style
238
239- Use specialized locators: `page.getByRole('button')` over `page.locator('[role=button]')`
240- Use `nanoid()` for unique identifiers (parallel-safe)
241- API setup over UI setup when possible (faster, more reliable)
242
243## Architecture
244
245```
246Tests (*.spec.ts)
247 ↓ uses
248Composables (*Composer.ts) - Multi-step business workflows
249 ↓ orchestrates
250Page Objects (*Page.ts) - UI interactions
251 ↓ extends
252BasePage - Common utilities
253```
254
255See `CONTRIBUTING.md` for detailed patterns and conventions.
256
257## Debugging
258
259See [README.md#debugging](./README.md#debugging) for detailed instructions on:
260- **Keepalive mode** - Keep containers running after tests with `N8N_CONTAINERS_KEEPALIVE=true`
261- **Victoria exports** - Logs/metrics automatically attached on failure, importable locally via `scripts/import-victoria-data.mjs`
262
263## Test Migration & Refactoring
264
265**Test Name = Contract**
266- Name declares intent, assertion proves it, everything else is flow
267- Bad: `should open W1 as U2` (describes action)
268- Good: `should allow sharee to edit shared workflow` (declares rule)
269
270**Coverage Parity Check**
2711. Read old test name → what was the intent?
2722. Find the explicit assertion that proved it
2733. Verify new test has equivalent proof
2744. No proof found? Document as intentional drop or gap
275
276**Legacy Tests (unauditable names/assertions)**
277- Prioritize clarity over parity - can't audit what you can't read
278- Document your best interpretation of intent
279- Accept short-term risk, fix regressions forward
280
281See [Quality Corner: Test Migration Guide](https://www.notion.so/n8n/Best-Practices-Test-Migration-Refactoring) for full rationale and examples.
282
283## Reference Files
284
285| Purpose | File |
286|---------|------|
287| Multi-user testing | `tests/e2e/building-blocks/user-service.spec.ts` |
288| Entry points | `composables/TestEntryComposer.ts` |
289| Page object example | `pages/CanvasPage.ts` |
290| Composable example | `composables/WorkflowComposer.ts` |
291| API helpers | `services/api-helper.ts` |
292| Capabilities | `fixtures/capabilities.ts` |
293
294```typescript
295const member = await api.publicApi.createUser({...});
296const memberN8n = await n8n.start.withUser(member);
297
298await memberN8n.navigate.toWorkflows();
299await expect(memberN8n.workflows.cards.getWorkflow(workflowName)).toBeVisible();
300```
301### Isolated API Contexts
302
303For API-only operations as another user, create isolated API contexts (no browser needed):
304
305```typescript
306const member = await api.publicApi.createUser({...});
307const memberApi = await api.createApiForUser(member);
308
309const memberProject = await memberApi.projects.getMyPersonalProject();
310await memberApi.credentials.createCredential({...});
311```
312
313### Identity-Based Assertions
314
315Assert by identity (name) rather than count for parallel-safe tests:
316
317```typescript
318await expect(credentialDropdown.getByText(testCredName)).toBeVisible();
319await expect(credentialDropdown.getByText(devCredName)).toBeHidden();
320```
321
322## Worker Isolation (Fresh Database)
323
324Use `test.use()` at file top-level with unique capability config:
325
326```typescript
327// my-isolated-tests.spec.ts
328import { test, expect } from '../fixtures/base';
329
330// Must be top-level, not inside describe block
331test.use({ capability: { env: { TEST_ISOLATION: 'my-isolated-tests' } } });
332
333test('test with clean state', async ({ n8n }) => {
334 // Fresh container with reset database
335});
336```
337
338For per-test database reset (when tests modify shared state like MFA), add `@db:reset` to the describe. **Note:** `@db:reset` is container-only - these tests won't run locally.
339
340```typescript
341test.use({ capability: { env: { TEST_ISOLATION: 'my-stateful-tests' } } });
342
343test.describe('My stateful tests @db:reset', () => {
344 // Each test gets a fresh database reset (container-only)
345});
346```
347
348## Data Setup
349
350Use API helpers for fast, reliable test data setup. Reserve UI interactions for testing UI behavior:
351
352```typescript
353// API for data setup
354const credential = await api.credentials.createCredential({
355 name: `Test Credential ${nanoid()}`,
356 type: 'notionApi',
357 data: { apiKey: 'test' },
358});
359
360const workflow = await api.workflows.createWorkflow({
361 name: `Test Workflow ${nanoid()}`,
362 nodes: [...],
363});
364
365// UI for verification
366await n8n.navigate.toCredentials();
367await expect(n8n.credentials.cards.getCredential(credential.name)).toBeVisible();
368```
369
370## Feature Enablement
371
372The `n8n` fixture automatically enables project features. For API-only tests (no `n8n` fixture), enable features explicitly:
373
374```typescript
375test('API-only test', async ({ api }) => {
376 await api.enableProjectFeatures();
377 // ...
378});
379```
380
381### Feature Flag Overrides
382
383To test features behind feature flags (experiments), use `TestRequirements` with storage overrides:
384
385```typescript
386import type { TestRequirements } from '../../../Types';
387
388const requirements: TestRequirements = {
389 storage: {
390 N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ 'your_experiment': true }),
391 },
392};
393
394test.use({ requirements });
395
396test('test with feature flag enabled', async ({ n8n }) => {
397 // Feature flag is now active for this test
398});
399```
400
401**Common patterns:**
402
403```typescript
404// Single experiment
405{ storage: { N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ '025_new_canvas': true }) } }
406
407// Multiple experiments
408{ storage: { N8N_EXPERIMENT_OVERRIDES: JSON.stringify({
409 '025_new_canvas': true,
410 '026_another_feature': 'variant_a'
411}) } }
412
413// Combined with other requirements
414const requirements: TestRequirements = {
415 storage: {
416 N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ 'your_experiment': true }),
417 },
418 capability: {
419 env: { TEST_ISOLATION: 'my-test-suite' },
420 },
421};
422```
423
424**Reference:** `Types.ts` for the full interface definition. Import depth follows
425the spec's own nesting. The example above assumes `tests/e2e/<area>/`; add one
426`../` per extra level down.
427
428## Shard Rebalancing
429
430When refactoring, adding, or moving significant numbers of tests, consider rebalancing test shards to maintain even CI distribution. See `docs/ORCHESTRATION.md` for details.
431
@@ −1 +1 @@
11 # AGENTS.md
22
3−This file provides guidance on how to work with the n8n repository.
3+## Commands
44
5−## Project Overview
5+```bash
6+# Run tests locally
7+pnpm --filter=n8n-playwright test:local <file-path>
8+pnpm --filter=n8n-playwright test:local tests/e2e/credentials/crud.spec.ts
69
7−n8n is a workflow automation platform written in TypeScript, using a monorepo
8−structure managed by pnpm workspaces. It consists of a Node.js backend, Vue.js
9−frontend, and extensible node-based workflow engine.
10+# Run with container capabilities (requires pnpm build:docker first)
11+pnpm --filter=n8n-playwright test:container:sqlite --grep @capability:email
1012
11−## General Guidelines
13+# Lint and typecheck
14+pnpm --filter=n8n-playwright lint
15+pnpm --filter=n8n-playwright typecheck
16+```
1217
13−- Always use pnpm
14−- **Secrets on the command line:** if a developer opted into anonymous dev
15− metrics (`scripts/dev-metrics`), pnpm command arguments are recorded. Arguments
16− of secret-carrying words (`config`, `login`, `publish`, `token`) — whether a
17− subcommand or baked into a flag — are dropped, and the home dir is stripped from
18− paths, but other args are sent as-is — so never put secrets in a command. Pass
19− sensitive values via environment variables, which are never captured.
20−- When adding comments, keep them concise and to the point - explain the "why"
21− in a line or two; don't be overly verbose. Comments should be scoped and
22− relevant to the surrounding code, not just to the current task
23−- We use Linear as a ticket tracking system
24−- We use Posthog for feature flags
25−- To find registered telemetry events (names, descriptions, properties), run
26− `pnpm --filter @n8n/telemetry catalog` (`--json` for structured output). The
27− registry is being adopted incrementally, so search call sites if the catalog
28− has no match. The `n8n:telemetry` skill covers adding or changing events
29−- When starting to work on a new ticket – create a new branch from fresh
30− master with the name specified in Linear ticket
31−- When creating a new branch for a ticket in Linear - use the branch name
32− suggested by Linear, **unless it is a security fix** (see Security Fix
33− Hygiene below)
34−- Use mermaid diagrams in MD files when you need to visualise something
35−- **Developing v3 features:** land normal feature work on `master` behind an
36− opt-in flag; introduce breaking changes only on the `3.x` branch. See
37− [.github/DEVELOPING_V3.md](.github/DEVELOPING_V3.md).
18+Always trim output: `--reporter=list 2>&1 | tail -50`
3819
39−## Agent Skills and Claude Code Plugin
20+## Test Maintenance (Janitor)
4021
41−n8n shared skills live in `.agents/skills/`. Claude Code consumes them through
42−symlinks in `.claude/plugins/n8n/skills/`; OpenCode reads `.agents/skills/`
43−directly. Harness-specific overrides remain real directories in the harness
44−path, such as `.opencode/skills/setup-mcps/`. See
45−[skills README](.agents/skills/AGENTS.md) for editing and sync guidance.
22+Static analysis for Playwright test architecture. Catches problems before they spread.
4623
47−n8n-specific Claude Code commands and agents live in `.claude/plugins/n8n/` and
48−are namespaced under `n8n:`. Use `n8n:` prefix when invoking them (e.g.
49−`/n8n:create-pr`, `/n8n:plan`, `n8n:developer` agent). See
50−[plugin README](.claude/plugins/n8n/README.md) for structure and details.
24+> **CRITICAL: Always use TCR for code changes.**
25+> When janitor identifies violations and you fix them, use `pnpm janitor tcr --execute` to safely commit. Never manually commit janitor-related fixes - TCR ensures tests pass before the commit lands.
5126
52−## Essential Commands
27+### Golden Rules
5328
54−### Fresh checkout / agent setup
29+1. **Analysis only?** Run `pnpm janitor` (no TCR needed)
30+2. **Making code changes?** Use TCR: `pnpm janitor tcr --execute -m="chore: ..."`
31+3. **Never** manually `git commit` janitor-related fixes - always go through TCR
32+4. **Never** modify `.janitor-baseline.json` via TCR - baseline updates must be done manually
5533
56−For a fresh checkout (cat-bot, a new hire, any agent verifying the repo
57−builds), prefer `pnpm agent:setup` over running install + build + tests by
58−hand. It chains them in one process, caps per-process memory and turbo
59−concurrency so a 6GB box doesn't OOM, streams all output to
60−`.agent-setup/<step>.log` (gitignored), and surfaces only a one-line summary
61−per step plus the tail of the failing log. A machine-readable
62−`.agent-setup/summary.json` is always written so a backgrounded run is
63−readable in a single shot — no polling, no scrolling logs.
34+### When to Use
6435
36+| User Says | Intent | Approach |
37+|-----------|--------|----------|
38+| "Clean up the test codebase" | Incremental cleanup | Create baseline first, then use `--max-diff-lines=500` for small PRs. |
39+| "Start tracking violations" | Enable incremental cleanup | Run `janitor baseline` to snapshot current state, commit `.janitor-baseline.json`. |
40+| "Add a test for X" | New test following patterns | After writing, run janitor to verify architecture compliance. |
41+| "Fix architecture drift" | Enforce layered architecture | Run `selector-purity` and `no-page-in-flow` rules. |
42+| "Find dead code" | Remove unused methods | Run `dead-code` rule with `--fix --write` for auto-removal. |
43+| "Find copy-paste code" | Detect duplicates | Run `duplicate-logic` rule to find structural duplicates. |
44+| "This file is messy" | Targeted cleanup | Analyze specific file, fix issues, TCR to safely commit. |
45+| "Refactor this page object" | Safe refactoring | Use TCR - changes commit if tests pass, revert if they fail. |
46+| "What tests would break?" | Impact analysis | Run `impact` command before changing shared code. |
47+| "Prepare for PR" | Pre-commit check | Run janitor on changed files to catch violations early. |
48+
49+### Architecture Rules
50+
51+The janitor enforces a layered architecture:
52+
53+```
54+Tests → Flows/Composables → Page Objects → Components → Playwright API
55+```
56+
57+| Rule | What It Catches |
58+|------|-----------------|
59+| `selector-purity` | Raw locators in tests/flows: `page.getByTestId()`, `someLocator.locator()` |
60+| `no-page-in-flow` | Flows accessing `page` directly (should use page objects) |
61+| `boundary-protection` | Pages importing other pages (creates coupling) |
62+| `scope-lockdown` | Unscoped locators that escape their container |
63+| `dead-code` | Unused public methods in page objects |
64+| `deduplication` | Same selector defined in multiple files |
65+| `duplicate-logic` | Copy-pasted code across tests/pages (AST fingerprinting) |
66+| `no-raw-editor-navigation` | Raw `page.goto()` to a `/workflow/` editor route in tests (use `n8n.start.*` so the canvas loader is awaited) |
67+| `valid-owner-annotation` | A spec with no team owner, or an owner not in the canonical list (`CANONICAL_OWNERS` in the rule, mirroring Notion "Ownership v2") |
68+
69+### Commands
70+
6571 ```bash
66−pnpm agent:setup # install → build → test (full suite)
67−pnpm agent:setup install # one step at a time
68−pnpm agent:setup --json # JSON summary on stdout (for scripts/agents)
72+# Analyze entire codebase
73+pnpm janitor
74+
75+# Analyze specific file
76+pnpm janitor --file=pages/CanvasPage.ts --verbose
77+
78+# Run specific rule
79+pnpm janitor --rule=dead-code
80+
81+# Auto-fix (dead-code only)
82+pnpm janitor:fix --rule=dead-code
83+
84+# List all rules (short)
85+pnpm janitor --list
86+
87+# Show detailed rule info (for AI agents)
88+pnpm janitor rules --json
89+
90+# Discover test specs (for orchestration)
91+pnpm janitor discover
92+
93+# Distribute specs across shards
94+pnpm janitor orchestrate --shards=14
6995 ```
7096
71−### Building
72−Use `pnpm build` to build all packages. ALWAYS redirect the output of the
73−build command to a file:
97+### Baseline (Incremental Cleanup)
7498
99+For codebases with existing violations, create a baseline to enable incremental cleanup:
100+
75101 ```bash
76−pnpm build > build.log 2>&1
102+# Create baseline - snapshots current violations
103+pnpm janitor baseline
104+
105+# Commit the baseline
106+git add .janitor-baseline.json && git commit -m "chore: add janitor baseline"
77107 ```
78108
79−You can inspect the last few lines of the build log file to check for errors:
109+Once baseline exists, janitor and TCR **only fail on NEW violations**. Pre-existing violations are tracked but don't block work.
110+
111+> **Safeguard:** TCR blocks commits that modify `.janitor-baseline.json`. This prevents accidentally "fixing" violations by updating the baseline instead of the actual code. Baseline updates must be done manually after fixing violations.
112+
80113 ```bash
81−tail -n 20 build.log
114+# Update baseline after fixing violations (manual commit required)
115+pnpm janitor baseline
116+git add .janitor-baseline.json && git commit -m "chore: update baseline after cleanup"
82117 ```
83118
84−If build outputs or the turbo cache are stale (e.g. after switching branches
85−or worktrees) but dependencies haven't changed, use `pnpm reset` (lightweight
86−by default) for a fast recovery: it cleans build outputs and force-rebuilds
87−(keeping `node_modules` and untracked files). If that doesn't fix your issue,
88−use `pnpm reset --full`, which also wipes untracked files and reinstalls
89−dependencies.
119+### Incremental Cleanup Strategy
90120
91−### Testing
92−- `pnpm test` - Run all tests
93−- `pnpm test:affected` - Runs tests based on what has changed since the last
94− commit
121+For large cleanups, keep diffs small and reviewable:
95122
96−Running a particular test file requires going to the directory of that test
97−and running: `pnpm test <test-file>`.
123+```bash
124+# Show ALL violations (ignoring baseline) for cleanup work
125+pnpm janitor --ignore-baseline --json
98126
99−When changing directories, use `pushd` to navigate into the directory and
100−`popd` to return to the previous directory. When in doubt, use `pwd` to check
101−your current directory.
127+# Find easiest files to fix (lowest violation count)
128+pnpm janitor --ignore-baseline --json 2>/dev/null | jq '.fileReports | sort_by(.violationCount) | .[:5]'
102129
103−### Code Quality
104−- `pnpm lint` - Lint code
105−- `pnpm typecheck` - Run type checks
130+# TCR with max diff size (skip if changes are too large)
131+pnpm janitor tcr --max-diff-lines=500 --execute -m="chore: cleanup"
132+```
106133
107−Always run lint and typecheck before committing code to ensure quality.
108−Execute these commands from within the specific package directory you're
109−working on (e.g., `cd packages/cli && pnpm lint`). Run the full repository
110−check only when preparing the final PR. When your changes affect type
111−definitions, interfaces in `@n8n/api-types`, or cross-package dependencies,
112−build the system before running lint and typecheck.
134+**AI Cleanup Workflow:**
135+1. Use `--ignore-baseline` to see all violations (not just new ones)
136+2. Pick small fixes from the list
137+3. Fix violations, then TCR to safely commit
138+4. After fixing, run `pnpm janitor baseline` to update the baseline
113139
114−## Architecture Overview
140+### TCR Workflow (Test && Commit || Revert)
115141
116−**Monorepo Structure:** pnpm workspaces with Turbo build orchestration
142+Safe refactoring loop: make changes, run affected tests, auto-commit or auto-revert.
117143
118−### Package Structure
144+```bash
145+# Dry run - see what would happen
146+pnpm janitor tcr --verbose
119147
120−The monorepo is organized into these key packages:
148+# Execute - actually commit/revert
149+pnpm janitor tcr --execute -m="chore: remove dead code"
121150
122−- **`packages/@n8n/api-types`**: Shared TypeScript interfaces between frontend and backend
123−- **`packages/workflow`**: Core workflow interfaces and types
124−- **`packages/core`**: Workflow execution engine
125−- **`packages/cli`**: Express server, REST API, and CLI commands
126−- **`packages/frontend/editor-ui`**: Vue 3 frontend application
127−- **`packages/frontend/@n8n/i18n`**: Internationalization for UI text
128−- **`packages/nodes-base`**: Built-in nodes for integrations
129−- **`packages/@n8n/nodes-langchain`**: AI/LangChain nodes
130−- **`packages/@n8n/instance-ai`**: "AI Assistant" in the UI, "Instance AI" in code — AI assistant backend. See its `CLAUDE.md` for architecture docs.
131−- **`@n8n/design-system`**: Vue component library for UI consistency
132−- **`@n8n/config`**: Centralized configuration management
151+# With guardrails - skip if diff too large
152+pnpm janitor tcr --execute --max-diff-lines=500 -m="chore: cleanup"
153+```
133154
134−## Technology Stack
155+### After Writing New Tests
135156
136−- **Frontend:** Vue 3 + TypeScript + Vite + Pinia + Storybook UI Library
137−- **Backend:** Node.js + TypeScript + Express + TypeORM
138−- **Testing:** Vitest (unit) + Playwright (E2E)
139−- **Database:** TypeORM with SQLite/PostgreSQL support
140−- **Code Quality:** Biome (for formatting) + ESLint + lefthook git hooks
157+Always run janitor after adding or modifying tests to catch architecture violations early:
141158
142−### Key Architectural Patterns
159+```bash
160+pnpm janitor --file=tests/my-new-test.spec.ts --verbose
161+```
143162
144−1. **Dependency Injection**: Uses `@n8n/di` for IoC container
145−2. **Controller-Service-Repository**: Backend follows MVC-like pattern
146−3. **Event-Driven**: Internal event bus for decoupled communication
147−4. **Context-Based Execution**: Different contexts for different node types
148−5. **State Management**: Frontend uses Pinia stores
149−6. **Design System**: Reusable components and design tokens are centralized in
150− `@n8n/design-system`, where all pure Vue components should be placed to
151− ensure consistency and reusability
163+See `packages/testing/janitor/README.md` for full documentation.
152164
153−## Key Development Patterns
165+## Entry Points
154166
155−- Each package has isolated build configuration and can be developed independently
156−- Hot reload works across the full stack during development
157−- Node development uses dedicated `node-dev` CLI tool
158−- Workflow tests are JSON-based for integration testing
159−- AI features have dedicated development workflow (`pnpm dev:ai`)
167+All tests should start with `n8n.start.*` methods. See `composables/TestEntryComposer.ts`.
160168
161−### Workflow Traversal Utilities
169+| Method | Use Case |
170+|--------|----------|
171+| `fromHome()` | Start from home page |
172+| `fromBlankCanvas()` | New workflow from scratch |
173+| `fromNewProjectBlankCanvas()` | Project-scoped workflow (returns projectId) |
174+| `fromNewProject()` | Project-scoped test, no canvas (returns projectId) |
175+| `fromImportedWorkflow(file)` | Test pre-built workflow JSON |
176+| `withUser(user)` | Isolated browser context per user |
177+| `withProjectFeatures()` | Enable sharing/folders/permissions |
162178
163−The `n8n-workflow` package exports graph traversal utilities from
164−`packages/workflow/src/common/`. Use these instead of custom traversal logic.
179+## Test Isolation
165180
166−**Key concept:** `workflow.connections` is indexed by **source node**.
167−To find parent nodes, use `mapConnectionsByDestination()` to invert it first.
181+Tests run in parallel. Design tests to be fully isolated so they don't interfere with each other.
168182
183+### Unique Identifiers
184+
185+Use `nanoid` for unique test data:
186+
169187 ```typescript
170−import { getParentNodes, getChildNodes, mapConnectionsByDestination } from 'n8n-workflow';
188+const credentialName = `Test Credential ${nanoid()}`;
189+const workflow = await api.workflows.createWorkflow({
190+ name: `Test Workflow ${nanoid()}`,
191+});
192+```
171193
172−// Finding parent nodes (predecessors) - requires inverted connections
173−const connectionsByDestination = mapConnectionsByDestination(workflow.connections);
174−const parents = getParentNodes(connectionsByDestination, 'NodeName', 'main', 1);
194+### Dynamic User Creation
175195
176−// Finding child nodes (successors) - uses connections directly
177−const children = getChildNodes(workflow.connections, 'NodeName', 'main', 1);
196+Create users dynamically via the public API:
197+
198+```typescript
199+const member = await api.publicApi.createUser({
200+ email: `member-${nanoid()}@test.com`,
201+ firstName: 'Test',
202+ lastName: 'Member',
203+});
178204 ```
179205
180−### TypeScript Best Practices
181−- **NEVER use `any` type** - use proper types or `unknown`
182−- **Avoid type casting with `as`** - use type guards or type predicates instead (except in test code where `as` is acceptable)
183−- **Define shared interfaces in `@n8n/api-types`** package for FE/BE communication
184−- **Lazy-load heavy modules** — if a module is only used in a specific code
185− path (not every request), use `await import()` at point of use instead of
186− top-level `import`. Applies especially to native modules and large parsers.
206+### Isolated Browser Contexts
187207
188−### Error Handling
189−- Don't use the deprecated `ApplicationError` class anywhere — it's a
190− compatibility shim kept only so community nodes keep resolving. Use one of
191− these instead, picking by cause:
192− - `UserError` — the user caused it (invalid input, unauthorized action,
193− business-rule violation).
194− - `OperationalError` — a transient, expected issue (network request failing,
195− DB query timing out) that should be handled gracefully.
196− - `UnexpectedError` — a bug in the code (logic mistake, unhandled case,
197− failed assertion) that developers need to fix.
198−- Import from appropriate error classes in each package
208+For UI tests requiring multiple users, create isolated browser contexts:
199209
200−### Persistence layer & the TypeORM boundary
210+```typescript
211+// 1. Create users via public API
212+const member1 = await api.publicApi.createUser({ role: 'global:member' });
213+const member2 = await api.publicApi.createUser({ role: 'global:member' });
201214
202−TypeORM (`@n8n/typeorm`) must stay in the **persistence layer** — the `@n8n/db`
203−package or a backend module's own `database/` folder (entity/repository files).
204−Business logic — services, controllers, handlers, commands, factories — must not
205−import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). In
206−`packages/cli` this is enforced by the `misplaced-n8n-typeorm-import` lint rule;
207−a new import (or an inline `eslint-disable` of the rule) fails CI.
215+// 2. Get isolated browser contexts
216+const member1Page = await n8n.start.withUser(member1);
217+const member2Page = await n8n.start.withUser(member2);
208218
209−- **Pattern:** when a query needs operators (`In`, `IsNull`, `LessThan`,
210− `FindOptionsWhere`, …), put it behind a **use-case-named repository method**
211− that takes plain parameters and returns domain-shaped values — not a generic
212− `find(options)` passthrough.
213−- **Transactions:** transaction orchestration belongs in the persistence layer.
214− Don't reach for `.manager` / `.manager.transaction(...)` or
215− `createQueryBuilder(...)` in business logic. Use the sanctioned primitive in
216− `@n8n/db`: inject the abstract `TransactionRunner` and wrap the unit of work in
217− `txRunner.run(ctx, async (ctx) => …)`. The callback receives an
218− `OperationContext` carrying the active transaction; thread that `ctx` into the
219− repository methods you call. `run` **requires** a context — pass an empty `{}`
220− at the operation entry point, and reuse the one you were handed everywhere
221− below it (a context that already carries a transaction is joined, not nested).
222− Repositories extend `BaseRepository` and resolve the right `EntityManager` with
223− `this.managerFor(ctx)`; the `Transaction` handle is opaque and never exposes a
224− driver type to business logic. See `oauth-token.service.ts` +
225− `oauth-*-token.repository.ts` for a worked example.
226−- **Anti-patterns reviewers reject** — they hide the dependency instead of
227− removing it:
228− - String-matching TypeORM errors, e.g. `error.name === 'QueryFailedError'`.
229− - Relabeling the import from `@n8n/typeorm` to `@n8n/db` to silence the rule
230− (`@n8n/db` re-exports several operators/types, but this relabels the
231− dependency rather than removing it).
232− - Pushing `.manager` / `createQueryBuilder` into business logic to avoid an
233− operator import — trades a visible leak for an invisible one.
219+// 3. Each operates independently (no session bleeding)
220+await member1Page.navigate.toWorkflows();
221+await member2Page.navigate.toCredentials();
222+```
234223
235−### Frontend Development
236−- Refer to `packages/frontend/AGENTS.md`
237−- **All UI text must use i18n** - add translations to `@n8n/i18n` package
238−- **Use CSS variables directly** - never hardcode spacing as px values
239−- **data-testid must be a single value** (no spaces or multiple values)
240−- Always use `design-system-rules` skill in reviews
224+**Reference:** `tests/e2e/building-blocks/user-service.spec.ts`
241225
242−### Testing Guidelines
243−- **Always work from within the package directory** when running tests
244−- **Mock all external dependencies** in unit tests
245−- **Prefer reusing hoisted shared `mock<T>(...)` fixtures** when a typed mock is immutable and used across tests. This rule exists to avoid massive test slowdowns from repeatedly creating nested proxy mocks while preserving the type contract. Avoid replacing these with `as unknown as T` helpers for entities like `User`.
246−- **Confirm test cases with user** before writing unit tests
247−- **Typecheck is critical before committing** - always run `pnpm typecheck`
248−- **When modifying pinia stores**, check for unused computed properties
249−- **For Vitest packages that use `@n8n/di` decorators**, use `createVitestConfigWithDecorators` from `@n8n/vitest-config/node-decorators`. It enables SWC `decoratorMetadata` (esbuild doesn't emit it) and externalizes workspace packages that register services (`@n8n/di`, `@n8n/config`, `@n8n/constants`, `n8n-workflow`) so a single DI `Container` instance is shared across the runtime. Loading them through Vitest's pipeline alongside their CJS dist produces two `Container`s and `Container.get(...)` returns `undefined`.
226+| Pattern | Why | Use Instead |
227+|---------|-----|-------------|
228+| `test.describe.serial` | Creates test dependencies | Parallel tests with isolated setup |
229+| Fresh DB per file | Tests need isolated container | `test.use({ capability: { env: { TEST_ISOLATION: 'name' } } })` |
230+| Fresh DB per test | Tests modify shared state | `@db:reset` tag on describe (container-only, combined with `test.use()`) |
231+| `n8n.api.signin()` | Session bleeding | `n8n.start.withUser()` |
232+| `Date.now()` for IDs | Race conditions | `nanoid()` |
233+| `waitForTimeout()` | Flaky | `waitForResponse()`, `toBeVisible()` |
234+| `.toHaveCount(N)` | Brittle | Named element assertions |
235+| Raw `page.goto()` | Bypasses setup | `n8n.navigate.*` methods |
250236
251−What we use for testing and writing tests:
252−- For testing nodes and other backend components, we use Vitest for unit tests. Examples can be found in `packages/nodes-base/nodes/**/*test*`.
253−- We use `nock` for server mocking
254−- For frontend we use `vitest`
255−- For E2E tests we use Playwright. Run with `pnpm --filter=n8n-playwright test:local`.
256− See `packages/testing/playwright/README.md` for details.
257−- **To iterate on a feature without docker rebuilds**, boot service containers
258− and run `pnpm dev` locally — `pnpm --filter n8n-containers services --services postgres,redis,mailpit,proxy`
259− then `pnpm dev`. See [Develop against running containers](packages/testing/playwright/README.md#develop-against-running-containers-avoid-docker-rebuilds).
260−- **For Playwright test maintenance/cleanup**, see `packages/testing/playwright/AGENTS.md` (includes janitor tool for static analysis, dead code removal, architecture enforcement, and TCR workflows).
237+## Code Style
261238
262−### Common Development Tasks
239+- Use specialized locators: `page.getByRole('button')` over `page.locator('[role=button]')`
240+- Use `nanoid()` for unique identifiers (parallel-safe)
241+- API setup over UI setup when possible (faster, more reliable)
263242
264−When implementing features:
265−1. Define API types in `packages/@n8n/api-types`
266−2. Implement backend logic in `packages/cli` module, follow
267− `scripts/backend-module/backend-module-guide.md`
268−3. Add API endpoints via controllers
269−4. Update frontend in `packages/frontend/editor-ui` with i18n support
270−5. Write tests with proper mocks
271−6. Run `pnpm typecheck` to verify types
243+## Architecture
272244
273−## Design Principles
245+```
246+Tests (*.spec.ts)
247+ ↓ uses
248+Composables (*Composer.ts) - Multi-step business workflows
249+ ↓ orchestrates
250+Page Objects (*Page.ts) - UI interactions
251+ ↓ extends
252+BasePage - Common utilities
253+```
274254
275−### Security Must Not Degrade the Building Experience
255+See `CONTRIBUTING.md` for detailed patterns and conventions.
276256
277−Security improvements, whether driven by enterprise requirements or internal
278−standards, must NEVER add friction to the common-case building experience. When
279−designing security-related features (defaults, behaviors, flows, error
280−handling), apply these checks:
257+## Debugging
281258
282−- **No friction for the common case:** A community builder's workflow should
283− remain intuitive. Security should be invisible when it can be.
284−- **Migration and upgrade paths:** Existing users must have a clear,
285− non-disruptive path forward when defaults or behaviors change.
286−- **Security layers on top, not in competition:** Great UX and strong security
287− are not trade-offs. They're both required. If a design forces a choice
288− between them, the design needs more work.
259+See [README.md#debugging](./README.md#debugging) for detailed instructions on:
260+- **Keepalive mode** - Keep containers running after tests with `N8N_CONTAINERS_KEEPALIVE=true`
261+- **Victoria exports** - Logs/metrics automatically attached on failure, importable locally via `scripts/import-victoria-data.mjs`
289262
290−### Security Fix Hygiene
263+## Test Migration & Refactoring
291264
292−**This is a public repository.** When working on security fixes, never expose
293−the attack vector or vulnerability type in any public-facing artifact. Attackers
294−monitor open-source repos for signals like branch names, commit messages, PR
295−titles, test descriptions, and Linear URLs.
265+**Test Name = Contract**
266+- Name declares intent, assertion proves it, everything else is flow
267+- Bad: `should open W1 as U2` (describes action)
268+- Good: `should allow sharee to edit shared workflow` (declares rule)
296269
297−**Rules for security fixes:**
270+**Coverage Parity Check**
271+1. Read old test name → what was the intent?
272+2. Find the explicit assertion that proved it
273+3. Verify new test has equivalent proof
274+4. No proof found? Document as intentional drop or gap
298275
299−- **Branch names:** Do NOT use the Linear-suggested branch name if it reveals
300− the vulnerability. Rename to describe the fix neutrally
301− (e.g. `node-1234-improve-request-handling`, not
302− `node-1234-fix-ddos-vulnerability`).
303−- **Commit messages:** Describe what the code now does, not the threat it
304− prevents (e.g. `fix: add payload size validation`, not
305− `fix: prevent denial of service`).
306−- **Test descriptions:** Use neutral, functional language
307− (e.g. `'should sanitize query parameters'`, not
308− `'should prevent SQL injection'`).
309−- **Code comments:** Do not describe the attack scenario in comments.
310−- **Linear references:** Never include the URL slug
311− (e.g. `.../N8N-1234/fix-ssrf-vulnerability`).
276+**Legacy Tests (unauditable names/assertions)**
277+- Prioritize clarity over parity - can't audit what you can't read
278+- Document your best interpretation of intent
279+- Accept short-term risk, fix regressions forward
312280
313−### Customer Confidentiality
281+See [Quality Corner: Test Migration Guide](https://www.notion.so/n8n/Best-Practices-Test-Migration-Refactoring) for full rationale and examples.
314282
315−**This is a public repository.** Never mention customer names in any
316−public-facing artifact — not all customers have agreed to be named publicly,
317−and naming them can reveal security-relevant details about their setup.
283+## Reference Files
318284
319−This applies to PR titles and descriptions, branch names, commit messages,
320−code, code comments, test names and test data, and fixtures. When implementing
321−a customer request, describe the use case neutrally (e.g. "a customer with a
322−large multi-main setup", not the company name) and use generic placeholder
323−names (e.g. `Acme Corp`) in tests and examples.
285+| Purpose | File |
286+|---------|------|
287+| Multi-user testing | `tests/e2e/building-blocks/user-service.spec.ts` |
288+| Entry points | `composables/TestEntryComposer.ts` |
289+| Page object example | `pages/CanvasPage.ts` |
290+| Composable example | `composables/WorkflowComposer.ts` |
291+| API helpers | `services/api-helper.ts` |
292+| Capabilities | `fixtures/capabilities.ts` |
324293
325−## Github Guidelines
326−- When creating a PR, use the conventions in
327− `.github/pull_request_template.md` and
328− `.github/pull_request_title_conventions.md`.
329−- Use `gh pr create --draft` to create draft PRs.
330−- If there is a corresponding Linear ticket, reference it in the PR
331− description using `https://linear.app/n8n/issue/[TICKET-ID]`. Do not
332− create a Linear ticket on your own — ask first.
333−- always link to the github issue if mentioned in the linear ticket.
294+```typescript
295+const member = await api.publicApi.createUser({...});
296+const memberN8n = await n8n.start.withUser(member);
297+
298+await memberN8n.navigate.toWorkflows();
299+await expect(memberN8n.workflows.cards.getWorkflow(workflowName)).toBeVisible();
300+```
301+### Isolated API Contexts
302+
303+For API-only operations as another user, create isolated API contexts (no browser needed):
304+
305+```typescript
306+const member = await api.publicApi.createUser({...});
307+const memberApi = await api.createApiForUser(member);
308+
309+const memberProject = await memberApi.projects.getMyPersonalProject();
310+await memberApi.credentials.createCredential({...});
311+```
312+
313+### Identity-Based Assertions
314+
315+Assert by identity (name) rather than count for parallel-safe tests:
316+
317+```typescript
318+await expect(credentialDropdown.getByText(testCredName)).toBeVisible();
319+await expect(credentialDropdown.getByText(devCredName)).toBeHidden();
320+```
321+
322+## Worker Isolation (Fresh Database)
323+
324+Use `test.use()` at file top-level with unique capability config:
325+
326+```typescript
327+// my-isolated-tests.spec.ts
328+import { test, expect } from '../fixtures/base';
329+
330+// Must be top-level, not inside describe block
331+test.use({ capability: { env: { TEST_ISOLATION: 'my-isolated-tests' } } });
332+
333+test('test with clean state', async ({ n8n }) => {
334+ // Fresh container with reset database
335+});
336+```
337+
338+For per-test database reset (when tests modify shared state like MFA), add `@db:reset` to the describe. **Note:** `@db:reset` is container-only - these tests won't run locally.
339+
340+```typescript
341+test.use({ capability: { env: { TEST_ISOLATION: 'my-stateful-tests' } } });
342+
343+test.describe('My stateful tests @db:reset', () => {
344+ // Each test gets a fresh database reset (container-only)
345+});
346+```
347+
348+## Data Setup
349+
350+Use API helpers for fast, reliable test data setup. Reserve UI interactions for testing UI behavior:
351+
352+```typescript
353+// API for data setup
354+const credential = await api.credentials.createCredential({
355+ name: `Test Credential ${nanoid()}`,
356+ type: 'notionApi',
357+ data: { apiKey: 'test' },
358+});
359+
360+const workflow = await api.workflows.createWorkflow({
361+ name: `Test Workflow ${nanoid()}`,
362+ nodes: [...],
363+});
364+
365+// UI for verification
366+await n8n.navigate.toCredentials();
367+await expect(n8n.credentials.cards.getCredential(credential.name)).toBeVisible();
368+```
369+
370+## Feature Enablement
371+
372+The `n8n` fixture automatically enables project features. For API-only tests (no `n8n` fixture), enable features explicitly:
373+
374+```typescript
375+test('API-only test', async ({ api }) => {
376+ await api.enableProjectFeatures();
377+ // ...
378+});
379+```
380+
381+### Feature Flag Overrides
382+
383+To test features behind feature flags (experiments), use `TestRequirements` with storage overrides:
384+
385+```typescript
386+import type { TestRequirements } from '../../../Types';
387+
388+const requirements: TestRequirements = {
389+ storage: {
390+ N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ 'your_experiment': true }),
391+ },
392+};
393+
394+test.use({ requirements });
395+
396+test('test with feature flag enabled', async ({ n8n }) => {
397+ // Feature flag is now active for this test
398+});
399+```
400+
401+**Common patterns:**
402+
403+```typescript
404+// Single experiment
405+{ storage: { N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ '025_new_canvas': true }) } }
406+
407+// Multiple experiments
408+{ storage: { N8N_EXPERIMENT_OVERRIDES: JSON.stringify({
409+ '025_new_canvas': true,
410+ '026_another_feature': 'variant_a'
411+}) } }
412+
413+// Combined with other requirements
414+const requirements: TestRequirements = {
415+ storage: {
416+ N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ 'your_experiment': true }),
417+ },
418+ capability: {
419+ env: { TEST_ISOLATION: 'my-test-suite' },
420+ },
421+};
422+```
423+
424+**Reference:** `Types.ts` for the full interface definition. Import depth follows
425+the spec's own nesting. The example above assumes `tests/e2e/<area>/`; add one
426+`../` per extra level down.
427+
428+## Shard Rebalancing
429+
430+When refactoring, adding, or moving significant numbers of tests, consider rebalancing test shards to maintain even CI distribution. See `docs/ORCHESTRATION.md` for details.
334431
