9.2 KiB
Developing v3 features
n8n is preparing a v3 major release (~October 2026). v3 is an operational release: breaking changes, removals, and legacy cleanup — not a big-bang feature launch. From July until release we keep two long-lived branches alive at once, and this guide explains how to develop against them without friction.
TL;DR
- Normal feature work → land on
master, behind an opt-in feature flag.- Breaking changes → a separate PR targeting
3.xdirectly. Never onmaster.masteris synced into3.xdaily, automatically.
The branch model
3.x is not a divergent fork — it is simply "whatever is on master, plus the
breaking-change commits". A daily sync keeps that literally true by replaying those
commits on top of master, so merging 3.x back into master at release time is painless.
flowchart LR
subgraph master["master (v2 line)"]
F["Feature work<br/>(behind opt-in flags)"]
D["Deprecation notices"]
end
subgraph threex["3.x (v3 line)"]
B["Breaking-change PRs"]
end
F --> master
D --> master
master -- "daily sync: replay 3.x commits<br/>onto master (util-sync-master-to-3x.yml)" --> threex
B --> threex
threex -. "merged to master at v3 release" .-> master
| You want to… | Where it goes | How |
|---|---|---|
| Ship a new feature/behavior | master |
Behind an opt-in flag (see below) |
| Warn engineers a function is going away | master |
Add a deprecation notice so usage drops before v3 |
| Create a migration | master |
Create a non-destructive migration in master. After the release of v3, you can create the destructive part of migration if needed |
| Remove/change something in a breaking way | 3.x |
A separate PR targeting 3.x directly |
Developing a normal feature on master (behind an opt-in flag)
Land new implementations on master disabled by default, so they ride the daily
sync into 3.x and can be trialed without affecting v2 users. n8n uses PostHog
for flags, evaluated server-side and bootstrapped to the frontend.
Frontend (editor-ui)
- Register the experiment in
packages/frontend/editor-ui/src/app/constants/experiments.tswithcreateExperiment, using the next numeric index prefix:
Add its name toexport const MY_V3_FEATURE_EXPERIMENT = createExperiment('0XX_my_v3_feature');EXPERIMENTS_TO_TRACKif it should emit exposure telemetry. - Gate the code via the PostHog store — for a boolean opt-in flag use
isFeatureEnabled:const posthog = usePostHogStore(); if (posthog.isFeatureEnabled(MY_V3_FEATURE_EXPERIMENT.name)) { // new v3 behavior } - Put per-experiment code in its own folder under
packages/frontend/editor-ui/src/experiments/<name>/.
The n8n:experiments skill (.agents/skills/experiments/)
is the authoritative, step-by-step procedure — including creating the disabled
PostHog flags in Staging/Production first.
Backend (cli / config)
A backend opt-in flag is three small pieces (worked example: the
084_eval_collections flag):
- Flag key constant in
@n8n/api-types(e.g.EVAL_COLLECTIONS_FLAG = '084_eval_collections'inpackages/@n8n/api-types/src/schemas/eval-collections.schema.ts). - Env toggle — an
@Env('N8N_...')boolean defaulting tofalsein a@n8n/configconfig class (e.g.N8N_EVAL_COLLECTIONS_ENABLEDinpackages/@n8n/config/src/configs/evaluation.config.ts). - Override wiring in
PostHogClient.applyEnvOverrides()(packages/cli/src/posthog/index.ts) — force-enable the flag when the env toggle is on:
The override is force-enable only;if (this.globalConfig.evaluation.collectionsEnabled) { overrides[EVAL_COLLECTIONS_FLAG] = true; }falsedefers to PostHog.
Evaluated flags flow to the frontend through the login / current-user response, so a single flag key can gate both backend and frontend behavior.
Testing behind a flag
Override flags locally without touching PostHog:
- Browser:
window.featureFlags.override('0XX_my_v3_feature', true). - Playwright: set the storage override in
TestRequirements:test.use({ requirements: { storage: { N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ '0XX_my_v3_feature': true }) }, } });
Introducing a breaking change (on 3.x)
Breaking changes go only on 3.x, via a PR that targets 3.x directly. Branch off
latest master and open the PR against 3.x — since 3.x is master plus the breaking
commits, such a PR merges cleanly and is immune to the daily force-push (see
How the daily sync works). Do not land breaking changes on
master — the sync guarantees master stays releasable as v2.
- Track the change in the v3 breaking-changes tracker and the Release v3 Linear project.
- Follow the
BREAKING CHANGE:PR-title convention (seepull_request_title_conventions.md).
Deprecations land on master. If you plan to remove a function/class in v3,
add a deprecation notice on master first so other engineers reduce usage ahead
of the breaking removal on 3.x.
How the daily sync works
util-sync-master-to-3x.yml runs daily and
replays the 3.x-only commits on top of master (a rebase), then force-pushes 3.x:
- Nothing to do when
3.xalready containsmaster. - Replay + force-push otherwise. A clean sync adds no commit of its own —
3.xstays literally "master + the breaking commits", every replayed commit kept as-is with its original message and author. Nothing is ever squashed. Merge commits in the range (breaking PRs merged into3.x) are flattened away, and a breaking commit that also landed onmasteris dropped as empty. - On a conflict,
3.xis left untouched and a draft conflict PR (labeledautomation:v3-sync) carrying the conflict markers is opened onsync/master-to-3x, plus a post to the#alerts-v3-syncSlack channel. Syncs pause until that PR is merged — so conflicts never pile up silently.
Whatever route it takes, the sync verifies that the content it is about to push is exactly
the tree a merge of 3.x and master produces (git merge-tree), and that no conflict
markers are present. Either check failing fails the run instead of rewriting 3.x.
3.xis force-pushed daily. Branch breaking-change PRs offmasterand target3.x— then your merge-base is amastercommit that survives every rewrite and your diff stays clean. If you branch off3.xitself, re-base after a sync (git rebase --onto origin/3.x <old-3.x-tip> <your-branch>). Commit links on already-merged3.xPRs keep working but point at commits no longer on any branch.
Resolving a conflict PR
The conflict branch is master merged into 3.x with the conflict markers committed, so
you see exactly what clashed — and the required checks stay red until they're gone, so the PR
can't be merged half-resolved:
git fetch origin sync/master-to-3x && git switch sync/master-to-3x
# fix the conflict markers, then commit them in ONE commit of your own
git push origin sync/master-to-3x
Then merge the PR with the normal merge button. master's commits arrive as-is and your
fix stays its own commit.
3.x never holds markers at its tip, so nightly images keep building; the merge commit that
carries them drops out of 3.x's history at the next sync (the replay takes the queue's
commits only).
The next sync then makes 3.x linear again. The plain replay stalls at that point (a fix
recorded around a merge commit leaves no patch to replay), so it replays a second time with
3.x's side favoured, and your fix commit — which is in the queue — does the real work.
Nothing is squashed, and the tree guard proves the result is exactly the merge of 3.x and
master.
Who gets pinged. The conflict is attributed to the authors of the 3.x commits behind the
conflicted files (.github/scripts/sync-conflict-owners.mjs, mapped to GitHub accounts).
Those authors are requested as reviewers on the conflict PR and listed in the
#alerts-v3-sync message.
Trialing v3
3.x publishes nightly Docker images (see
build-v3-nightly.yml):
docker pull n8nio/n8n:v3-nightly # latest v3 nightly
docker pull n8nio/n8n:v3-nightly-20260625 # a specific build date
Use these to trial v3 in docker/kubernetes before release. Do not use them in production.
See also
.github/WORKFLOWS.md— full CI/CD + release lifecycle.- Root
AGENTS.md— general repo guidance. - Branching strategy & releases (Notion).