Skip to content

feat(folders): manage workflow folders as code across apply and import - #82

Merged
syucream merged 2 commits into
mainfrom
feat/workflow-folders-as-code
Aug 29, 2026
Merged

syucream merged 2 commits into
mainfrom
feat/workflow-folders-as-code

Conversation

@syucream

Copy link
Copy Markdown
Contributor

Summary

Adds workflow folder management to n8n-cli, closing the loop between apply and import: a definition can declare which folder its workflow belongs to, apply puts it there (creating missing folders), and import brings assignments back down. Folder trees themselves become code via a folders.yaml in the definitions directory.

Two upstream constraints shaped the design:

  • A workflow's folder assignment is write-only. n8n's public API accepts parentFolderId on create and PATCH /workflows/:id, but no GET response ever includes it. So apply asserts a declared folder on every run — idempotently, including on SKIP operations, because "content unchanged" says nothing about a field the API cannot read back — and import reads assignments over n8n's instance-level MCP server, whose search_workflows / get_workflow_details tools do report parentFolderId.
  • Folder IDs are server-assigned. CreateFolderDto accepts only name + parentFolderId, so local files address folders by path ("Reporting/Daily") and apply resolves 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:

  • YAML definitions: a top-level 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 raw parentFolderId; import writes both forms.
  • definitions/folders.yaml: folder trees as code (projects[].folders nested trees, projectId optional when -p/defaultProjectId supplies one), synced create-only in Phase 0 of every apply, 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.
  • import folder lookups: --mcp / --mcp-token <token> / N8N_MCP_TOKEN / N8N_MCP=1 enable an MCP connection. Bulk resolution via paginated search_workflows, per-workflow fallback via get_workflow_details (with the spec's initialize → notifications/initialized handshake and Mcp-Session-Id echo), id→path via the REST folders API per owning project. Two deployment shapes: the CLI holds an MCP access token, or an n8n-cli proxy holds it and injects Authorization: Bearer for /mcp-server/* via the existing bearer-token-inject middleware — the CLI then needs no secret at all (transparent forwarding of the MCP surface, SSE included, already works).
  • apply flags: --no-folders (disable entirely), --no-create-missing-folders (refuse unresolvable paths), --strict-folders (escalate folder problems to errors).
  • folder command group (list / get / create / move / delete) for imperative one-off operations.
  • Config: the mcp section in .n8nctlrc.json (mode: off/direct/proxy, token with ${ENV} interpolation, strict), documented in the shipped n8nctlrc.schema.json. Distinct from the proxy's proxy.mcp gate 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-folders is passed. A malformed folders.yaml remains an authoring error and fails the apply. On create, servers too old to accept parentFolderId (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: apply could not place a workflow in a folder, and import could 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 pass
  • bun 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), YAML folder round 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-strict fail), and apply folder moves + folders.yaml sync through the proxy with write-only GET semantics honoured

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.
@syucream
syucream merged commit de4f32b into main Aug 29, 2026
6 checks passed
@syucream
syucream deleted the feat/workflow-folders-as-code branch August 29, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant