Repository navigation
feat(folders): manage workflow folders as code across apply and import - #82
Merged
Merged
Conversation
Workflow folders can now be declared in definitions and applied, closing the
loop with import. Two upstream constraints shaped the design:
- a workflow's folder assignment (`parentFolderId`) is write-only in n8n's
public API — create/PATCH accept it, no GET ever returns it. So apply
*asserts* a declared folder on every run (idempotent, including on skips)
instead of diffing it, and `import` reads assignments over n8n's
instance-level MCP server (`search_workflows` /
`get_workflow_details` do report `parentFolderId`).
- folder IDs are server-assigned, so local files address folders by path
("Reporting/Daily") and apply resolves paths to IDs through the folders
API, creating missing segments parent-first.
What this adds:
- YAML definitions: a top-level `folder:` path key (`null` / "root"
manages the project root deliberately; an absent key means "do not
touch", matching the API's own omit-unchanged semantics). JSON files may
carry the raw `parentFolderId`; import writes both.
- `definitions/folders.yaml`: folder trees as code, synced create-only
before workflow processing. Deliberately add-only — renames and moves
cannot be expressed reliably without client-chosen IDs. All scanners
(apply, import incl. orphan cleanup, lint, fmt, convert) skip the
filename so it is never read as a workflow or deleted as an orphan.
- import: `--mcp` / `--mcp-token` / `N8N_MCP_TOKEN` / `N8N_MCP` attach
folder assignments; bulk via `search_workflows`, per-workflow fallback
via `get_workflow_details`, id→path via the REST folders API per owning
project. A proxy in front of n8n can hold the MCP access token and inject
it for /mcp-server/* (bearer-token-inject) — the CLI then needs no secret.
- apply: `--no-folders`, `--no-create-missing-folders`, `--strict-folders`.
Folder problems (missing license, unresolvable path, failed move) degrade
to warnings; the workflow write itself is never blocked unless strict.
- a `folder` command group (list/get/create/move/delete) for imperative use.
- config: the `mcp` section in .n8nctlrc.json (`mode`/`token`/`strict`),
distinct from the proxy's MCP `proxy.mcp` gate section.
Folder support requires an n8n plan with folders licensed (`feat:folders`,
`folder:*" scopes); everywhere else it degrades to warnings rather than
blocking workflow management. On create, servers too old to accept
`parentFolderId` fall back to create-then-PATCH automatically.
7 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds workflow folder management to n8n-cli, closing the loop between
applyandimport: a definition can declare which folder its workflow belongs to,applyputs it there (creating missing folders), andimportbrings assignments back down. Folder trees themselves become code via afolders.yamlin the definitions directory.Two upstream constraints shaped the design:
parentFolderIdon create andPATCH /workflows/:id, but noGETresponse ever includes it. Soapplyasserts a declared folder on every run — idempotently, including onSKIPoperations, because "content unchanged" says nothing about a field the API cannot read back — andimportreads assignments over n8n's instance-level MCP server, whosesearch_workflows/get_workflow_detailstools do reportparentFolderId.CreateFolderDtoaccepts onlyname+parentFolderId, so local files address folders by path ("Reporting/Daily") andapplyresolves paths to IDs through the folders API, creating missing segments parent-first. Renames and moves of folders are deliberately not reconciled — without client-chosen IDs they cannot be expressed reliably as code.What this adds:
folder:path key.folder: null(or"root") manages the project root deliberately — the key must not be dropped at the root, because absent means "leave the folder untouched" and the assignment would silently stop being managed. An absent key on any file means "do not touch", matching the API's own omit-unchanged semantics. JSON files carry the rawparentFolderId;importwrites both forms.definitions/folders.yaml: folder trees as code (projects[].foldersnested trees,projectIdoptional when-p/defaultProjectIdsupplies one), synced create-only in Phase 0 of everyapply, before any workflow write. Dry-run reports what it would create. All scanners — apply, import (including orphan cleanup, which would otherwise delete the file),lint,fmt,convert— now skip the filename so it is never mistaken for a workflow.importfolder lookups:--mcp/--mcp-token <token>/N8N_MCP_TOKEN/N8N_MCP=1enable an MCP connection. Bulk resolution via paginatedsearch_workflows, per-workflow fallback viaget_workflow_details(with the spec'sinitialize→notifications/initializedhandshake andMcp-Session-Idecho), id→path via the REST folders API per owning project. Two deployment shapes: the CLI holds an MCP access token, or an n8n-cliproxyholds it and injectsAuthorization: Bearerfor/mcp-server/*via the existingbearer-token-injectmiddleware — the CLI then needs no secret at all (transparent forwarding of the MCP surface, SSE included, already works).applyflags:--no-folders(disable entirely),--no-create-missing-folders(refuse unresolvable paths),--strict-folders(escalate folder problems to errors).foldercommand group (list/get/create/move/delete) for imperative one-off operations.mcpsection in.n8nctlrc.json(mode:off/direct/proxy,tokenwith${ENV}interpolation,strict), documented in the shippedn8nctlrc.schema.json. Distinct from the proxy'sproxy.mcpgate section.Degrade semantics: folder support requires an n8n plan with folders licensed (
feat:folders,folder:*scopes; MCP needs an access token). Where folders are unlicensed or a path cannot be resolved, folder handling degrades to a per-workflow warning (surfaced as⚠ Folder: …in the report) and the workflow write proceeds — an apply never fails because of folders unless--strict-foldersis passed. A malformedfolders.yamlremains an authoring error and fails the apply. On create, servers too old to acceptparentFolderId(400 on the unknown property) fall back to create-then-PATCH automatically. Files without any folder key behave exactly as before.Motivation
n8n-cli manages workflows, tags and projects as code, but folders — the primary organization unit inside a project once a workspace grows past a few dozen workflows — were invisible:
applycould not place a workflow in a folder, andimportcould not record where it lived, because the public API hides the assignment on every read. Teams either re-dragged workflows into folders by hand after every fresh environment bootstrap, or gave up on folders entirely. The MCP server n8n already runs was the missing read path: it exposes exactly the assignment the REST API withholds, and the CLI's proxy already had the middleware seam to mediate it for token-less clients.Test plan
make quality-gate(generate-schemas, typecheck, lint, check-third-party-licenses,bun test) — 1939 passbun run test— 1939 pass, incl. new suites: folder service (17), MCP client transport (12), folder-as-code primitives (20), apply×folders executor (18), import×MCP folders (22), YAMLfolderround trip (5)bun test tests/integration— 22 pass, incl. CLI → proxy → mock-n8n scenarios proving all three deployment shapes: proxy-injected MCP token (CLI holds none), CLI-held token, no token at all (degrade +--mcp-strictfail), andapplyfolder moves +folders.yamlsync through the proxy with write-only GET semantics honoured