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 workflows
  • docs/playbooks/n8n/n8n-http-request-quirks.md — known n8n 2.26.x bugs
  • docs/n8n-cloud-operations.md — n8n Cloud overview
  • docs/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  -Force

Example: 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

ScenariomaxTrieswaitBetweenTries (ms)Notes
Pinbox24 gateway 502360 000Gateway restart takes ~1–2 min
Supabase transient210 000Usually recovers in seconds
CF Worker 502215 000Faster recovery than full server
Discord webhook rate-limit35 000429 not 502 — but retry still helps
GitHub API25 000Rate-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:

ErrorCauseFix
request/body must NOT have additional propertiesExtra fields in body (id, versionId, tags, etc.)Strip to only name, nodes, connections, settings, staticData
401 UnauthorizedAPI key expired or wrongRe-read key from SOPS; verify N8N_CLOUD_API_KEY header value
404 Not FoundWrong workflow IDCheck ID via GET /api/v1/workflows?limit=250
400 Bad RequestMalformed JSONCheck 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

ProblemAction
PUT fails with 400 after stripping extra fieldsLog the full body to a temp file and check for null values in nodes[].parameters — n8n rejects null inside parameters
Workflow stops working after updateRestore from $env:TEMP\wf_$WORKFLOW_ID_backup.json — re-GET then PUT the original body
Retry not appearing in UIClear browser cache; the UI may show a cached version — check via GET API call
API key expiredn8n 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