🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman
CLI for n8n workflow files and API
n8n-cli works with JSON, YAML, and TypeScript workflow definitions, and lets you move between local files and an n8n server. It can apply changes with conflict detection, compare local and remote workflows, lint against policy, and format or convert definitions locally. It also includes a proxy that blocks writes before they reach n8n, MCP gating for exposing only selected tools, and support for reading project settings from `CLAUDE.md`.
Builders who want to manage n8n workflows as code from the terminal and keep agent-made changes reviewable.
You can edit workflows locally, check the diff, and deploy with policy checks before changes hit n8n.
What it does
Apply workflows
Deploy local workflow definitions to an n8n server with dry-run, conflict detection, duplicate-name checks, and Git diff filtering.
Diff workflows
Compare two files, a git range, or local definitions against the n8n server, with text, JSON, Mermaid, or HTML output.
Convert workflow formats
Convert workflows between JSON, YAML, and TypeScript, including YAML subfiles and TypeScript generation against `@n8n/workflow-sdk`.
Import from n8n
Pull workflows from n8n into local files, with optional YAML or TypeScript output, cleanup of orphan files, and folder lookup through MCP.
Lint workflow files
Validate workflow definitions with configurable rules such as banned nodes, secret detection, and schedule frequency limits.
Proxy and MCP gating
Intercept workflow saves, run server-side lint checks, and expose only selected MCP tools and workflows through policy.
Workflow and execution management
List, create, update, delete, activate, deactivate, retry, and stop workflows and executions through the n8n API.
CLAUDE.md settings
Read default project settings, auto tags, YAML mode, and TypeScript mode from `CLAUDE.md`.
How to get it
- 1Run
git clone https://github.com/ubie-oss/n8n-cli.git cd n8n-cli bun install make build
- 2Build binaries for multiple platforms
make cross-compile
- 3Set environment variables for your n8n instance
export N8N_API_URL="https://your-n8n-instance.example.com" export N8N_API_KEY="your-api-key"
- 4List workflows
./n8n-cli workflow list
- 5Import a workflow to a local file
./n8n-cli import --ids=<workflow-id> --yaml -d ./definitions
- 6Edit the local file and apply changes
./n8n-cli apply --ids=<workflow-id> --dry-run -d ./definitions ./n8n-cli apply --ids=<workflow-id> -d ./definitions
README
n8n-cli
A command-line interface for managing n8n workflows as code. Import, export, lint, format, test, and deploy workflow definitions from your terminal.
Features
- Apply - Deploy local workflow definitions (JSON/YAML/TypeScript) to an n8n server with dry-run support and conflict detection
- Diff - Show what changed between two workflow files, a git ref and the working tree, or local definitions and the n8n server — as text, JSON, or Mermaid
- Convert - Convert workflow files between JSON, YAML and TypeScript formats locally
- Import - Pull workflows from an n8n server to local files, with optional YAML/TypeScript conversion and code externalization
- Lint - Validate workflow definitions against configurable rules
- Proxy - Transparent HTTP proxy that intercepts workflow saves to the n8n public API and runs lint server-side, blocking violations before they reach n8n, and refusing writes built from an out-of-date copy of a workflow
- MCP gating - Put your own policy in front of n8n's MCP server: expose only the tools you chose, and only against the workflows your convention says an agent may reach
- Format - Auto-organize node positions for cleaner workflow layouts
- Test - Execute CLI tests against workflows via webhook endpoints
- Webhook - List and call a workflow's webhook nodes through the authenticated egress path
- Workflow management - List, get, create, update, delete, activate, and deactivate workflows via the n8n API
- Execution management - List executions, get execution details, delete, retry, and stop executions
- Tag management - List, get, create, update, and delete tags
- Credential management - List, get, create, update, delete credentials, get schema, and transfer between projects
- Data table management - List, get, create, update, delete data tables and manage rows (insert, update, upsert, delete)
- Node schema - Inspect built-in node type schemas (list and dump)
- Trace - Analyze data flow and item cardinality through workflow nodes
- Git integration - Apply only workflows changed in a Git diff
- YAML support - Work with YAML workflow definitions and external code/SQL files
- TypeScript support - Work with type-checked
.tsworkflow definitions written against@n8n/workflow-sdk - CLAUDE.md integration - Read project settings (default project ID, auto tags, YAML mode, TypeScript mode) from CLAUDE.md
Installation
Prerequisites
- Bun v1.0 or later
Build from source
git clone https://github.com/ubie-oss/n8n-cli.git
cd n8n-cli
bun install
make build
This produces a standalone n8n-cli binary in the project root.
Cross-compile
Build binaries for multiple platforms:
make cross-compile
Outputs are placed in dist/:
n8n-cli-darwin-arm64(macOS Apple Silicon)n8n-cli-darwin-x64(macOS Intel)n8n-cli-linux-x64(Linux x64)n8n-cli-windows-x64(Windows x64)
Quick Start
- Set environment variables for your n8n instance:
export N8N_API_URL="https://your-n8n-instance.example.com"
export N8N_API_KEY="your-api-key"
- List workflows:
./n8n-cli workflow list
- Import a workflow to a local file:
./n8n-cli import --ids=<workflow-id> --yaml -d ./definitions
- Edit the local file and apply changes:
./n8n-cli apply --ids=<workflow-id> --dry-run -d ./definitions
./n8n-cli apply --ids=<workflow-id> -d ./definitions
Commands
apply
Deploy local workflow definitions to the n8n server.
n8n-cli apply [options]
| Option | Description |
|---|---|
-d, --dir <path> | Path to definitions directory (default: ./definitions) |
-p, --project <id> | Target project ID for workflow transfer |
--ids <ids> | Comma-separated workflow IDs to process |
--from-git-changes <spec> | Apply only files changed in Git diff (e.g., origin/main..HEAD) |
--dry-run | Preview changes without applying |
--force | Override conflict detection and duplicate warnings |
--no-auto-tag | Disable automatic tagging |
--yaml / --no-yaml | Enable/disable YAML file processing |
--ts / --no-ts | Enable/disable .ts file processing (see TypeScript workflow definitions) |
--allow-duplicates | Skip the upstream duplicate-name check (the check is on by default; use --force to push through warnings instead of disabling the check) |
--no-lint | Skip the pre-write lint check (the check is on by default; an error-level violation marks the workflow as failed and prevents the API call. --force does NOT bypass lint failures — they represent policy, not merge conflicts) |
--lint-config <path> | Path to .n8nlintrc.json used by the pre-write lint check (auto-discovered if omitted) |
--lint-disable-rule <rules> | Comma-separated rule names to disable during the pre-write lint check |
--no-folders | Disable folder management: skip folders.yaml sync and ignore folder declarations in workflow files (folder support is on by default, see Workflow folders as code) |
--no-create-missing-folders | Refuse (error/warning) instead of creating folder paths referenced by definitions that do not exist upstream |
--strict-folders | Treat folder problems (missing license, unresolvable path, failed move) as apply errors instead of warnings |
Conflict detection
A definition records the upstream updatedAt it was written from. Before updating, apply compares that stamp against the live workflow: when upstream is newer and the content differs, the operation is reported as a conflict instead of being pushed, because applying it would revert a change nobody imported. --force overrides it; import resolves it properly.
After a successful write the local file is re-stamped with the server's new timestamp, so the next edit is not mistaken for a conflict. In a CI-driven setup this happens on the runner, which means the stamp in version control stays behind until something writes it back — either commit the re-stamped files from the apply job, or run import on a schedule. Until it catches up, a second change to the same workflow will report a conflict that --force can push through.
Behaviour change (YAML): YAML definitions written before this feature carry no
updatedAt, and applies against them were unconditional. Onceimportre-writes them with a stamp, those same applies start reporting conflicts — which is the point, but it is a change in behaviour for existing repositories. JSON and.tsdefinitions already carried the stamp.A second consequence, on the machine that ran the apply:
importskips a workflow whose local stamp is already current, so anything the server normalised during the write (defaulted parameters, ids it assigned) does not come back down until the workflow changes again upstream. YAML now matches what JSON has always done, and what.tsdoes deliberately. The skip is by timestamp, not by selection, so--idsdoes not override it — delete the local file, or restore the committed version whose stamp is older, and import again.
For enforcement that does not depend on the client (any working copy can pass --force, and other tools do not run this check at all), see the proxy's stale-write guard.
Exit Codes
| Code | Description |
|---|---|
0 | Success |
1 | Error detected |
2 | Conflict detected (dry-run) or warning detected (non-force mode) |
diff
Show what changed between workflow definitions. Read-only: it never writes to files or the server.
n8n-cli diff [left] [right] [options]
Three comparison modes:
| Mode | Invocation | Left (old) / Right (new) |
|---|---|---|
| Two files | n8n-cli diff old.json new.json | Any .json / .yaml / .ts workflow file pair |
| Git ref vs working tree | n8n-cli diff --git-spec origin/main...HEAD -d ./definitions | Files at the base ref vs the working tree |
| Local dir vs server | n8n-cli diff -d ./definitions | Definitions directory vs workflows fetched from the n8n API |
| Option | Description |
|---|---|
-d, --dir <path> | Definitions directory used by git/server comparisons (default: ./definitions) |
--git-spec <spec> | Compare at the base ref of a git range (e.g. origin/main...HEAD) against the working tree |
--ids <ids> | Comma-separated workflow IDs to include |
--stat | Print only per-workflow summary lines (token-efficient for AI consumption) |
-f, --format <format> | Output format: text (default), json, mermaid, or html |
--include-position | Report node position changes instead of ignoring them |
What counts as a change
The diff is change-centric and noise-free by design:
- Node positions,
staticDatadrift, key order, and nil-vs-empty differences are ignored (--include-positionopts positions back in) - Nodes are matched by stable ID first, then by name; renames are detected even when IDs changed (as happens with
.tsdefinitions, whose IDs derive from node names) - Parameter changes report concrete paths (
parameters.url) with before/after values - Multi-line code parameters (Code nodes, SQL queries) get line-level diffs instead of full value dumps
- Connection changes are reported per edge — including
ai_*connection types — so rewiring is visible without reading JSON - Credentials are compared as references (name/ID) only; secret values never appear in output
Exit Codes
Designed for CI gates and AI self-verification loops (edit → diff → apply --dry-run → apply → re-diff):
| Code | Description |
|---|---|
0 | No differences found |
1 | Differences found |
2 | The diff could not be computed (bad input, API error, ...) |
Example session:
$ n8n-cli diff before.json after.json
M Nightly customer sync (id: wf-demo-1) +2 nodes, -1 nodes, ~3 nodes, ~connections (5)
=== Nightly customer sync (id: wf-demo-1) ===
Metadata:
active: true → false
Nodes:
~ "Fetch Customers" → "Fetch Users":
parameters.url: "https://api.example.com/customers" → "https://api.example.com/v2/users"
~ "Transform":
parameters.jsCode:
- return rows.filter(r => r.active);
+ const active = rows.filter(r => r.status === 'active');
+ return active;
+ "Upsert BigQuery" (n8n-nodes-base.dataTable)
- "Post to Slack" (n8n-nodes-base.slack)
Connections:
+ "Fetch Users" →[main:0] "Upsert BigQuery"
- "Transform" →[main:0] "Post to Slack"
--format mermaid renders each changed workflow as a color-coded flowchart (green = added, red = removed, yellow = modified, blue = renamed) suitable for PRs and Markdown. --format html > report.html writes a self-contained change-centric HTML report (summary badges, interactive diagram with per-node diffs, git-style parameter diffs, and a raw line diff of the key-sorted workflow JSON). --format json emits the full structured report for tooling.
convert
Convert workflow files between formats (JSON / YAML / TypeScript). This is a local-only operation that does not require an n8n server connection.
n8n-cli convert [options] [files...]
| Option | Description |
|---|---|
--format <format> | Target format: json, yaml, ts (required) |
--ts | Include .ts files when scanning a directory (explicit file arguments are always honoured) |
-d, --directory <dir> | Directory to scan for workflow files |
--ids <ids> | Comma-separated workflow IDs to convert |
--tags <tags> | Filter by tags (comma-separated, AND condition) |
-t, --threshold <n> | Minimum lines for code externalization (JSON→YAML) |
--dry-run | Preview conversions without writing files |
--keep | Keep original files after conversion |
Examples:
# Convert all JSON workflows in a directory to YAML
n8n-cli convert -d ./definitions --format yaml
# Convert specific workflows by ID
n8n-cli convert -d ./definitions --format json --ids wf-100,wf-200
# Preview conversions without making changes
n8n-cli convert -d ./definitions --format yaml --dry-run
# Convert but keep the original files
n8n-cli convert -d ./definitions --format yaml --keep
# Convert a specific file
n8n-cli convert --format yaml workflow__wf-100.json
Behavior:
- JSON → YAML: Generates YAML with code externalization (
_subfiles/) anddescription.md - YAML → JSON: Resolves
!includedirectives (inlines external files) and removes_subfiles/directories - → TypeScript: Emits a
.tsfile against@n8n/workflow-sdk. The generated file is parsed back and compared against the source before it is written; a workflow the SDK cannot represent faithfully fails with an error instead of being silently mangled - Files already in the target format are skipped
- Original files are removed after conversion unless
--keepis specified
import
Import workflows from n8n to local files.
n8n-cli import [options]
| Option | Description |
|---|---|
--dry-run | Preview changes without writing files |
-d, --dir <directory> | Target directory for workflow files (default: ./definitions) |
--ids <ids> | Comma-separated workflow IDs to import (empty = all) |
--include-archived | Include archived workflows |
--yaml / --no-yaml | Output as YAML format with external files / Force JSON |
--ts / --no-ts | Write new workflows as .ts against @n8n/workflow-sdk |
-t, --threshold <n> | Minimum lines for code externalization |
--cleanup-orphans | Delete local files without matching remote workflow |
--cleanup-subfiles | Delete orphan external files |
--tags <tags> | Filter by tags (comma-separated, AND condition) |
--mcp | Attach folder assignments to imported files by calling n8n's MCP server (also enabled by --mcp-token / N8N_MCP_TOKEN / N8N_MCP=1) |
--mcp-token <token> | MCP access token for the folder lookup (env: N8N_MCP_TOKEN). Without a token, MCP calls rely on an n8n-cli proxy injecting the token for /mcp-server/*. MCP requests use the same client middleware chain as REST (iap-auth, impersonator-token, …) so they can reach an authenticating gateway in front of that proxy |
--mcp-strict | Fail the import when the MCP folder lookup fails, instead of warning and continuing without folder information |
folder
Manage n8n workflow folders. Folder support is an n8n enterprise feature —
these commands need a plan with folders licensed and an API key with
folder:* scopes.
n8n-cli folder list -p <projectId> [--parent <folderId> | --root]
n8n-cli folder get <folderId> -p <projectId>
n8n-cli folder create <name> -p <projectId> [--parent <folderId>]
n8n-cli folder move <folderId> -p <projectId> [--parent <folderId> | --root] [--rename <name>]
n8n-cli folder delete <folderId> -p <projectId> [--transfer-to <folderId>] [-f]
lint
Lint workflow definition files.
n8n-cli lint [options]
| Option | Description |
|---|---|
-d, --dir <directory> | Directory to scan for workflow files |
-f, --file <files...> | Specific files to lint |
-c, --config <path> | Path to .n8nlintrc.json config file |
--disable-rule <rules...> | Disable specific rules |
--list-rules | List all available rules and exit |
-o, --output <format> | Output format: text, json (default: text) |
--tags <tags> | Filter by tags (comma-separated, AND condition) |
--project <id> | Project ID context for local files. Remote lint detects ownership per workflow |
Lint Configuration (.n8nlintrc.json)
Create a .n8nlintrc.json file to configure lint rules. Each rule can be set to:
"error"/"warning"— enable with the specified severity"off"orfalse— disable the rule["error", { ...options }]— enable with severity and rule-specific options
{
"rules": {
"orphaned-node": "warning",
"node-params": "error",
"webhook-id-required": "off"
}
}
Rules under the top-level rules key are global. They continue to apply to
every workflow. Add a projects block keyed by n8n Project ID to layer stricter
or project-specific policy on top:
{
"rules": {
"no-plaintext-secrets": "error",
"banned-node": ["error", {
"nodes": [{ "type": "n8n-nodes-base.executeCommand" }]
}]
},
"projects": {
"pLx9cQ2mNv7aB1dK": {
"rules": {
"banned-node": ["error", {
"nodes": [{ "type": "n8n-nodes-base.code", "reason": "Use reviewed nodes only" }]
}],
"schedule-trigger-frequency": ["error", { "minInterval": "daily" }]
}
}
}
}
The global and matching project layers both run. A project-level "off" only
disables that project-layer entry; it never weakens a global rule. If both
layers produce the same finding, the output contains one finding with the
stricter severity.
lint --remote reads the owner from each workflow's shared metadata. For
local definitions, pass --project <id> when that metadata is absent. apply
uses its existing --project / default-project setting.
Lint Rules with Options
banned-node
Detects usage of banned node types and enforces per-node parameter policies. Requires the array config format to specify options.
| Option | Type | Description |
|---|---|---|
deny | Array<Matcher> | Node types banned outright. deny wins over allow |
allow | Array<Matcher> | When non-empty, switches the rule into allowlist mode: every node must match at least one entry, otherwise it is banned. Entries may still be narrowed by params |
params | Record<node-matcher, ParamsPolicy> | Per-node parameter policy (see below). Keys are node type matchers merged in order of specificity (broadest first, exact last) |
A Matcher is { "type": "..." } (exact) or { "pattern": "...", "match": "exact" \| "glob" \| "regex" } (match defaults to "glob"). deny entries may carry a reason. The legacy nodes: [{ type, reason }] option is an alias for deny.
A ParamsPolicy:
| Option | Type | Description |
|---|---|---|
allowParams | string[] | When present, every top-level parameter name must match at least one pattern. Patterns match the parameter key (e.g. additional*), not a path into it |
denyParams | string[] | Top-level parameter names matching any pattern are violations. To forbid a nested key, use values with an empty allow list |
expressions | "allow" | "deny" | Default policy for expression values on this node. Expression values are strings beginning with = or containing {{ ... }}. Default: "allow" |
values | Record<path, ValueRule> | Value-level rules keyed by dot-path (channelId.value) supporting * globs and /regex/. For each field (allow, pattern, expressions) the most specific matching path rule that defines that field wins, so a broad rule and a narrow rule compose rather than shadow each other. Code-bearing params (jsCode, inputSchema) are exempt from expression checks |
A ValueRule combines three orthogonal constraints:
| Option | Type | Description |
|---|---|---|
allow | string[] | Exact values the parameter may hold. An empty array forbids the path entirely |
pattern | string | A glob (default) or, with match: "regex", a regular expression the value must match |
expressions | "allow" | "deny" | Overrides the node's expressions policy for just that path |
Expression values skip the literal allow / pattern checks because they are dynamic; the expressions policy decides their fate instead. Invalid matchers, regexes and option values are reported as error-severity config violations.
{
"rules": {
"banned-node": ["error", {
"deny": [
{ "type": "n8n-nodes-base.executeCommand", "reason": "Security risk: arbitrary command execution" },
{ "pattern": "n8n-nodes-base.*Command", "match": "glob" },
{ "pattern": "n8n-nodes-base\\.(code|function)", "match": "regex" }
],
"allow": [
{ "type": "n8n-nodes-base.slack" },
{ "type": "n8n-nodes-base.httpRequest" }
],
"params": {
"*": { "expressions": "deny" },
"n8n-nodes-base.slack": {
"allowParams": ["resource", "operation", "channelId", "text", "additionalFields"],
"denyParams": ["messageType"],
"values": {
"channelId.value": { "allow": ["#general", "#ops"] },
"text": { "expressions": "allow" }
}
},
"n8n-nodes-base.httpRequest": {
"allowParams": ["url", "method"],
"values": { "url": { "pattern": "^https://", "match": "regex" } }
}
}
}]
}
}
The example above bans executeCommand, all *Command glob matches and the Code/Function nodes; only Slack and HTTP Request are allowed; expressions are forbidden everywhere except text on Slack; Slack may only post to #general or #ops; and HTTP Request may only target https:// URLs.
no-plaintext-secrets
Detects plaintext secrets (API keys, tokens, passwords) embedded in node parameters. Enabled by default with severity error. Detection is best-effort and layered:
- Schema-declared password fields — parameters that n8n itself masks as passwords (
typeOptions.passwordin node schemas, e.g.crypto.secret,jwt.token,*.password) containing literal values - Sensitive name heuristics — keys like
Authorization,X-API-Key,api_key,token,secret,password,cookiein HTTP Request / GraphQL header collections, query parameter collections, Set node assignments, embedded JSON strings (jsonHeaders,jsonBody, ...), URL query strings (?api_key=...), and URL userinfo (https://user:password@host) - Known token formats — string values anywhere (including Code node source and sticky notes) matching well-known secret formats: AWS access keys, GitHub/GitLab/Slack/npm tokens, OpenAI/Anthropic/Google/Stripe/SendGrid/Twilio API keys, JWTs, private key blocks, and
password: "..."-style assignments
Values written as n8n expressions (e.g. =Bearer {{ $env.API_TOKEN }}) are considered safe, and secret values are redacted in lint messages.
| Option | Type | Description |
|---|---|---|
additionalNames | string[] | Extra key names to treat as sensitive |
additionalPatterns | string[] | Extra value regexes to treat as secrets |
allowValues | string[] | Regexes; matching values are never flagged (e.g. test fixtures) |
minSecretLength | number | Minimum literal length for name-based checks (default: 8) |
{
"rules": {
"no-plaintext-secrets": ["error", {
"additionalNames": ["signingSeed"],
"additionalPatterns": ["ACME-INTERNAL-[0-9]{10}"],
"allowValues": ["^test-fixture-"],
"minSecretLength": 8
}]
}
}
schedule-trigger-frequency
Validates that Schedule Trigger nodes don't fire more frequently than a configured minimum interval.
| Option | Type | Description |
|---|---|---|
minInterval | "minutes" | "hourly" | "daily" | "weekly" | "monthly" | Minimum allowed trigger interval (default: "hourly") |
{
"rules": {
"schedule-trigger-frequency": ["warning", { "minInterval": "daily" }]
}
}
mcp-tool-description
Checks that a workflow reachable through n8n's instance-level MCP server carries a description worth reading. Enabled by default with severity warning; it does nothing to a
Files in the repo
- .claude
- .github
- schemas
- scripts
- src
- tests
- .dockerignore
- .gitignore
- biome.json
- bun.lock
- CONTRIBUTING.md
- Dockerfile
- LICENSE
- Makefile
- package.json
- README.md
- THIRD_PARTY_LICENSES.md
- tsconfig.json
Discussion (0)
Ask about usage, or say what you built with itSign in to join the discussion.
No comments yet. Be the first to say what this is good for.
More tools
The best-benchmarked open-source AI memory system. And it's free.
Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io
Never stop coding. Free MIT AI gateway: one endpoint, 352 providers (150+ free), 1200+ models Kimi, Claude, GPT, Gemini, GLM, DeepSeek, MiniMax. Works with Claude Code, Codex, Cursor, OpenCode, Cline & Copilot. Quota-aware auto-fallback, RTK+Caveman compression saves 15-95% tokens, MCP/A2A, Desktop/PWA. Built by 550+ contributors
Compress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.