AutomationFlowsGeneral › Addendo — Cost Guard V1

Addendo — Cost Guard V1

Addendo — Cost Guard v1. Uses redis. Webhook trigger; 15 nodes.

Webhook trigger★★★★☆ complexity15 nodesRedis
General Trigger: Webhook Nodes: 15 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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.

Pro

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 →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Statemachine. Uses redis, redisTrigger. Webhook trigger; 20 nodes.

Redis, Redis Trigger
General

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,

Redis
General

Cleaning Inspection - Submit. Uses redis. Webhook trigger; 7 nodes.

Redis
General

A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without

Crypto, Data Table, Execute Workflow Trigger
General

Reagendamiento. Uses executeWorkflowTrigger, redis, n8n-nodes-evolution-api, dataTable. Event-driven trigger; 73 nodes.

Execute Workflow Trigger, Redis, N8N Nodes Evolution Api +2