| name | discovering-n8n-instance |
| description | Connects to a user's n8n instance via REST API to catalog all workflows, credentials, server resources, and n8n version. Activate when the user provides n8n connection details, types /start, or asks to begin migration, connect to their server, or discover their workflows.
|
Discovering n8n Instance
You are performing initial reconnaissance on a user's production n8n instance.
Your goal: build a complete inventory without modifying anything.
1. What You Need From the User
Before proceeding, you MUST have:
| Required | How to Get It |
|---|
| n8n Base URL | Ask user (e.g., https://n8n.example.com or http://IP:5678) |
| n8n API Key | User creates in n8n: Settings → n8n API → Create API Key |
| Optional | Purpose |
|---|
SSH access (user@ip) | Server resource metrics for benchmarking phase |
If the user provides SSH, remind them: "I recommend creating a read-only Linux user
for this. I will only run top, df, free, and docker stats — nothing else."
2. Connection Verification
Test connectivity before doing anything else:
curl -s https://<BASE_URL>/healthz
curl -s https://<BASE_URL>/healthz/readiness
curl -s -I -H "X-N8N-API-KEY: <KEY>" https://<BASE_URL>/api/v1/workflows?limit=1
If any of these fail, STOP. Help the user debug (wrong URL? firewall? API key not
created yet?). Do NOT proceed until connectivity is confirmed.
3. Extraction Sequence
Execute in this exact order:
3a. List All Workflows
curl -s -H "X-N8N-API-KEY: <KEY>" \
"https://<BASE_URL>/api/v1/workflows?limit=250" | jq '.'
Save to migration-state/discovery/workflow-list.json.
For each workflow, extract and save the full definition:
curl -s -H "X-N8N-API-KEY: <KEY>" \
"https://<BASE_URL>/api/v1/workflows/<ID>" \
> migration-state/discovery/workflows/<ID>_<sanitized_name>.json
3b. List All Credentials (names and types only)
curl -s -H "X-N8N-API-KEY: <KEY>" \
"https://<BASE_URL>/api/v1/credentials" | jq '.'
Save to migration-state/discovery/credentials-list.json.
WARNING: This returns credential names and types, NOT secret values. NEVER
attempt to extract, decrypt, or access actual credential values.
3c. Pull Recent Execution History — ALWAYS with ?includeData=true
For each active workflow, pull the last 20 successful executions with full
node-level I/O data on the first pass:
curl -s -H "X-N8N-API-KEY: <KEY>" \
"https://<BASE_URL>/api/v1/executions?workflowId=<ID>&status=success&limit=20&includeData=true" \
| jq '.'
Save to migration-state/discovery/executions/<workflow_id>/.
CRITICAL: Do NOT skip includeData=true "to save bandwidth". Without it the
executions list is just IDs and timing — you will end up re-fetching every
execution individually for the analysis phase (fixture extraction) AND again for
the benchmarking phase (per-interaction timing). Pull it once, with data.
Then for each execution, get the full data (also includeData=true):
curl -s -H "X-N8N-API-KEY: <KEY>" \
"https://<BASE_URL>/api/v1/executions/<EXEC_ID>?includeData=true" \
> migration-state/discovery/executions/<workflow_id>/<exec_id>.json
Note: n8n prunes executions after 14 days by default. If the user needs
older data, inform them: "You may want to temporarily increase
EXECUTIONS_DATA_MAX_AGE in your n8n environment variables."
3c.1 — Capture Disabled Nodes (Deliberate Divergences)
For every workflow, capture the list of disabled nodes — they represent
behavior the user has deliberately turned OFF in production. The translation
phase uses this to skip them by default.
for wf in migration-state/discovery/workflows/*.json; do
jq --arg f "$(basename "$wf" .json)" '
{workflow: $f, disabled_nodes: [.nodes[] | select(.disabled == true) | .name]}
' "$wf"
done > migration-state/discovery/disabled-nodes.json
Surface these in the discovery report under "Deliberate divergences detected".
3c.2 — Capture Production Row Counts
If a Postgres credential is in use, ask the user to run (or run via a read-only
connection) a quick row-count snapshot. Required for benchmarking extrapolation
later — much easier to capture once during discovery than to retrofit at the end.
Suggested queries (project-specific — adapt to the actual schema):
SELECT count(*) AS total_users FROM users;
SELECT count(*) AS active_users FROM users WHERE is_paused = false;
SELECT count(*) AS daily_active FROM users WHERE last_active_at > NOW() - INTERVAL '24 hours';
Save to migration-state/discovery/db-row-counts.json.
3d. Server Resources (if SSH provided)
ssh user@ip "top -bn1 | head -20"
ssh user@ip "free -h"
ssh user@ip "df -h"
ssh user@ip "docker stats --no-stream"
Save to migration-state/discovery/server-baseline.txt.
3d.1 — Container Timezone
n8n's CronTrigger interprets schedules in the container's timezone. You MUST
record this so the translation phase can pass timezone="..." explicitly to
APScheduler (which defaults to UTC even inside a non-UTC container).
ssh user@ip 'docker inspect n8n --format "{{json .Config.Env}}" | grep -o "TZ=[^,\"]*"'
ssh user@ip 'docker exec n8n date'
ssh user@ip 'docker exec n8n cat /etc/timezone 2>/dev/null || true'
Save the result to migration-state/discovery/container-tz.txt.
3d.2 — Authoritative CPU Baseline (cgroup, not docker stats)
docker stats --no-stream is a 1-second snapshot and has nasty artifacts —
postgres routinely alternates 0% ↔ 100% in samples. For a real baseline that
benchmarking can compare against, capture cumulative cgroup CPU time and the
container start time. The lifetime average is then (cpu.stat.usage_usec / (now - started_at) / num_cores).
ssh user@ip 'docker inspect n8n --format "{{.State.StartedAt}}"'
ssh user@ip 'docker exec n8n cat /sys/fs/cgroup/cpu.stat 2>/dev/null \
|| docker exec n8n cat /sys/fs/cgroup/cpuacct/cpuacct.usage 2>/dev/null'
Repeat for each container the user is benchmarking against (n8n, postgres,
redis, etc.). Save to migration-state/discovery/cgroup-baseline.json.
3d.3 — Droplet / VM Size and Price
If you have SSH, infer the host shape so benchmarking can produce a real
"could fit on a smaller server" cost number without re-asking the user later:
ssh user@ip "nproc"
ssh user@ip "free -h | awk '/^Mem:/ {print \$2}'"
ssh user@ip "cat /etc/cloud/build.info 2>/dev/null || true"
ssh user@ip "curl -s http://169.254.169.254/metadata/v1.json 2>/dev/null | jq . || true"
Save to migration-state/discovery/host-shape.json. The benchmarking phase
will combine this with a provider pricing table — you generally do NOT need to
ask the user "what droplet size are you on?" if SSH is available.
4. Build the Manifest
After extraction, create migration-state/manifest.json:
{
"toolkit_version": "1.0.0",
"started_at": "<timestamp>",
"n8n_url": "<base_url>",
"n8n_version": "<detected_version>",
"ssh_available": true,
"current_phase": "discovery",
"workflows": [
{
"id": "<workflow_id>",
"name": "<workflow_name>",
"active": true,
"node_count": 0,
"executions_pulled": 0,
"status": "discovered",
"phases": {
"analyzed": false,
"credentials_ready": false,
"translated": false,
"tested": false,
"benchmarked": false
}
}
],
"integrations": [],
"total_workflows": 0,
"total_active_workflows": 0,
"total_nodes_across_all": 0
}
5. Present Discovery Report
After all data is collected, present a summary to the user:
Discovery Report
================
n8n Version: [version]
Server: [IP / hostname]
Database: [Postgres / SQLite — detected from env or config]
Workflows Found: [total] ([active] active, [inactive] inactive)
Total Nodes: [count across all workflows]
Credentials: [count] ([list types: Telegram, Postgres, HTTP Header, etc.])
Execution History Available: [count] executions across [n] workflows
Oldest execution: [date]
Newest execution: [date]
Server Resources (if SSH available):
Host shape: [N vCPU / Y GB RAM / Z GB disk] — [provider/droplet size if detected]
Load: [1m/5m/15m]
n8n Container: [CPU% / MEM%]
n8n Container TZ: [e.g., Africa/Cairo]
n8n Lifetime CPU: [X% over Y days from cgroup cpu.stat]
Postgres lifetime CPU: [X% over Y days]
Database Snapshot (if available):
Total users: [N]
Active users: [N]
Daily active: [N]
Deliberate Divergences Detected:
- "Daily 4am Check" disabled in workflow X (entire chain skipped)
- Node "Old Notification" disabled in workflow Y
→ Translation will SKIP these by default. Re-enable explicitly if desired.
Top 5 Largest Workflows:
1. [name] — [node_count] nodes
2. ...
Flags:
- [any warnings: e.g., "SQLite detected — recommend migrating to Postgres first"]
- [any very large workflows that will need subgraph decomposition]
- [any deprecated nodes detected]
Then ask: "This is everything I found. Ready to proceed to Phase 2 (Analysis)?"
6. Things You Should NOT Need To Ask The User
The discovery phase is the right time to capture data so the user is not asked
the same question later in the migration. If SSH is available, infer the answer
instead of prompting. Only ask if SSH is unavailable AND the data isn't in the
discovery output.
| Question | Auto-capture instead from |
|---|
| "How many active users does production have?" | §3c.2 db row counts |
| "What's the current droplet size and price?" | §3d.3 host shape + provider pricing table |
| "What timezone does n8n run in?" | §3d.1 container TZ |
| "Should I keep this disabled node enabled?" | §3c.1 disabled-nodes list — present once during analysis |
| "What's the n8n container's CPU baseline?" | §3d.2 cgroup cpu.stat |