Sandbox
@ubie-oss/n8n-cli

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`.

46 stars9 forksTypeScriptUpdated 17d ago
Who it's for

Builders who want to manage n8n workflows as code from the terminal and keep agent-made changes reviewable.

What it delivers

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

  1. 1Run
    git clone https://github.com/ubie-oss/n8n-cli.git
    cd n8n-cli
    bun install
    make build
  2. 2Build binaries for multiple platforms
    make cross-compile
  3. 3Set environment variables for your n8n instance
    export N8N_API_URL="https://your-n8n-instance.example.com"
    export N8N_API_KEY="your-api-key"
  4. 4List workflows
    ./n8n-cli workflow list
  5. 5Import a workflow to a local file
    ./n8n-cli import --ids=<workflow-id> --yaml -d ./definitions
  6. 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 .ts workflow 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

  1. Set environment variables for your n8n instance:
export N8N_API_URL="https://your-n8n-instance.example.com"
export N8N_API_KEY="your-api-key"
  1. List workflows:
./n8n-cli workflow list
  1. Import a workflow to a local file:
./n8n-cli import --ids=<workflow-id> --yaml -d ./definitions
  1. 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]
OptionDescription
-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-runPreview changes without applying
--forceOverride conflict detection and duplicate warnings
--no-auto-tagDisable automatic tagging
--yaml / --no-yamlEnable/disable YAML file processing
--ts / --no-tsEnable/disable .ts file processing (see TypeScript workflow definitions)
--allow-duplicatesSkip the upstream duplicate-name check (the check is on by default; use --force to push through warnings instead of disabling the check)
--no-lintSkip 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-foldersDisable 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-foldersRefuse (error/warning) instead of creating folder paths referenced by definitions that do not exist upstream
--strict-foldersTreat 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. Once import re-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 .ts definitions already carried the stamp.

A second consequence, on the machine that ran the apply: import skips 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 .ts does deliberately. The skip is by timestamp, not by selection, so --ids does 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

CodeDescription
0Success
1Error detected
2Conflict 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:

ModeInvocationLeft (old) / Right (new)
Two filesn8n-cli diff old.json new.jsonAny .json / .yaml / .ts workflow file pair
Git ref vs working treen8n-cli diff --git-spec origin/main...HEAD -d ./definitionsFiles at the base ref vs the working tree
Local dir vs servern8n-cli diff -d ./definitionsDefinitions directory vs workflows fetched from the n8n API
OptionDescription
-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
--statPrint only per-workflow summary lines (token-efficient for AI consumption)
-f, --format <format>Output format: text (default), json, mermaid, or html
--include-positionReport node position changes instead of ignoring them

What counts as a change

The diff is change-centric and noise-free by design:

  • Node positions, staticData drift, key order, and nil-vs-empty differences are ignored (--include-position opts positions back in)
  • Nodes are matched by stable ID first, then by name; renames are detected even when IDs changed (as happens with .ts definitions, 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 → diffapply --dry-run → apply → re-diff):

CodeDescription
0No differences found
1Differences found
2The 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...]
OptionDescription
--format <format>Target format: json, yaml, ts (required)
--tsInclude .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-runPreview conversions without writing files
--keepKeep 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/) and description.md
  • YAML → JSON: Resolves !include directives (inlines external files) and removes _subfiles/ directories
  • → TypeScript: Emits a .ts file 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 --keep is specified

import

Import workflows from n8n to local files.

n8n-cli import [options]
OptionDescription
--dry-runPreview 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-archivedInclude archived workflows
--yaml / --no-yamlOutput as YAML format with external files / Force JSON
--ts / --no-tsWrite new workflows as .ts against @n8n/workflow-sdk
-t, --threshold <n>Minimum lines for code externalization
--cleanup-orphansDelete local files without matching remote workflow
--cleanup-subfilesDelete orphan external files
--tags <tags>Filter by tags (comma-separated, AND condition)
--mcpAttach 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-strictFail 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]
OptionDescription
-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-rulesList 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" or false — 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.

OptionTypeDescription
denyArray<Matcher>Node types banned outright. deny wins over allow
allowArray<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
paramsRecord<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:

OptionTypeDescription
allowParamsstring[]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
denyParamsstring[]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"
valuesRecord<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:

OptionTypeDescription
allowstring[]Exact values the parameter may hold. An empty array forbids the path entirely
patternstringA 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:

  1. Schema-declared password fields — parameters that n8n itself masks as passwords (typeOptions.password in node schemas, e.g. crypto.secret, jwt.token, *.password) containing literal values
  2. Sensitive name heuristics — keys like Authorization, X-API-Key, api_key, token, secret, password, cookie in 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)
  3. 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.

OptionTypeDescription
additionalNamesstring[]Extra key names to treat as sensitive
additionalPatternsstring[]Extra value regexes to treat as secrets
allowValuesstring[]Regexes; matching values are never flagged (e.g. test fixtures)
minSecretLengthnumberMinimum 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.

OptionTypeDescription
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

Repository payload18 top-level entries
  • .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 it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More tools

JuliusBrussee/
caveman

🪨 why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman

105k
1 add
MemPalace/
mempalace

The best-benchmarked open-source AI memory system. And it's free.

59k
stablyai/
orca

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.

66k

A cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io

132k

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

64k
headroomlabs-ai/
headroom

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.

71k