The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Addendo \u2014 Cost Guard v1",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "cost-guard-v1",
"responseMode": "responseNode",
"options": {}
},
"id": "1a000000-0000-4000-8000-000000000001",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
0
]
},
{
"parameters": {
"jsCode": "// Validate body and compute UTC hour key.\n// _route in {invalid, check, record}.\nconst raw = $input.first().json;\nconst body = (raw && typeof raw === 'object' && raw.body && typeof raw.body === 'object') ? raw.body : raw;\n\nconst errors = [];\nconst mode = body && body.mode;\nif (mode !== 'check' && mode !== 'record') {\n errors.push(\"mode must be 'check' or 'record'\");\n}\n\nconst workflowId = body && body.workflow_id;\nif (typeof workflowId !== 'string' || workflowId.trim() === '') {\n errors.push('workflow_id required (non-empty string)');\n}\n\nlet costUsd = 0;\nif (mode === 'record') {\n costUsd = Number(body.cost_usd);\n if (!Number.isFinite(costUsd) || costUsd <= 0) {\n errors.push('cost_usd required (positive number) when mode=record');\n } else if (costUsd > 100) {\n errors.push('cost_usd exceeds sanity limit (100 USD)');\n }\n}\n\nconst d = new Date();\nconst pad = n => String(n).padStart(2, '0');\nconst hourKey = `sec:cost:hourly:${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}-${pad(d.getUTCHours())}`;\n\nif (errors.length > 0) {\n return [{\n json: {\n _route: 'invalid',\n _status: 400,\n error: 'validation_failed',\n details: errors\n }\n }];\n}\n\nreturn [{\n json: {\n _route: mode,\n workflow_id: workflowId.trim(),\n cost_usd: costUsd,\n key: hourKey,\n cap_usd: 3.00\n }\n}];"
},
"id": "1a000000-0000-4000-8000-000000000002",
"name": "Validate & Build Key",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "cond-invalid",
"leftValue": "={{ $json._route }}",
"rightValue": "invalid",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "1a000000-0000-4000-8000-000000000003",
"name": "IF invalid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
480,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { error: $json.error, details: $json.details } }}",
"options": {
"responseCode": 400
}
},
"id": "1a000000-0000-4000-8000-000000000004",
"name": "Respond 400",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
720,
-200
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "cond-check",
"leftValue": "={{ $json._route }}",
"rightValue": "check",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "1a000000-0000-4000-8000-000000000005",
"name": "IF check?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
720,
200
]
},
{
"parameters": {
"operation": "get",
"key": "={{ $json.key }}",
"options": {}
},
"id": "1a000000-0000-4000-8000-000000000006",
"name": "Redis GET (check)",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
960,
100
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Parse Redis GET output (check mode).\nconst input = $input.first().json;\nconst meta = $('Validate & Build Key').first().json;\nconst key = meta.key;\nconst cap = meta.cap_usd;\n\n// n8n Redis GET returns { propertyName: <value> } when value exists,\n// or { propertyName: null } / empty object when key absent.\n// Discover the value field robustly.\nlet rawValue = null;\nfor (const k of Object.keys(input)) {\n if (input[k] !== undefined && input[k] !== null && k !== 'key') {\n rawValue = input[k];\n break;\n }\n}\n// Fallback: also handle direct value or 'value' field\nif (rawValue === null && typeof input === 'object') {\n if ('value' in input) rawValue = input.value;\n else if (key in input) rawValue = input[key];\n}\n\nconst current = (rawValue === null || rawValue === '' || rawValue === undefined)\n ? 0.00\n : Number(rawValue);\n\nif (!Number.isFinite(current)) {\n console.log(JSON.stringify({\n level: 'ERROR', component: 'cost-guard-v1',\n event: 'corrupt_value_failclosed', mode: 'check', key, raw_value: rawValue\n }));\n return [{ json: { allow: false, reason: 'redis_corrupt_value_failclosed', key, cap_usd: cap } }];\n}\n\nif (current >= cap) {\n return [{ json: { allow: false, reason: 'hourly_cap_exceeded', current_usd: current, cap_usd: cap, key } }];\n}\nreturn [{ json: { allow: true, current_usd: current, cap_usd: cap, key } }];"
},
"id": "1a000000-0000-4000-8000-000000000007",
"name": "Parse CHECK",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
100
]
},
{
"parameters": {
"jsCode": "// FAIL-CLOSED: Redis unavailable on check.\nconst meta = $('Validate & Build Key').first().json;\nconsole.log(JSON.stringify({\n level: 'ERROR', component: 'cost-guard-v1',\n event: 'redis_unavailable_failclosed', mode: 'check',\n key: meta.key, error: $input.first().json\n}));\nreturn [{ json: { allow: false, reason: 'redis_unavailable_failclosed', key: meta.key, cap_usd: meta.cap_usd } }];"
},
"id": "1a000000-0000-4000-8000-000000000008",
"name": "Fail-Closed CHECK",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
280
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "1a000000-0000-4000-8000-000000000009",
"name": "Respond CHECK",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1440,
180
]
},
{
"parameters": {
"operation": "get",
"key": "={{ $json.key }}",
"options": {}
},
"id": "1a000000-0000-4000-8000-00000000000a",
"name": "Redis GET (record)",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
960,
400
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Compute new total = current + cost. Carry forward all fields needed by next nodes.\nconst input = $input.first().json;\nconst meta = $('Validate & Build Key').first().json;\nconst key = meta.key;\nconst costUsd = meta.cost_usd;\n\nlet rawValue = null;\nfor (const k of Object.keys(input)) {\n if (input[k] !== undefined && input[k] !== null && k !== 'key') {\n rawValue = input[k];\n break;\n }\n}\nif (rawValue === null && typeof input === 'object') {\n if ('value' in input) rawValue = input.value;\n else if (key in input) rawValue = input[key];\n}\n\nconst current = (rawValue === null || rawValue === '' || rawValue === undefined)\n ? 0.00\n : Number(rawValue);\n\nif (!Number.isFinite(current)) {\n // Treat corrupt as fresh (defensive). Log warn.\n console.log(JSON.stringify({\n level: 'WARN', component: 'cost-guard-v1',\n event: 'record_corrupt_existing_value_reset', key, raw_value: rawValue\n }));\n}\n\nconst safeCurrent = Number.isFinite(current) ? current : 0;\n// Round to 8 decimals to avoid float drift on long sequences\nconst newTotal = Math.round((safeCurrent + costUsd) * 1e8) / 1e8;\n\nreturn [{\n json: {\n key,\n new_total_usd: newTotal,\n cost_usd_pendiente: costUsd,\n workflow_id_caller: meta.workflow_id\n }\n}];"
},
"id": "1a000000-0000-4000-8000-00000000000b",
"name": "Compute New Total",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
380
]
},
{
"parameters": {
"operation": "set",
"key": "={{ $json.key }}",
"value": "={{ $json.new_total_usd.toString() }}",
"keyType": "string",
"expire": true,
"ttl": 3600
},
"id": "1a000000-0000-4000-8000-00000000000c",
"name": "Redis SET (record)",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
1440,
380
],
"credentials": {
"redis": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Build success response for record.\nconst prev = $('Compute New Total').first().json;\nreturn [{\n json: {\n recorded: true,\n new_total_usd: prev.new_total_usd,\n key: prev.key,\n ttl_seconds: 3600\n }\n}];"
},
"id": "1a000000-0000-4000-8000-00000000000d",
"name": "Build RECORD success",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
320
]
},
{
"parameters": {
"jsCode": "// FAIL: Redis unavailable on record (GET or SET branch). Structured alert + alert_sent flag.\nconst meta = $('Validate & Build Key').first().json;\nconst alertEvent = {\n level: 'CRITICAL',\n component: 'cost-guard-v1',\n event: 'record_redis_unavailable',\n mode: 'record',\n key: meta.key,\n cost_usd_pendiente: meta.cost_usd,\n workflow_id_caller: meta.workflow_id,\n raw_error: $input.first().json,\n timestamp_utc: new Date().toISOString()\n};\nconsole.log('COST_GUARD_ALERT ' + JSON.stringify(alertEvent));\nreturn [{\n json: {\n recorded: false,\n reason: 'redis_unavailable',\n cost_usd_pendiente: meta.cost_usd,\n key: meta.key,\n alert_sent: true\n }\n}];"
},
"id": "1a000000-0000-4000-8000-00000000000e",
"name": "Fail-Closed RECORD",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
580
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "1a000000-0000-4000-8000-00000000000f",
"name": "Respond RECORD",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1920,
480
]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Validate & Build Key",
"type": "main",
"index": 0
}
]
]
},
"Validate & Build Key": {
"main": [
[
{
"node": "IF invalid?",
"type": "main",
"index": 0
}
]
]
},
"IF invalid?": {
"main": [
[
{
"node": "Respond 400",
"type": "main",
"index": 0
}
],
[
{
"node": "IF check?",
"type": "main",
"index": 0
}
]
]
},
"IF check?": {
"main": [
[
{
"node": "Redis GET (check)",
"type": "main",
"index": 0
}
],
[
{
"node": "Redis GET (record)",
"type": "main",
"index": 0
}
]
]
},
"Redis GET (check)": {
"main": [
[
{
"node": "Parse CHECK",
"type": "main",
"index": 0
}
],
[
{
"node": "Fail-Closed CHECK",
"type": "main",
"index": 0
}
]
]
},
"Parse CHECK": {
"main": [
[
{
"node": "Respond CHECK",
"type": "main",
"index": 0
}
]
]
},
"Fail-Closed CHECK": {
"main": [
[
{
"node": "Respond CHECK",
"type": "main",
"index": 0
}
]
]
},
"Redis GET (record)": {
"main": [
[
{
"node": "Compute New Total",
"type": "main",
"index": 0
}
],
[
{
"node": "Fail-Closed RECORD",
"type": "main",
"index": 0
}
]
]
},
"Compute New Total": {
"main": [
[
{
"node": "Redis SET (record)",
"type": "main",
"index": 0
}
]
]
},
"Redis SET (record)": {
"main": [
[
{
"node": "Build RECORD success",
"type": "main",
"index": 0
}
],
[
{
"node": "Fail-Closed RECORD",
"type": "main",
"index": 0
}
]
]
},
"Build RECORD success": {
"main": [
[
{
"node": "Respond RECORD",
"type": "main",
"index": 0
}
]
]
},
"Fail-Closed RECORD": {
"main": [
[
{
"node": "Respond RECORD",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"timezone": "UTC"
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
redis
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Addendo — Cost Guard v1. Uses redis. Webhook trigger; 15 nodes.
Source: https://github.com/AddendoGrowthPartner/addendo-website/blob/main/workflows/subworkflows/Cost-Guard-v1.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Statemachine. Uses redis, redisTrigger. Webhook trigger; 20 nodes.
This workflow is great for n8n users who want to prevent duplicate or overlapping workflow runs. If you're a developer, DevOps engineer, or automation enthusiast managing tasks like database updates,
Cleaning Inspection - Submit. Uses redis. Webhook trigger; 7 nodes.
A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without
Reagendamiento. Uses executeWorkflowTrigger, redis, n8n-nodes-evolution-api, dataTable. Event-driven trigger; 73 nodes.