This document describes the current 0.1.x shape of n8nc.
It is intentionally narrower than the original brainstorm. The tool is now specified as a same-instance workflow sync and development CLI, not as a multi-environment deployment system.
n8nc is for:
- listing workflows from a configured n8n instance
- listing recent executions and fetching one execution by ID
- fetching one or all workflows into canonical local artifacts
- creating local workflow drafts and editing local workflow JSON structurally
- creating a remote workflow from a local file and converting it into a tracked artifact
- executing non-webhook workflows, through a configured external backend or n8n's internal REST API
- validating and formatting local workflow files
- pushing a tracked workflow back safely
- activating and deactivating workflows
- calling webhook trigger URLs during development
n8nc is not yet for:
- promoting a workflow across multiple environments
- remapping credential IDs or project bindings between instances
- non-webhook execution through a stable public n8n API (none exists; the internal REST route is used instead)
Top-level commands:
n8nc
├── init
├── doctor
├── auth add
├── auth test
├── auth session add
├── auth session test
├── auth session remove
├── auth list
├── auth remove
├── ls
├── get
├── runs ls
├── runs get
├── runs watch
├── pull [--all [--active|--inactive]]
├── push
├── workflow new
├── workflow create
├── workflow execute
├── workflow show
├── workflow rm
├── node ls
├── node add
├── node set
├── node rename
├── node rm
├── conn add
├── conn rm
├── expr set
├── credential ls
├── credential schema
├── credential set
├── status
├── diff
├── activate
├── deactivate
├── trigger
├── fmt
└── validate
init creates:
.
├── n8n.toml
├── workflows/
└── .n8n/
└── cache/
n8n.toml stores:
schema_versiondefault_instanceworkflow_dir- instance aliases and base URLs
- optional per-instance workflow execute backend config
Example:
schema_version = 1
default_instance = "prod"
workflow_dir = "workflows"
[instances.prod]
base_url = "https://your-instance.app.n8n.cloud"
api_version = "v1"
[instances.prod.execute]
backend = "command"
program = "uvx"
args = ["your-mcp-runner", "execute_workflow", "{workflow_id}", "{instance_alias}"]
stdin_json = true
cwd = "."Tracked files:
workflows/<slug>--<workflow_id>.workflow.json
workflows/<slug>--<workflow_id>.meta.json
The path is intentionally environment-neutral, but the sidecar binds the file to the instance it came from. That keeps the Git history of a single instance clean while making the current scope explicit.
workflow new creates a local .workflow.json draft without a sidecar. workflow create takes a sidecar-free local file, creates it remotely, writes the tracked workflow file plus sidecar, and removes the original in-repo draft when the tracked target path changes.
New drafts and create payloads fill in these workflow settings when they are missing:
executionOrder = "v1"saveDataSuccessExecution = "all"saveDataErrorExecution = "all"saveManualExecutions = truesaveExecutionProgress = true
The cache stores one canonical base snapshot per tracked workflow:
.n8n/cache/<instance>--<workflow_id>.workflow.json
That snapshot is refreshed on pull and successful push.
pull --all batch-pulls every workflow from the remote instance. It fetches each workflow, canonicalizes and hashes the payload, and skips the disk write when the local sidecar already records a matching remote_hash. Optional --active and --inactive filters restrict which workflows are listed. Individual fetch failures are collected and reported without aborting the batch. When any workflow fails, pull --all returns exit code 6 with the full results in the JSON data field.
Credentials are resolved in this order:
N8NC_TOKEN_<ALIAS>- OS keychain entry stored by
auth add
Browser-session auth for the internal REST fallback is resolved in this order:
N8NC_SESSION_COOKIE_<ALIAS>plusN8NC_BROWSER_ID_<ALIAS>- OS keychain entries stored by
auth session add
Example:
N8NC_TOKEN_PRODN8NC_TOKEN_STAGINGN8NC_SESSION_COOKIE_PRODN8NC_BROWSER_ID_PROD
auth add is non-interactive in v0.1. You must provide --token or --stdin.
auth session add stores both the session cookie and browser ID for one alias. It accepts --cookie or --cookie-stdin, plus --browser-id.
doctor is a setup and reachability check for humans and agents.
Supported options:
--instance <alias>--skip-network
Checks currently include:
- repo
config_file - repo
workflow_dir - repo
cache_dir - repo
sensitive_data - repo
instances - repo
default_instance - per-instance
config - per-instance
token - per-instance
api - per-instance
credential_inventory - per-instance
workflow_execute
Failure behavior:
- returns exit code
13when any check fails - in JSON mode, returns an error envelope with the full doctor report attached under
data - in human mode, prints the report before returning the failure summary
repo.sensitive_data scans tracked .workflow.json files and fails when it finds likely inline secrets. It is skipped when the workflow directory is missing.
instance.credential_inventory reports whether full credential inventory is available through the public API, available only through the opt-in internal REST fallback, or limited to workflow-reference coverage.
instance.workflow_execute reports whether non-webhook execution is available: through a configured backend, or through the internal REST session fallback when session auth is stored. It is skipped only when neither is available.
The implementation targets the public n8n API with:
- header:
X-N8N-API-KEY - default base path:
/api/v1
The CLI currently assumes these workflow endpoints exist and are reachable through the public API:
GET /workflowsGET /workflows/{id}PUT /workflows/{id}POST /workflows/{id}/activatePOST /workflows/{id}/deactivate
The execution commands currently assume these endpoints exist and are reachable through the public API:
GET /executionsGET /executions/{id}
runs get uses includeData=true when fetching a single execution for --details, --summary, --node or --raw.
run_data is the canonical location for the run payload. --details strips execution.data, which carried the same bytes: on a real 27-node execution that duplicate was 1.42 MB, and --details fell from 4.0 MB to 1.5 MB. The two fields that only lived there, lastNodeExecuted and the error message, are surfaced as last_node_executed and error. --raw returns the execution exactly as n8n sent it, including n8n's internal executionData, with no derived keys.
trigger does not use the public API. It makes a direct HTTP request to a full URL or a path resolved against the configured instance base URL.
workflow execute resolves the workflow through the public API, then runs it one of two ways:
- Through a configured local backend (
[instances.<alias>.execute]), such as an MCP runner or other adapter command. - Otherwise through n8n's internal REST API,
POST /rest/workflows/:id/run, authenticated with the stored session cookie and browser id. This is the same call the editor's Execute Workflow button makes, because the public API has no run endpoint (POST /api/v1/workflows/:id/runreturns405).
The internal route is not part of n8n's public API surface and carries no stability guarantee: its request shape changed between n8n 1.x and 2.x. n8nc sends workflowData plus triggerToStartFrom, which both accept. The workflow must have a Manual Trigger node; anything else is reported as such, pointing at n8nc trigger for webhook workflows.
n8n registers the run and returns an executionId before the workflow finishes, so workflow execute polls the public executions API until it settles. --no-wait returns as soon as the run is registered, and --timeout <SECONDS> bounds the wait (default 300). A run that finishes with status error or crashed exits non-zero.
Tracked workflow files are canonical JSON.
Current canonicalization rules:
- top-level payload must be a JSON object
- top-level volatile fields are removed:
createdAtupdatedAtversionId
- top-level keys are emitted in this order when present:
idnameactivetagssettingsnodesconnections
- nested object keys are sorted
- array order is preserved
- output is pretty JSON with 2-space indentation and trailing newline
This canonicalization is versioned.
Current values:
canonical_version = 1hash_algorithm = "sha256"
Each pulled workflow has a committed sidecar:
{
"schema_version": 1,
"canonical_version": 1,
"hash_algorithm": "sha256",
"instance": "prod",
"workflow_id": "abc123",
"local_relpath": "workflows/order-alert--abc123.workflow.json",
"pulled_at": "2026-03-26T10:31:54Z",
"remote_updated_at": "2026-03-26T10:30:10Z",
"remote_hash": "sha256:..."
}The important field is remote_hash. It is the lease token used by push.
status is local by default in 0.1.x.
Base local states:
clean: workflow file and sidecar are valid, and the local canonical hash matches the recordedremote_hashmodified: workflow file and sidecar are valid, and the local canonical hash differs from the recordedremote_hashuntracked: workflow file exists without a sidecarinvalid: workflow file or sidecar cannot be used safelyorphaned_meta: sidecar exists without a matching workflow file
invalid currently covers cases like:
- workflow JSON parse failure
- sidecar parse failure
- metadata
workflow_idmismatch - unsupported
canonical_version - unsupported
hash_algorithm - validation errors such as missing node targets
status --refresh adds live remote sync classification for entries that are already clean or modified.
Remote sync states:
clean: local file still matches the remote lease recorded in the sidecarmodified: local file changed, but the remote still matches the recorded leasedrifted: local file is unchanged, but the remote no longer matches the recorded leaseconflict: both local file and remote changed since the last pull or successful pushmissing_remote: the tracked workflow no longer exists remotely
If remote refresh fails for a tracked workflow, the CLI still returns the local row, leaves sync_state unset, and records the reason in remote_detail. untracked, invalid, and orphaned_meta entries remain visible with their local state but do not contribute to sync_summary.
push is update-only in 0.1.x.
Algorithm:
- Read the local workflow file.
- Canonicalize it and hash it.
- Read the sidecar.
- Fetch the current remote workflow by ID.
- Canonicalize the remote payload and hash it.
- Compare remote hash to
meta.remote_hash.
Outcomes:
- if
remote_hash != meta.remote_hash, refuse the push with exit code12 - if
local_hash == meta.remote_hash, report no-op - otherwise, update the workflow with
PUT /workflows/{id}using onlyname,nodes,connections, andsettings
If local edits also changed unsupported top-level fields, push fails explicitly instead of silently dropping them.
After a successful push, the CLI re-fetches the workflow and re-writes the workflow and sidecar from that remote snapshot so local state stays canonical.
diff is local by default in 0.1.x.
It compares:
- the current canonical local workflow file
- the cached base snapshot from
.n8n/cache
If a cache snapshot is unavailable, diff falls back to hash and state reporting only and tells the user to re-pull the workflow to seed local diff data.
The human output includes:
- status summary
- file path
- workflow ID
- local, recorded, and base hashes when available
- changed top-level sections
- unified patch when a base snapshot exists and content changed
The JSON output includes:
- the local status object
base_hashbase_snapshot_availablechanged_sections- optional
patch
diff --refresh keeps the base snapshot comparison and also fetches the current remote workflow by ID.
Additional human output in refresh mode:
- remote sync state
- remote hash
- remote update timestamp when present
- changed top-level sections between the current remote workflow and the local file
- unified
remotevslocalpatch when both sides are available and differ
If remote refresh fails, the command still returns the local base snapshot diff, leaves the remote comparison unavailable, and records the reason in status.remote_detail.
Additional JSON fields in refresh mode:
status.sync_statestatus.remote_hashstatus.remote_updated_atstatus.remote_detailremote_comparison_availableremote_changed_sections- optional
remote_patch
The local authoring surface in 0.1.x is intentionally narrow and file-based.
Current commands:
workflow new <name> [--path <path>] [--id <id>] [--active]workflow create <file> --instance <alias> [--activate]workflow execute <id-or-name> --instance <alias> [--input <value>|--input-file <path>|--stdin]workflow show <file> [--instance <alias>]workflow rm <target> [--instance <alias>] [--local-only|--keep-local]node ls <file>node add <file> --name <name> --type <node_type> [--type-version <number>] [--x <int>] [--y <int>] [--disabled]node set <file> <node> <path> [value] [--json-value|--number|--bool|--null]node rename <file> <current_name> <new_name>node rm <file> <node>expr set <file> <node> <path> <expression>credential ls [--instance <alias>] [--workflow <id-or-name>] [--type <credential_type>] [--source auto|public|rest-session|workflow-refs]credential schema [--instance <alias>] <credential_type>credential set <file> <node> --type <credential_type> --id <credential_id> [--name <credential_name>]conn add <file> --from <node> --to <node> [--kind <type>] [--target-kind <type>] [--output-index <n>] [--input-index <n>]conn rm <file> --from <node> --to <node> [--kind <type>] [--target-kind <type>] [--output-index <n>] [--input-index <n>]
Behavior:
- all edit commands operate on local workflow files only
- edit commands rewrite the file in canonical JSON form after each successful mutation
- tracked sidecars are left untouched, so tracked files become locally
modifieduntil they are pushed - edit commands also run the sensitive-data scanner after write and include
warning_countin JSON output workflow showsummarizes local nodes, edges, credential references, and webhook URLs, using the explicit--instance, the tracked sidecar instance, or the repo default instance for local draftsworkflow createrequires a repo because it writes the new tracked file and sidecar into the configured workflow directoryworkflow createrefuses files that already have a sidecar and expects you to usepushfor tracked workflowsworkflow createremoves localidandactivebefore the create request, ensures execution-savingsettingsdefaults exist, normalizes webhook nodes for remote creation, and re-fetches the created workflow before storing the new tracked stateworkflow executerequires[instances.<alias>.execute]config for a local backend and intentionally fails with a config error when none is presentworkflow executesupports inline input, file input, or stdin; input that parses as JSON is preserved structurally, otherwise it is passed through as a plain stringworkflow executeresolves the workflow first, then expands{workflow_id},{workflow_name},{instance_alias}, and{base_url}placeholders in the configured backend argumentsworkflow executeexports workflow context through environment variables and, whenstdin_json = true, writes a JSON request envelope to stdin for adapter-style runnersauth listreports token source plus browser-session readiness for each configured instanceauth session testverifies that the internal REST credential-inventory path is reachable with the configured session cookie and browser IDcredential lsdefaults to--source autocredential ls --source autoresolves inventory in this order:- public API inventory via
GET /api/v1/credentials - internal REST inventory via
GET /rest/credentialsonly if browser-session auth is configured throughauth session addor bothN8NC_SESSION_COOKIE_<ALIAS>andN8NC_BROWSER_ID_<ALIAS> - workflow-reference discovery from fetched workflows
- public API inventory via
credential ls --source publicrequires the public credential inventory endpoint to work and fails explicitly if it does notcredential ls --source rest-sessionrequires browser-session auth and uses the internal browser-session route intentionally as an opt-in fallback, not a default dependencycredential ls --source workflow-refsonly reports credentials referenced by workflowscredential ls --workflow <id-or-name>is workflow-reference scoped and therefore only works with--source autoor--source workflow-refs- full-inventory sources still enrich results with workflow usage counts by scanning current workflow references; unused credentials show
usage_count = 0 credential schemareturns the official schema payload fromcredentials/schema/{credentialTypeName}credential setrequires an existing credential ID, which can come fromcredential ls, the n8n UI, or another trusted sourceworkflow rmaccepts a workflow file path, workflow ID, or exact workflow nameworkflow rm <file>removes a local draft directly; for tracked files it also deletes the remote workflow unless--local-onlyis setworkflow rm <id-or-name>deletes the remote workflow and removes matching tracked local artifacts unless--keep-localis set
Webhook-specific behavior:
node add --type n8n-nodes-base.webhookdefaultstypeVersionto2- webhook nodes get an auto-derived
webhookId - setting
pathnormalizes leading and trailing slashes - when
webhookIdstill uses the auto-derived value, changingpathupdateswebhookIdtoo workflow create,workflow show, andactivatereturn resolved production and test webhook URLs when a base URL is available
Path rules for node set and expr set:
urlmeansparameters.urloptions.timeoutmeansparameters.options.timeout- explicit top-level node fields such as
position,disabled,typeVersion,notes,alwaysOutputData, and retry-related fields are supported directly id,name,type, andcredentialsare intentionally blocked fromnode set
Expression rules:
- if the input already looks like
={{ ... }}, it is preserved - if the input looks like
{{ ... }}, the CLI prefixes= - otherwise the CLI wraps the value as
={{...}}
Connection rules:
- source and target node names must already exist
- duplicate connection edges are deduplicated
- the default source output type is
main - the default target input type is the same as
--kind node renamerewrites the node name, outbound connection key, and inbound edge targetsnode rmremoves the node, its outbound key, and inbound edges pointing at itconn rmremoves matching edges without disturbing other edges in the same branch
validate currently checks:
- file parses as JSON
- workflow payload is an object
idexistsnodesis an arrayconnectionsis an object- node names are unique
- connection targets point to existing node names
- if a sidecar exists,
workflow_idmatches the workflow file
validate also emits non-fatal warnings for likely sensitive literals in tracked workflow files, including:
- inline private key material
- URLs with embedded basic-auth credentials
- token-like literal prefixes such as
sk-,ghp_,github_pat_,xoxb-,xoxp-, andBearer ... - literal values stored under field names like
password,token,secret,clientSecret, orapiKey
The scanner intentionally ignores obvious placeholders and common n8n dynamic references such as ={{ ... }} and $env.*.
Warnings do not fail validate, but they are returned in human output, JSON output, and the post-write summaries from pull and successful push.
get <id-or-name> prints the canonical workflow JSON. A real workflow is tens of kilobytes, and the usual question is about one node, so three projections answer it directly without shipping the rest:
get <id> --node "<NAME>"— one node's definition, matched by display name then byid, the same resolutionnode setuses. An unknown name is anot_founderror (exit11), never an empty result.get <id> --nodes— a summary row per node: name, type,typeVersion, disabled.get <id> --connections— the wiring, somain[0](true) andmain[1](false) branches are readable directly.
The three are mutually exclusive. On a 52 KB production workflow, --node returns 662 bytes.
These mirror runs get --summary / --node, which do the same for executions.
node set-remote <workflow> <node> <path> <value> edits one node on a remote workflow without making it a tracked artifact. It takes the same node, path and value syntax as node set, including --value-file for multiline bodies, and runs the identical in-memory mutation, so the two cannot drift apart.
The write is guarded:
- Lease. The workflow is re-read immediately before writing. If it changed on the remote since it was read, the command exits
12(conflict) rather than overwriting a concurrent edit. - Mutable fields only. The payload goes through the same
workflow_update_payloadthatpushuses, soid,activeand timestamps are never sent. - Active is preserved. n8n's update endpoint can return the workflow with
activecleared. When that happens the workflow is reactivated andreactivated: trueis reported. - No-op writes nothing. If the value already matches, no
PUTis issued andchanged: falseis reported. --dry-runreports the change without writing.
This exists because the safe path (pull -> node set -> push --verify) requires tracking the workflow in a repo. For a one-off fix to a workflow you do not want to track, that ceremony pushes people into hand-rolled PUT /api/v1/workflows/:id calls, which is exactly where active gets silently dropped.
runs stats [workflow] aggregates every execution in the window. It never samples: pagination walks each page, so the totals cannot be computed from a truncated set.
--status <STATUS>filters server-side, so--status erroraggregates only failures.--by workflowbreaks the window down per workflow, ranked by failures first. That answers "which workflow is failing" in one call rather than by paginating executions by hand.- Cadence is always reported:
first,last, and the largest interval between consecutive executions asmax_gap_mswith itsmax_gap_from/max_gap_toendpoints.
Cadence turns a monitoring workflow into an outage detector. A heartbeat running every five minutes, with a gap of a hundred, means the instance was down:
$ n8nc runs stats <heartbeat-id> --last 7d
Cadence:
First: 2026-07-03T14:30:19.000Z
Last: 2026-07-10T14:25:54.000Z
Largest gap: 1h 40m (2026-07-05T09:35:19.000Z -> 2026-07-05T11:15:54.000Z)
With no executions in the window, first, last and max_gap_ms are absent rather than zero: nothing ran and a gap of zero are different facts.
lint checks tracked workflow files against configurable rules. Severities are off, warn, or error, set per rule in n8n.toml. Any diagnostic at error fails the command.
| rule | default | what it catches |
|---|---|---|
no-hardcoded-urls |
warn | HTTP Request nodes with a literal URL instead of an expression |
no-disabled-nodes |
warn | nodes left disabled in a tracked workflow |
require-error-handler |
off | nodes without an error-handling path |
no-default-names |
warn | nodes still carrying their default name |
no-empty-expressions |
warn | expressions that resolve to nothing |
params-match-type-version |
warn | parameters written for a different typeVersion than the node declares |
params-match-type-version exists because n8n resolves a node's parameter schema from its typeVersion and then reads keys that may not be there, without warning. An IF at typeVersion: 2 whose parameters still use the v1 conditions.boolean shape evaluates zero conditions, so every item takes the true branch and the guard is silently inert. The mirror case is equally quiet: Set v3 assignments on a typeVersion: 1 node assigns nothing.
Only migrations confirmed against real workflows are encoded (if, filter, set). An unrecognised node type is never flagged, so the rule cannot invent a false positive. Raise it to error in n8n.toml if a silently-inert guard should fail your build.
runs ls returns recent executions from the remote instance.
Current supported options:
--limit--workflow <id-or-exact-name>--status <value>--since <RFC3339>--last <window>
Time filtering is client-side in 0.1.x. The CLI pages through recent executions until it collects the requested number of matching rows or exhausts the remote result set.
Because execution listings are treated as recent-first, the CLI stops paging once a page has crossed below the active --since cutoff.
--since includes executions at or after the given timestamp.
--last computes a rolling window from the current time and accepts these suffixes:
ssecondsmminuteshhoursddays
List rows currently include:
- execution ID
- workflow ID
- workflow name when it can be resolved
- status
- mode
- started and stopped timestamps
- computed duration in milliseconds when both timestamps exist
If runs ls --workflow ... returns zero rows for an active workflow whose settings do not explicitly save successful production executions, the CLI includes a note explaining that successful runs may not appear in history.
runs get <execution-id> returns the execution summary.
runs get <execution-id> --details fetches the detailed execution payload and, in human output, summarizes:
- workflow name and ID
- status and mode
- start and stop timestamps
- computed duration
- node-level execution status
- node execution time
- output item counts per node based on
data.resultData.runData
In JSON output, runs get --details returns:
execution: the raw detailed execution payload from n8nrun_data: the extracteddata.resultData.runDataobject when presentnode_executions: a stable per-node summary array with status, execution time, and output item counts
runs watch polls the execution list repeatedly and is intended for active debugging sessions.
Current supported options:
--workflow <id-or-exact-name>--status <value>--since <RFC3339>--last <window>--limit--interval <seconds>--iterations <count>
Human output behavior:
- first poll prints the current execution window
- prints the active workflow, status, and time-window filters when present
- later polls print only newly seen executions
- no output is emitted for unchanged polls after the initial snapshot
JSON output behavior:
- emits one compact JSON envelope per poll
- first poll uses
event = "snapshot" - later polls use
event = "update"when new executions appear - later polls use
event = "heartbeat"when no new execution IDs appear
Each JSON watch event currently includes:
pollinterval_secondscountnew_countexecutionsnew_executions
The current diagnostics model is intentionally simple:
severitycodemessagefile- optional JSON path
- optional suggestion
The user concern that started this implementation was valid: developers need more than pull and push.
The current answer is:
- use
lsandgetfor fast inspection - use
activateanddeactivatefor workflow state changes; they wait for the remote state to converge and refresh tracked local artifacts when available - use
triggerfor webhook-based development flows - use
workflow executefor non-webhook workflows when you have a configured local backend
trigger supports:
- full URLs
- instance-relative paths
- custom method
- repeated
--header key:value - repeated
--query key=value - request body from
--data,--data-file, or--stdin
If the request body looks like JSON and no Content-Type header was provided explicitly, trigger sends Content-Type: application/json.
Webhook-specific error handling:
- non-2xx responses include the resolved request path and a summarized response body in the error message
404responses for/webhook-test/...explain that test listeners must be active in the n8n editor404responses for/webhook/...explain that the path may be wrong, the workflow may be inactive, or n8n may not have registered the webhook yet
This avoids pretending there is a stable public “run workflow by ID” endpoint when that has not been verified in the implementation.
workflow execute supports:
- workflow ID or exact workflow name resolution through the public API
- optional input from
--input,--input-file, or--stdin - adapter commands configured per instance in
n8n.toml - placeholder expansion in configured args:
{workflow_id}{workflow_name}{instance_alias}{base_url}
- execution context in environment variables:
N8NC_EXECUTE_INSTANCE_ALIASN8NC_EXECUTE_BASE_URLN8NC_EXECUTE_WORKFLOW_IDN8NC_EXECUTE_WORKFLOW_NAMEN8NC_EXECUTE_WORKFLOW_ACTIVEN8NC_EXECUTE_INPUT_JSON
- optional stdin JSON request envelopes when
stdin_json = true
Example adapter request body:
{
"tool": "execute_workflow",
"instance_alias": "prod",
"base_url": "https://example.n8n.cloud",
"workflow": {
"id": "abc123",
"name": "Nightly Digest",
"active": true
},
"input": {
"dryRun": true
}
}This keeps the semantics honest:
triggermeans “call a webhook URL”workflow executemeans “ask a configured local adapter to run this workflow”
Every command supports --json.
Success envelope:
{
"ok": true,
"command": "ls",
"version": "0.1.0",
"contract_version": 1,
"data": {}
}Error envelope:
{
"ok": false,
"command": "push",
"version": "0.1.0",
"contract_version": 1,
"error": {
"code": "conflict.remote_changed",
"message": "Remote workflow changed since the last pull."
}
}Validation failures and doctor failures may also include a data object with diagnostics or the full doctor report.
validate success and failure payloads include both error_count and warning_count. pull and successful push also include warning_count, plus diagnostics when warnings are present.
Local edit command success payloads include:
workflow_pathchangedwarning_count- command-specific fields such as
workflow_id,node,path,from,to, orcredential_type
workflow create success payloads also include:
instancesource_pathsource_removedmeta_path- optional
active - optional
webhooks
workflow rm success payloads include:
target- optional
workflow_id - optional
workflow_name - optional
instance remote_removedlocal_removedremoved_paths
workflow execute success payloads include:
action = "execute"instanceworkflow_idworkflow_name- optional
active execution.backendexecution.program- optional
execution.args - optional
execution.cwd - optional
execution.output - optional
execution.stderr
0: success2: usage error3: config error4: auth error5: network error6: API error10: validation error11: not found12: conflict refusal13: doctor failures
The n8n public API caps a page at 250 rows on both /workflows and /executions, and returns 400 above it. That cap is a page size, not a limit on what a caller may request.
--limitis the number of results wanted, not the page size.n8nc runs ls --limit 600walksnextCursoracross three pages of 250 and returns 600 rows.- Commands that need the whole set (
runs stats,pull --all, the credential inventory) walk every page, so their aggregates are never computed from a truncated sample. truncated: trueappears in the JSON envelope when--limitstopped the walk while rows remained. It errs towardtrue: anextCursorproves the source has more rows, not that any of them still match a client-side filter such as--name. Where exhaustion can be proven it is, so a page that already crossed a--sincecutoff reportsfalse. The error is a needless "there may be more", never a silent "this is everything". In text mode the same condition prints a note on stderr, for bothlsandruns ls.--offsetis applied in-band, after the fetch. Commands therefore fetchoffset + limitresults, so--offset 20 --limit 20returns the second page of twenty rather than nothing. Pages are contiguous and non-overlapping.runs ls --explainissues oneGET /executions/<id>?includeData=trueper returned row. That response carries the whole run payload, so pair--explainwith a small--limitand a--statusfilter.
- The tool is currently strongest when a repo mirrors one n8n instance.
workflow createdepends on the public workflow-create endpoint and still assumes the returned payload can be stored with the same canonicalization rules as pulled workflows.tagsare preserved structurally, not normalized semantically.lsassumes a paginated workflow list response withdataand optionalnextCursor.- remote drift and API health remain opt-in via
status --refresh,diff --refresh, anddoctor. doctoruses a cheap workflow-list probe and does not verify every endpoint.diffis best after a freshpull, because older repos may not have cached base snapshots yet.- sensitive-data scanning is heuristic. It is tuned to catch likely mistakes, not to prove a workflow is secret-free.
- non-webhook execution currently depends on a local adapter command you configure yourself;
n8ncdoes not ship an MCP runner or other backend implementation. - archive support is still outside the CLI until a stable public endpoint is verified.
The next improvements that fit the current design are:
- shell completions and packaging
- more contract snapshot coverage for agent-facing JSON
- richer workflow inspection or graph rendering in human output
- optional archive support if a stable public endpoint exists
- only after that: a real environment-promotion model with explicit mappings and lock files