Managing objects promotion across instances in an enterprise environment

Describe the problem/error/question

Selective, per-user promotion between two n8n environments — is there a better pattern than the default Git flow?

Hi all,

We’re running two n8n environments (QA and production) on self-hosted EKS, in queue mode, and we’re trying to design a robust promotion process. I’d love to hear how others have solved this at scale, because the default Git-based source control flow doesn’t seem to fit our case.

Our context

We expect hundreds of users with differentiated access across dozens of n8n projects. Users should only be able to promote the objects (workflows, credentials, variables, data tables) they actually have visibility on, based on their project access.

Why the default 2-environment / 2-branch flow breaks down for us

The standard flow is: QA pushes to a development branch, a user opens an MR to main, and after merge, production pulls from main. The problem is that there’s a single shared development branch and push/pull operate at instance level, with no per-object selectivity:

  • If two users from different teams push from QA, everything lands together in development. There’s no way to choose which of the two sets of changes to carry forward to main — they both go in.
  • On the pull side, production pulls the whole branch state; you can’t select which objects to import.

So one team’s promotion inevitably drags along another team’s work. With our scale and access model, this makes promotion governance essentially unmanageable.

What we considered building, and why we’re hesitant

We designed a custom promotion workflow: a user selects the specific objects they’re entitled to promote via a form, and the workflow creates an isolated branch + merge request per promotion event (using the GitLab Commits API, so each promotion is atomic and independent, and an admin can approve one MR without affecting another’s work in main). The isolation part works well conceptually.

The blocker is how to obtain the object files in the correct format. We first tried fetching objects from the n8n public API (e.g. GET /workflows/{id}) and committing those. But we found the API response format is not the same as the format n8n writes to the repo during a native push.

We could write custom logic to convert the API format into the repo format, but we really don’t want to because it’s fragile: if a future n8n version changes either the API response or the native push/pull format, our conversion logic breaks, and in the meantime nobody can promote between environments. That’s an unacceptable failure mode for something this central.

The question

Has anyone solved selective, per-user, per-object promotion across environments at this kind of scale? Specifically:

  1. Is there a supported way to get n8n to serialise objects in the native repo format without reconstructing it ourselves (short of relying on the full instance-level push to a shared branch)?
  2. Has anyone built a custom promotion layer on top of native source control, and how did you handle the per-object selectivity and format problem?
  3. Are we missing a feature or configuration that would make per-object / per-project promotion feasible with the native tooling?

Any pointers, patterns, or war stories would be hugely appreciated. Thanks!

Information on your n8n setup

  • n8n version: 2.30.6
  • Database (default: SQLite): PostgreSQL
  • n8n EXECUTIONS_PROCESS setting (default: own, main): queue mode
  • Running n8n via (Docker, npm, n8n cloud, desktop app): Docker
  • Operating system: Linux

Hi @Lorenzo_Tacconi1 Welcome!
The format mismatch is expected: GET /workflows/{id} returns the database entity, while native source control writes its own serialization, so any converter you build on top of the API will always be fragile. Don’t convert. n8n’s native push already writes each object as a separate file in exactly the repo format you want, and the commit modal already lets you choose which workflows go into a push. On the connected branch it lays them out like this:

workflows/<workflowId>.json
credential_stubs/<credentialId>.json
variable_stubs.json
tags.json
projects/<projectId>.json

So flip where your promotion layer reads from: let QA push (object-selective) to the dev branch, then have your workflow read the files n8n itself committed there and cherry-pick the specific workflows/<id>.json (plus the matching credential_stubs/<id>.json) into your isolated per-promotion branch and MR. Your GitLab Commits API isolation stays exactly as designed, you’re just moving n8n’s own output byte for byte instead of reconstructing it, so a future format change can’t break you because there’s no conversion step left to break.
The real limitation is on the pull side: native pull is whole-branch, with no cherry-pick or per-file selectivity, so per-object promotion has to live in Git, which is what you’re already doing. You’re not missing a hidden config here.

Welcome @Lorenzo_Tacconi1!

The native Source Control API does support selective pushes - POST /api/v1/source-control/push accepts a fileNames array where you specify exactly which workflow/credential/variable files to include. So you can push per-object rather than the full instance state. The repo format is standard JSON that mirrors the API export, with the key difference that credentials are stripped (only their structure/IDs are stored).

For the multi-team selectivity problem, the pattern that scales best: give each team or project its own Git branch, and use CI/CD (GitHub Actions / GitLab CI) to handle selective merges into a shared release branch before production pulls. n8n pulls from whatever branch the instance is configured to track, so you can point production at release and control exactly what lands there via PR reviews rather than trying to filter at the n8n level.

The fragility concern about format changes is real but manageable - the API format has been stable across minor versions, and selective pushes via the API are less likely to break than manually reconstructing the file structure. Worth raising as a GitHub issue for a proper “per-project promotion” feature though.

Hi @Lorenzo_Tacconi1

The recommended production‑grade pattern is to run separate n8n instances for each environment (Dev → Staging → Prod). Each instance has its own dedicated Git branch (or repo), so promotion becomes a simple merge of a branch rather than a merge of whole instances.

To achieve per‑object, per‑user promotion you can build a lightweight workflow that (a) lets the user pick workflow, credential, or data‑table IDs via a Form node, (b) fetches each selected object with the native “Get …” nodes (which already output the repository‑ready JSON), and (c) commits those files to a temporary branch (e.g., promo/<user>/<ticket>) using a Git/GitLab node.

After committing, the same workflow opens a Merge Request (or Pull Request). Administrators review and merge it; once merged, the Production instance pulls main. Because each MR contains only the files the user selected, promotion is atomic and isolated, with no risk of unintentionally pulling another team’s changes.

Credentials are never stored in Git (n8n strips them automatically). Instead, keep environment‑specific credentials in each instance and rely on environment variables for any secret values needed by the workflow JSON, ensuring the same workflow file works across all environments.

For additional safety you can add a CI step that runs n8n import:workflow --dry-run on every changed file before the MR can be merged, catching schema‑breaking errors early. This combination of separate instances, native export nodes, per‑promotion branches, and optional CI validation provides a robust, scalable solution for selective, per‑user promotion between QA and Production.

Hi guys, thank you for your very valuable insights.

I like the temporary branch @kjooleng suggested, I was thinking about the same. Do you have any material to share with us to speed up the development of this workflow?

Especially regarding the UI to let people select the objects to promote from the lower to the higher branch. How can we build that UI? How can we ensure the user can pick only the objects that are visible to him, given his projects membership?

Thank you.

Lorenzo

For the UI part, the n8n Form Trigger is the most straightforward way to build this without a separate frontend:

  1. Object selection: Use the Form Trigger with a multi-select field populated via an Expression. The options list gets fetched from your n8n API (GET /workflows, GET /credentials, etc.) filtered by the requester’s project.

  2. Project membership access control: n8n’s built-in Projects/RBAC (Enterprise) already scopes which workflows/credentials a user can see via the API — so if you call the n8n API with each user’s session token, the API will automatically return only their visible objects. If you’re not on Enterprise, you can approximate this by tagging objects with a project name and filtering by tag in the Form’s option list.

  3. Starter approach: A simple Form workflow that:

    • Takes the requester’s email + a list of workflow names they enter
    • Uses the n8n API node to export those workflows by name
    • Creates a Git commit on a promo/<user>/<ticket> branch via the GitHub/GitLab node
    • Notifies approvers

For the Git integration, the n8n GitHub node handles most of the branch+commit operations. Would it help if I sketched out the specific nodes needed for the export → Git step?

Hi @nguyenthieutoan, that’s very interesting! How can we call the n8n API with the user’s session token? Is it an output of the form node? We are in enterprise plan.

Hi @Lorenzo_Tacconi1
There’s no session token to pass, and the Form Trigger can’t give you one, a form submission isn’t an authenticated n8n user session, so it doesn’t carry the submitter’s identity or token. The public API doesn’t use session tokens at all, it authenticates with a per-user API key in the X-N8N-API-KEY header:

curl -H "X-N8N-API-KEY: <that-user-key>" https://<your-instance>/api/v1/workflows

The scoping you want comes from whose key it is. On Enterprise, a Member’s API key only returns the workflows, credentials, and data tables in the projects that user belongs to, so GET /workflows called with their key already returns exactly their visible set. An Owner or Admin key sees everything, which defeats the per-user filter, so don’t run it with one shared owner key.
Practical wiring: each promoter generates their own key once under Settings > n8n API, you keep a mapping of form identity to key in a secret store (not in the form), and the promotion workflow calls the API with that person’s key when they submit. The cost is managing many keys. If that’s too much, use one service key and instead read the requester’s membership from GET /projects and filter the object list yourself.