Playbook: Modifying Existing n8n Workflows via API
Applies to: n8n Cloud (p24.app.n8n.cloud) and bms-4 (n8n.bms-4.infra.zintegrowana.online)
Last updated: 2026-07-03
See also:
docs/playbooks/n8n/n8n-workflow-creation.md— creating new workflowsdocs/playbooks/n8n/n8n-http-request-quirks.md— known n8n 2.26.x bugsdocs/n8n-cloud-operations.md— n8n Cloud overviewdocs/n8n-operations.md— bms-4 n8n overview
When to use the API (not the UI)
Use this API-based approach when:
- Updating a setting on many nodes at once (e.g. retry on all HTTP nodes)
- Making batch modifications that would be tedious in the UI
- Automating workflow updates as part of a deployment step
Use the n8n UI for:
- Single-node edits
- Adding/removing nodes
- Reconnecting edges (connections are hard to edit by hand in JSON)
Platform — Always Windows PowerShell
All commands below use PowerShell (Windows dev workstation). Do not use Bash for this — the dev machine runs Windows. VPS agents (bms-4, vps-i1) can use bash with the equivalent curl commands, but the canonical procedure is PowerShell from the Windows workstation.
Step 0 — Session setup
n8n Cloud
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
# Key is stored in n8n-bms4.env.sops (for management) AND monitoring.env.sops (for exporter)
# Use n8n-bms4.env.sops as primary source for manual management tasks
$env:N8N_CLOUD_API_KEY = (
sops --decrypt --input-type dotenv --output-type dotenv `
C:\code_2026\p24-infra\secrets\n8n-bms4.env.sops |
Select-String "^N8N_CLOUD_API_KEY="
).ToString().Split("=", 2)[1]
$cloudHost = "https://p24.app.n8n.cloud"
$cloudHeaders = @{ "X-N8N-API-KEY" = $env:N8N_CLOUD_API_KEY; "Content-Type" = "application/json" }bms-4
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$env:BMS4_N8N_API_KEY = (
sops --decrypt --input-type dotenv --output-type dotenv `
C:\code_2026\p24-infra\secrets\n8n-bms4.env.sops |
Select-String "^BMS4_N8N_API_KEY="
).ToString().Split("=", 2)[1]
$bms4Host = "https://n8n.bms-4.infra.zintegrowana.online"
$bms4Headers = @{ "X-N8N-API-KEY" = $env:BMS4_N8N_API_KEY; "Content-Type" = "application/json" }Clear keys after use:
$env:N8N_CLOUD_API_KEY = ""
$env:BMS4_N8N_API_KEY = ""Step 1 — GET the workflow
# Replace $host and $headers with cloud or bms-4 variables from Step 0
$WORKFLOW_ID = "vuZ1bgFHeiLr6JXp" # example
$wf = Invoke-RestMethod -Uri "$cloudHost/api/v1/workflows/$WORKFLOW_ID" `
-Headers $cloudHeaders -Method GET
# Save a local copy before any edits (safety net)
$wf | ConvertTo-Json -Depth 30 | Out-File "$env:TEMP\wf_$WORKFLOW_ID.json" -Encoding utf8
"Node count: $(($wf.nodes).Count)"Step 2 — Modify in memory
Critical rule: use Add-Member -Force, never direct assignment
PowerShell’s ConvertFrom-Json returns PSCustomObject. You cannot set a property that
doesn’t already exist with $obj.property = value — it throws:
Exception setting "property": "The property 'property' cannot be found on this object."
Always use Add-Member with -Force — it works for both new and existing properties:
# WRONG — fails on nodes that don't already have retryOnFail
$node.retryOnFail = $true
# CORRECT — works on all nodes regardless of whether the property exists
$node | Add-Member -NotePropertyName "retryOnFail" -NotePropertyValue $true -Force
$node | Add-Member -NotePropertyName "maxTries" -NotePropertyValue 3 -Force
$node | Add-Member -NotePropertyName "waitBetweenTries" -NotePropertyValue 60000 -ForceExample: add retry to all HTTP nodes targeting a specific domain
foreach ($node in $wf.nodes) {
if ($node.type -ne "n8n-nodes-base.httpRequest") { continue }
$url = [string]$node.parameters.url # cast to string — expression nodes return PSObject
if ($url -match "pinbox24\.com" -or $node.name -like "*downloadPdf*") {
# Pinbox24 gateway — 502 recovery needs ~60s for gateway restart
$node | Add-Member -NotePropertyName "retryOnFail" -NotePropertyValue $true -Force
$node | Add-Member -NotePropertyName "maxTries" -NotePropertyValue 3 -Force
$node | Add-Member -NotePropertyName "waitBetweenTries" -NotePropertyValue 60000 -Force
}
elseif ($url -match "\.radieu\.workers\.dev") {
# Cloudflare Workers (auth, etc.) — faster recovery
$node | Add-Member -NotePropertyName "retryOnFail" -NotePropertyValue $true -Force
$node | Add-Member -NotePropertyName "maxTries" -NotePropertyValue 2 -Force
$node | Add-Member -NotePropertyName "waitBetweenTries" -NotePropertyValue 15000 -Force
}
}Retry setting values reference
| Scenario | maxTries | waitBetweenTries (ms) | Notes |
|---|---|---|---|
| Pinbox24 gateway 502 | 3 | 60 000 | Gateway restart takes ~1–2 min |
| Supabase transient | 2 | 10 000 | Usually recovers in seconds |
| CF Worker 502 | 2 | 15 000 | Faster recovery than full server |
| Discord webhook rate-limit | 3 | 5 000 | 429 not 502 — but retry still helps |
| GitHub API | 2 | 5 000 | Rate-limit is 403, transient is 5xx |
Step 3 — Validate before upload
3a. Verify modifications look correct
# Print a summary of all HTTP nodes with their retry settings
$wf.nodes | Where-Object { $_.type -eq "n8n-nodes-base.httpRequest" } | ForEach-Object {
"$($_.name): retry=$($_.retryOnFail) max=$($_.maxTries) wait=$($_.waitBetweenTries)"
}Expected output: every node should show retry=True (or explicitly retry=False if intentional).
3b. Check the body you’ll send contains only allowed fields
The n8n PUT /workflows/{id} endpoint accepts only these top-level fields:
name · nodes · connections · settings · staticData
Sending any extra field (id, versionId, active, tags, createdAt, updatedAt, etc.)
returns HTTP 400: request/body must NOT have additional properties.
Build the body explicitly:
$updateBody = @{
name = $wf.name
nodes = $wf.nodes
connections = $wf.connections
settings = $wf.settings
staticData = $wf.staticData
} | ConvertTo-Json -Depth 30
# Quick sanity check: make sure the JSON is valid and node count is preserved
$parsed = $updateBody | ConvertFrom-Json
"Nodes in body: $(($parsed.nodes).Count) (expected: $(($wf.nodes).Count))"3c. Dry-run: compare against local backup
If there is a local JSON backup from Step 1, diff the node count and names:
$original = Get-Content "$env:TEMP\wf_$WORKFLOW_ID.json" | ConvertFrom-Json
$origNames = ($original.nodes | Select-Object -ExpandProperty name) | Sort-Object
$newNames = ($wf.nodes | Select-Object -ExpandProperty name) | Sort-Object
$diff = Compare-Object $origNames $newNames
if ($diff) { "NODE MISMATCH — review before uploading:"; $diff } else { "Node names match — safe to upload" }Step 4 — PUT the workflow back
$response = Invoke-RestMethod `
-Uri "$cloudHost/api/v1/workflows/$WORKFLOW_ID" `
-Headers $cloudHeaders `
-Method PUT `
-Body $updateBody
"Upload OK — updatedAt: $($response.updatedAt)"Success: response contains updatedAt timestamp (2026-07-02T20:14:08.192Z format).
Failure: Invoke-RestMethod throws with the API error message.
Common errors:
| Error | Cause | Fix |
|---|---|---|
request/body must NOT have additional properties | Extra fields in body (id, versionId, tags, etc.) | Strip to only name, nodes, connections, settings, staticData |
401 Unauthorized | API key expired or wrong | Re-read key from SOPS; verify N8N_CLOUD_API_KEY header value |
404 Not Found | Wrong workflow ID | Check ID via GET /api/v1/workflows?limit=250 |
400 Bad Request | Malformed JSON | Check ConvertTo-Json -Depth 30 (not -Depth 5 — truncates nested objects) |
Step 5 — Verify
# Re-fetch and confirm the change persisted
$verify = Invoke-RestMethod -Uri "$cloudHost/api/v1/workflows/$WORKFLOW_ID" `
-Headers $cloudHeaders -Method GET
$verify.nodes | Where-Object { $_.type -eq "n8n-nodes-base.httpRequest" } | ForEach-Object {
"$($_.name): retry=$($_.retryOnFail) max=$($_.maxTries) wait=$($_.waitBetweenTries)"
}Complete single-run script (retry update on all HTTP nodes)
Copy-paste ready. Run once from PowerShell on the dev machine:
# ── CONFIG ──────────────────────────────────────────────────────────────────
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$WORKFLOW_ID = "vuZ1bgFHeiLr6JXp" # <-- change this
$N8N_HOST = "https://p24.app.n8n.cloud"
$KEY_NAME = "N8N_CLOUD_API_KEY" # in n8n-bms4.env.sops
# For bms-4: $N8N_HOST = "https://n8n.bms-4.infra.zintegrowana.online"; $KEY_NAME = "BMS4_N8N_API_KEY"
# ── AUTH ─────────────────────────────────────────────────────────────────────
$env:N8N_API_KEY = (
sops --decrypt --input-type dotenv --output-type dotenv `
C:\code_2026\p24-infra\secrets\n8n-bms4.env.sops |
Select-String "^$KEY_NAME="
).ToString().Split("=", 2)[1]
$headers = @{ "X-N8N-API-KEY" = $env:N8N_API_KEY; "Content-Type" = "application/json" }
# ── GET ───────────────────────────────────────────────────────────────────────
$wf = Invoke-RestMethod -Uri "$N8N_HOST/api/v1/workflows/$WORKFLOW_ID" -Headers $headers -Method GET
$wf | ConvertTo-Json -Depth 30 | Out-File "$env:TEMP\wf_$WORKFLOW_ID`_backup.json" -Encoding utf8
"GET OK — $($wf.name) — $(($wf.nodes).Count) nodes"
# ── MODIFY ────────────────────────────────────────────────────────────────────
foreach ($node in $wf.nodes) {
if ($node.type -ne "n8n-nodes-base.httpRequest") { continue }
$url = [string]$node.parameters.url
if ($url -match "pinbox24\.com" -or $node.name -like "*downloadPdf*") {
$node | Add-Member -NotePropertyName "retryOnFail" -NotePropertyValue $true -Force
$node | Add-Member -NotePropertyName "maxTries" -NotePropertyValue 3 -Force
$node | Add-Member -NotePropertyName "waitBetweenTries" -NotePropertyValue 60000 -Force
}
elseif ($url -match "\.radieu\.workers\.dev") {
$node | Add-Member -NotePropertyName "retryOnFail" -NotePropertyValue $true -Force
$node | Add-Member -NotePropertyName "maxTries" -NotePropertyValue 2 -Force
$node | Add-Member -NotePropertyName "waitBetweenTries" -NotePropertyValue 15000 -Force
}
}
# ── VALIDATE ──────────────────────────────────────────────────────────────────
"HTTP nodes after modification:"
$wf.nodes | Where-Object { $_.type -eq "n8n-nodes-base.httpRequest" } | ForEach-Object {
" $($_.name): retry=$($_.retryOnFail) max=$($_.maxTries) wait=$($_.waitBetweenTries)"
}
# ── BUILD BODY ───────────────────────────────────────────────────────────────
$body = @{
name = $wf.name
nodes = $wf.nodes
connections = $wf.connections
settings = $wf.settings
staticData = $wf.staticData
} | ConvertTo-Json -Depth 30
# ── PUT ────────────────────────────────────────────────────────────────────────
$response = Invoke-RestMethod -Uri "$N8N_HOST/api/v1/workflows/$WORKFLOW_ID" `
-Headers $headers -Method PUT -Body $body
"PUT OK — updatedAt: $($response.updatedAt)"
# ── CLEAR SECRETS ─────────────────────────────────────────────────────────────
$env:N8N_API_KEY = ""Pinbox24 auth pattern (for reference when reviewing other workflows)
All HTTP nodes calling api.w4.pinbox24.com should authenticate via a setToken Set node
that holds the bearer token, referenced as:
authorization: ={{ $('setToken').item.json.token }}
NOT via hardcoded strings or environment variables. If you see authorization as a static
string, that node uses a stale pattern and needs updating.
The p24-auth HTTP Request node (or equivalent) should call p24-auth.radieu.workers.dev/token
and its output feeds the setToken Set node. No Respond to Webhook node should exist inside
the auth sub-chain (that node is for webhook-triggered workflows only — its presence inside
a processing loop causes the execution to hang waiting for a webhook response).
Escalation
| Problem | Action |
|---|---|
| PUT fails with 400 after stripping extra fields | Log the full body to a temp file and check for null values in nodes[].parameters — n8n rejects null inside parameters |
| Workflow stops working after update | Restore from $env:TEMP\wf_$WORKFLOW_ID_backup.json — re-GET then PUT the original body |
| Retry not appearing in UI | Clear browser cache; the UI may show a cached version — check via GET API call |
| API key expired | n8n Cloud API key is not rotatable via API — must do it manually in n8n Cloud UI under Settings → n8n API, then update N8N_CLOUD_API_KEY in secrets/n8n-bms4.env.sops AND secrets/monitoring.env.sops |