# AGENTS.md This file provides guidance on how to work with the n8n repository. ## Project Overview n8n is a workflow automation platform written in TypeScript, using a monorepo structure managed by pnpm workspaces. It consists of a Node.js backend, Vue.js frontend, and extensible node-based workflow engine. ## General Guidelines - Always use pnpm - Write all technical text (code comments, PR descriptions, issue and ticket descriptions, docs) in ASD-STE100 Simplified Technical English: use short sentences, the active voice, and one instruction for each sentence - **Secrets on the command line:** if a developer opted into anonymous dev metrics (`scripts/dev-metrics`), pnpm command arguments are recorded. Arguments of secret-carrying words (`config`, `login`, `publish`, `token`) — whether a subcommand or baked into a flag — are dropped, and the home dir is stripped from paths, but other args are sent as-is — so never put secrets in a command. Pass sensitive values via environment variables, which are never captured. - When adding comments, keep them concise and to the point - explain the "why" in a line or two; don't be overly verbose. Comments should be scoped and relevant to the surrounding code, not just to the current task - We use Linear as a ticket tracking system - We use Posthog for feature flags - To find registered telemetry events (names, descriptions, properties), run `pnpm --filter @n8n/telemetry catalog` (`--json` for structured output). The registry is being adopted incrementally, so search call sites if the catalog has no match. The `n8n:telemetry` skill covers adding or changing events - When starting to work on a new ticket – create a new branch from fresh master with the name specified in Linear ticket - When creating a new branch for a ticket in Linear - use the branch name suggested by Linear, **unless it is a security fix** (see Security Fix Hygiene below) - Use mermaid diagrams in MD files when you need to visualise something - **Developing v3 features:** land normal feature work on `master` behind an opt-in flag; introduce breaking changes only on the `3.x` branch. See [.github/DEVELOPING_V3.md](.github/DEVELOPING_V3.md). - The AI gateway feature is **"Gateway credits"** in user-facing text (UI copy, error messages, prompts). Only internal identifiers, i18n keys, telemetry, and comments keep the historical `n8nConnect` / `n8n credits` / AI Gateway names - **Shared utilities:** before you hand-roll a utility (`isRecord`, secret or PII redaction, JSON extraction from LLM output, Zod to JSON Schema, model-id parsing, AI client HTTP transport and response-size limiting, …), you MUST check the shared packages for an existing implementation and use it: `@n8n/utils` (generic helpers, redaction), `@n8n/ai-utilities` (AI- and LLM-specific helpers) and `n8n-workflow` (workflow graph and traversal). A new shared helper usually belongs in one of these packages too; domain logic stays in the package that owns the domain. ## Agent Skills and Claude Code Plugin n8n shared skills live in `.agents/skills/`. Claude Code consumes them through symlinks in `.claude/plugins/n8n/skills/`; OpenCode reads `.agents/skills/` directly. Harness-specific overrides remain real directories in the harness path, such as `.opencode/skills/setup-mcps/`. See [skills README](.agents/skills/AGENTS.md) for editing and sync guidance. n8n-specific Claude Code commands and agents live in `.claude/plugins/n8n/` and are namespaced under `n8n:`. Use `n8n:` prefix when invoking them (e.g. `/n8n:create-pr`, `/n8n:plan`, `n8n:developer` agent). See [plugin README](.claude/plugins/n8n/README.md) for structure and details. ## Essential Commands For full-repo lint and typecheck, use `pnpm agent:lint` and `pnpm agent:typecheck`. They save full logs and return compact results. For focused tests, use `pnpm agent:test` or `pnpm agent:playwright`. Run each command with `--help` for its package, script, and file options. ### Fresh checkout / agent setup For a fresh checkout (cat-bot, a new hire, any agent verifying the repo builds), prefer `pnpm agent:setup` over running install + build + tests by hand. It chains them in one process, caps per-process memory and turbo concurrency so a 6GB box doesn't OOM, streams all output to `.agent-setup/.log` (gitignored), and surfaces only a one-line summary per step plus the tail of the failing log. A machine-readable `.agent-setup/summary.json` is always written so a backgrounded run is readable in a single shot — no polling, no scrolling logs. ```bash pnpm agent:setup # install → build → test (full suite) pnpm agent:setup install # one step at a time pnpm agent:setup --json # JSON summary on stdout (for scripts/agents) ``` ### Building Use `pnpm build` to build all packages. ALWAYS redirect the output of the build command to a file: ```bash pnpm build > build.log 2>&1 ``` You can inspect the last few lines of the build log file to check for errors: ```bash tail -n 20 build.log ``` If build outputs or the turbo cache are stale (e.g. after switching branches or worktrees) but dependencies haven't changed, use `pnpm reset` (lightweight by default) for a fast recovery: it cleans build outputs and force-rebuilds (keeping `node_modules` and untracked files). If that doesn't fix your issue, use `pnpm reset --full`, which also wipes untracked files and reinstalls dependencies. ### Seeding a local instance An empty instance is a bad place to test anything that reads a user's work. These commands fill one. They are dev tooling on the private root package, so they never reach a user. ```bash N8N_API_KEY= pnpm seed:preference # 10 workflows in one house style, plus history N8N_API_KEY= pnpm seed:account # ~500 varied workflows across 30 projects pnpm inspect:activity # read-only activity_event viewer on 127.0.0.1 ``` Both seed profiles delete their own prior output, so a re-run replaces it. That clear step deletes anything named `[seed]` and any empty team project, whoever made them, so do not point either at a shared instance. The viewer is unauthenticated and serves the whole table: keep it on loopback. See [scripts/instance-seeding/AGENTS.md](scripts/instance-seeding/AGENTS.md) for profiles, tokens, determinism, and the other commands. ## Architecture Overview **Monorepo Structure:** pnpm workspaces with Turbo build orchestration ### Package Structure The monorepo is organized into these key packages: - **`packages/@n8n/api-types`**: Shared TypeScript interfaces between frontend and backend - **`packages/workflow`**: Core workflow interfaces and types - **`packages/core`**: Workflow execution engine - **`packages/cli`**: Express server, REST API, and CLI commands - **`packages/frontend/editor-ui`**: Vue 3 frontend application - **`packages/frontend/@n8n/i18n`**: Internationalization for UI text - **`packages/nodes-base`**: Built-in nodes for integrations - **`packages/@n8n/nodes-langchain`**: AI/LangChain nodes - **`packages/@n8n/instance-ai`**: "n8n Assistant" in the UI, "Instance AI" in code — n8n Assistant backend. See its `CLAUDE.md` for architecture docs. - **`@n8n/design-system`**: Vue component library for UI consistency - **`@n8n/config`**: Centralized configuration management ## Technology Stack - **Frontend:** Vue 3 + TypeScript + Vite + Pinia + Storybook UI Library - **Backend:** Node.js + TypeScript + Express + TypeORM - **Testing:** Vitest (unit) + Playwright (UI, API, infrastructure, lifecycle, performance, and E2E orchestration) - **Database:** TypeORM with SQLite/PostgreSQL support - **Code Quality:** Biome (for formatting) + ESLint + lefthook git hooks ### Key Architectural Patterns 1. **Dependency Injection**: Uses `@n8n/di` for IoC container 2. **Controller-Service-Repository**: Backend follows MVC-like pattern 3. **Event-Driven**: Internal event bus for decoupled communication 4. **Context-Based Execution**: Different contexts for different node types 5. **State Management**: Frontend uses Pinia stores 6. **Design System**: Reusable components and design tokens are centralized in `@n8n/design-system`, where all pure Vue components should be placed to ensure consistency and reusability ## Key Development Patterns - Each package has isolated build configuration and can be developed independently - Hot reload works across the full stack during development - Node development uses dedicated `node-dev` CLI tool - Workflow tests are JSON-based for integration testing - AI features have dedicated development workflow (`pnpm dev:ai`) ### Workflow Traversal Utilities The `n8n-workflow` package exports graph traversal utilities from `packages/workflow/src/common/`. Use these instead of custom traversal logic. **Key concept:** `workflow.connections` is indexed by **source node**. To find parent nodes, use `mapConnectionsByDestination()` to invert it first. ```typescript import { getParentNodes, getChildNodes, mapConnectionsByDestination } from 'n8n-workflow'; // Finding parent nodes (predecessors) - requires inverted connections const connectionsByDestination = mapConnectionsByDestination(workflow.connections); const parents = getParentNodes(connectionsByDestination, 'NodeName', 'main', 1); // Finding child nodes (successors) - uses connections directly const children = getChildNodes(workflow.connections, 'NodeName', 'main', 1); ``` ### TypeScript Best Practices - **NEVER use `any` type** - use proper types or `unknown` - **Avoid type casting with `as`** - use type guards or type predicates instead (except in test code where `as` is acceptable) - **Define shared interfaces in `@n8n/api-types`** package for FE/BE communication - **Lazy-load heavy modules** — if a module is only used in a specific code path (not every request), use `await import()` at point of use instead of top-level `import`. Applies especially to native modules and large parsers. ### Error Handling - Don't use the deprecated `ApplicationError` class anywhere — it's a compatibility shim kept only so community nodes keep resolving. Use one of these instead, picking by cause: - `UserError` — the user caused it (invalid input, unauthorized action, business-rule violation). - `OperationalError` — a transient, expected issue (network request failing, DB query timing out) that should be handled gracefully. - `UnexpectedError` — a bug in the code (logic mistake, unhandled case, failed assertion) that developers need to fix. - Import from appropriate error classes in each package ### Persistence layer & the TypeORM boundary TypeORM (`@n8n/typeorm`) must stay in the **persistence layer** — the `@n8n/db` package or a backend module's own `database/` folder (entity/repository files). Business logic — services, controllers, handlers, commands, factories — must not import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). In `packages/cli` this is enforced by the `misplaced-n8n-typeorm-import` lint rule; a new import (or an inline `eslint-disable` of the rule) fails CI. - **Pattern:** when a query needs operators (`In`, `IsNull`, `LessThan`, `FindOptionsWhere`, …), put it behind a **use-case-named repository method** that takes plain parameters and returns domain-shaped values — not a generic `find(options)` passthrough. - **Transactions:** transaction orchestration belongs in the persistence layer. Don't reach for `.manager` / `.manager.transaction(...)` or `createQueryBuilder(...)` in business logic. Use the sanctioned primitive in `@n8n/db`: inject the abstract `TransactionRunner` and wrap the unit of work in `txRunner.run(ctx, async (ctx) => …)`. The callback receives an `OperationContext` carrying the active transaction; thread that `ctx` into the repository methods you call. `run` **requires** a context — pass an empty `{}` at the operation entry point, and reuse the one you were handed everywhere below it (a context that already carries a transaction is joined, not nested). Repositories extend `BaseRepository` and resolve the right `EntityManager` with `this.managerFor(ctx)`; the `Transaction` handle is opaque and never exposes a driver type to business logic. See `oauth-token.service.ts` + `oauth-*-token.repository.ts` for a worked example. - **Anti-patterns reviewers reject** — they hide the dependency instead of removing it: - String-matching TypeORM errors, e.g. `error.name === 'QueryFailedError'`. - Relabeling the import from `@n8n/typeorm` to `@n8n/db` to silence the rule (`@n8n/db` re-exports several operators/types, but this relabels the dependency rather than removing it). - Pushing `.manager` / `createQueryBuilder` into business logic to avoid an operator import — trades a visible leak for an invisible one. ### ESLint configuration layers Rule policy lives in four shared configs in `@n8n/eslint-config`, and a package config picks exactly one: | layer | subpath | for | |---|---|---| | `baseConfig` | `@n8n/eslint-config/base` | runtime-agnostic libraries | | `backendConfig` | `@n8n/eslint-config/backend` | anything that runs on Node; adds the network and encryption boundaries | | `frontendConfig` | `@n8n/eslint-config/frontend` | Vue packages | | `nodesConfig` | `@n8n/eslint-config/nodes` | `n8n-nodes-base` and `@n8n/nodes-langchain`; adds the node and credential file rules | A package config may add `ignores`, an additive plugin config, a block that raises rules to `error`, and blocks scoped to `files`. It must not turn a rule down for the whole package: every lint script runs with `--quiet`, so a `warn` enforces nothing and reads as if it did. The code-health rule `lint-config-layering` enforces this, with existing debt in `.code-health-baseline.json`, which only shrinks. To stop enforcing a rule everywhere, retire it in `base.ts` with the count behind the decision. To enforce one again in a package that is ready, set it to `error` there. `node scripts/lint-parity/majority.mjs` prints how many packages downgrade each rule, and `scripts/lint-parity/snapshot.mjs` plus `diff.mjs` prove a config change only altered what you meant it to. ### Encryption boundary New code encrypts and decrypts only through `cipher.encryptV2()` / `cipher.decryptV2()` — the key-manager module decides which key is used and in which output format. Enforced in CI by the rules in `packages/@n8n/eslint-config/src/configs/encryption-boundary.ts` (part of `backendConfig`, and so of `nodesConfig`; every package that runs on Node extends one of those layers): - `Cipher` does not expose the legacy or explicit-key methods. - The raw AES classes stay inside `packages/core/src/encryption/`. - **Deployment keys are never deleted** — data encrypted with a key becomes unreadable without it. Deactivate keys instead; the repository does not expose deletion and database triggers reject direct deletion. - Inline disables that name these rules, and bare line-form disables, are themselves lint errors. The code-health rule `encryption-boundary` (CI "Static Analysis") is the enforcement layer: it checks that every package that depends on `n8n-core` or `@n8n/db` extends `backendConfig` (or `nodesConfig`) at `error` severity, and rejects every directive form that would silence the rules in non-test code (`eslint-disable*` and inline `eslint` configuration comments). Widening the boundary happens in `encryption-boundary.ts` only; that file and the rule files require security (IAM) approval via OWNERS. ### Frontend Development - Refer to `packages/frontend/AGENTS.md` - **All UI text must use i18n** - add translations to `@n8n/i18n` package - **Use CSS variables directly** - never hardcode spacing as px values - **data-testid must be a single value** (no spaces or multiple values) - Always use the `design-system` skill in reviews ### Verify changes - Run focused tests from the owning package: `pnpm test `. - Run that package's `pnpm lint` and `pnpm typecheck` before committing code. Build first when shared types or cross-package dependencies change. - Use Vitest for unit tests. Use [Playwright](packages/quality/testing/playwright/AGENTS.md) when a test needs its browser, fixtures, or managed containers. - For Vitest packages with `@n8n/di` decorators, use `createVitestConfigWithDecorators` from `@n8n/vitest-config/node-decorators`. - Check import and mock side effects before running tests. Keep tests out of user-owned directories. Set `N8N_USER_FOLDER` to a test-owned directory before importing n8n settings. Clean up only paths that the test created. - CI runs [`@n8n/code-health`](packages/quality/policy/code-health/README.md) static analysis on PRs. It checks monorepo rules, including dependency hygiene and encryption-boundary coverage. ### Local development | Goal | Command | |------|---------| | Run product E2E against a local instance | `pnpm --filter=n8n-playwright test:local` | | Run backend with PostgreSQL, Redis, email, and proxy services | `pnpm --filter n8n-containers services --services postgres,redis,mailpit,proxy`, then `pnpm dev:be` | | Add editor hot reload | `pnpm dev:fe:editor` | | Start a Codespace backend and share its port | `pnpm dev:up` | The root `pnpm dev` command does not start a server. See the [Playwright guide](packages/quality/testing/playwright/README.md) and the [Codespaces guide](.devcontainer/codespaces/README.md) for details. ### Common Development Tasks When implementing features: 1. Define API types in `packages/@n8n/api-types` 2. Implement backend logic in `packages/cli` module, follow `scripts/backend-module/backend-module-guide.md` 3. Add API endpoints via controllers 4. Update frontend in `packages/frontend/editor-ui` with i18n support. For a frontend feature module, obey `packages/@n8n/module-cli/frontend-module-guide.md` 5. Write tests with proper mocks ## Design Principles ### Security Must Not Degrade the Building Experience Security improvements, whether driven by enterprise requirements or internal standards, must NEVER add friction to the common-case building experience. When designing security-related features (defaults, behaviors, flows, error handling), apply these checks: - **No friction for the common case:** A community builder's workflow should remain intuitive. Security should be invisible when it can be. - **Migration and upgrade paths:** Existing users must have a clear, non-disruptive path forward when defaults or behaviors change. - **Security layers on top, not in competition:** Great UX and strong security are not trade-offs. They're both required. If a design forces a choice between them, the design needs more work. ### Security Fix Hygiene **This is a public repository.** When working on security fixes, never expose the attack vector or vulnerability type in any public-facing artifact. Attackers monitor open-source repos for signals like branch names, commit messages, PR titles, test descriptions, and Linear URLs. **Rules for security fixes:** - **Branch names:** Do NOT use the Linear-suggested branch name if it reveals the vulnerability. Rename to describe the fix neutrally (e.g. `node-1234-improve-request-handling`, not `node-1234-fix-ddos-vulnerability`). - **Commit messages:** Describe what the code now does, not the threat it prevents (e.g. `fix: add payload size validation`, not `fix: prevent denial of service`). - **Test descriptions:** Use neutral, functional language (e.g. `'should sanitize query parameters'`, not `'should prevent SQL injection'`). - **Code comments:** Do not describe the attack scenario in comments. - **Linear references:** Never include the URL slug (e.g. `.../N8N-1234/fix-ssrf-vulnerability`). ### Customer Confidentiality **This is a public repository.** Never mention customer names in any public-facing artifact — not all customers have agreed to be named publicly, and naming them can reveal security-relevant details about their setup. This applies to PR titles and descriptions, branch names, commit messages, code, code comments, test names and test data, and fixtures. When implementing a customer request, describe the use case neutrally (e.g. "a customer with a large multi-main setup", not the company name) and use generic placeholder names (e.g. `Acme Corp`) in tests and examples. ## Github Guidelines - When creating a PR, use the conventions in `.github/pull_request_template.md` and `.github/pull_request_title_conventions.md`. - Use `gh pr create --draft` to create draft PRs. - If there is a corresponding Linear ticket, reference it in the PR description using `https://linear.app/n8n/issue/[TICKET-ID]`. Do not create a Linear ticket on your own — ask first. - always link to the github issue if mentioned in the linear ticket.