{
  "name": "Issue gapless sequential invoice and ticket numbers from a Data Table",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "next-number",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "2b10df80-14da-4f8b-9e2a-9e4f165a9d08",
      "name": "When Number Is Requested",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        576,
        288
      ]
    },
    {
      "parameters": {
        "jsCode": "var items = $input.all();\nvar src = items.length ? items[0].json : {};\nvar body = (src.body && typeof src.body === 'object') ? src.body : {};\nvar query = (src.query && typeof src.query === 'object') ? src.query : {};\nfunction pick(name, fallback){\n  if (body[name] !== undefined && body[name] !== null && body[name] !== '') return body[name];\n  if (query[name] !== undefined && query[name] !== null && query[name] !== '') return query[name];\n  if (src[name] !== undefined && src[name] !== null && src[name] !== '') return src[name];\n  return fallback;\n}\nvar keyRe = new RegExp('^[a-z0-9_-]{1,50}$');\nvar key = String(pick('sequence_key', 'invoice')).trim().toLowerCase();\nif (!keyRe.test(key)) key = 'invoice';\nvar prefixRe = new RegExp('^[A-Za-z0-9_-]{0,12}$');\nvar prefix = String(pick('prefix', '')).trim();\nif (!prefixRe.test(prefix)) prefix = '';\nvar pad = parseInt(pick('pad', 6), 10);\nif (!isFinite(pad) || pad < 1) pad = 6;\nif (pad > 12) pad = 12;\nreturn [{ json: { sequence_key: key, prefix: prefix, pad: pad, prefix_requested: prefix.length > 0 } }];"
      },
      "id": "df0841fe-e127-47a7-bf8b-086e7454a396",
      "name": "Read Sequence Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        816,
        288
      ]
    },
    {
      "parameters": {
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "number_sequences"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "sequence_key",
              "keyValue": "={{ $json.sequence_key }}"
            }
          ]
        },
        "limit": 1
      },
      "id": "61cc150e-7408-4a44-8134-64ac0f78ca77",
      "name": "Get Sequence Row",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1104,
        288
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "var req = $('Read Sequence Request').first().json;\nvar items = $input.all();\nvar row = (items.length && items[0].json && typeof items[0].json === 'object') ? items[0].json : {};\nvar stored = Number(row.current_value);\nif (!isFinite(stored) || stored < 0) stored = 0;\nstored = Math.floor(stored);\nvar next = stored + 1;\nvar prefix = req.prefix_requested ? req.prefix : (typeof row.prefix === 'string' ? row.prefix : '');\nvar digits = String(next);\nwhile (digits.length < req.pad) { digits = '0' + digits; }\nreturn [{ json: {\n  sequence_key: req.sequence_key,\n  current_value: next,\n  previous_value: stored,\n  prefix: prefix,\n  formatted_number: prefix + digits,\n  updated_at: new Date().toISOString()\n} }];"
      },
      "id": "6722f35a-4773-4d6e-a748-c63200cc2adc",
      "name": "Compute Next Number",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1360,
        288
      ]
    },
    {
      "parameters": {
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "number_sequences"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "sequence_key",
              "keyValue": "={{ $json.sequence_key }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "sequence_key"
          ],
          "value": {
            "sequence_key": "={{ $json.sequence_key }}",
            "current_value": "={{ $json.current_value }}",
            "prefix": "={{ $json.prefix }}",
            "updated_at": "={{ $json.updated_at }}"
          },
          "schema": [
            {
              "id": "sequence_key",
              "displayName": "sequence_key",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "current_value",
              "displayName": "current_value",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            },
            {
              "id": "prefix",
              "displayName": "prefix",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "updated_at",
              "displayName": "updated_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "d84bfebd-73fb-4609-b676-47b50a6a701a",
      "name": "Save Sequence Counter",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1664,
        288
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { \"sequence_key\": $(\"Compute Next Number\").item.json.sequence_key, \"number\": $(\"Compute Next Number\").item.json.formatted_number, \"current_value\": $(\"Compute Next Number\").item.json.current_value, \"issued_at\": $(\"Compute Next Number\").item.json.updated_at } }}",
        "options": {}
      },
      "id": "f47277b9-959d-4826-b215-e7b8cbfdcb27",
      "name": "Return Issued Number",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        1920,
        288
      ]
    },
    {
      "parameters": {
        "content": "## Issue gapless sequential invoice and ticket numbers from a Data Table\n\n### How it works\n1. A `POST` webhook at `next-number` accepts a `sequence_key`, plus an optional `prefix` and zero-pad width `pad`.\n2. A Code node validates the request and falls back to safe defaults, so a malformed call still returns a usable number.\n3. The `number_sequences` Data Table is read for that key, then a Code node adds one to `current_value`, starting at 1 when the key has never been used.\n4. The counter is written back with an upsert on `sequence_key`, and Respond to Webhook returns the formatted number, for example `INV-000042`.\n\n### Setup steps\n- [ ] Create a Data Table named `number_sequences` with the columns `sequence_key`, `current_value`, `prefix`, and `updated_at`.\n- [ ] Confirm that selection on both `Get Sequence Row` and `Save Sequence Counter`.\n- [ ] Activate the workflow, then POST `{\"sequence_key\": \"invoice\"}` to the production URL.\n- [ ] Set this workflow to a single concurrent execution before you rely on the sequence being unbroken.\n\n### Customization\nData Tables give no atomic read-modify-write, so the no-gaps promise is best effort: two genuinely simultaneous calls can read the same `current_value`. Serialise your callers if you need a hard guarantee. Change the default pad width in `Compute Next Number`, or turn on header auth on the webhook to keep the issuer private.",
        "height": 724,
        "width": 692
      },
      "id": "8d6cf6d9-8d5a-4ed9-ad5a-7b7496c271fe",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -256,
        -80
      ]
    },
    {
      "parameters": {
        "content": "## Read and validate the request",
        "height": 312,
        "width": 480,
        "color": 7
      },
      "id": "034288b5-f2fe-413a-b89f-bb0fa8e2d891",
      "name": "Section Read Request",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        512,
        176
      ]
    },
    {
      "parameters": {
        "content": "## Look up and increment\n\nBest effort, not a lock: two genuinely simultaneous calls can read the same `current_value`. Serialise callers for a hard guarantee.",
        "height": 376,
        "width": 496,
        "color": 7
      },
      "id": "979df71f-c2d1-424a-9d7c-882518bf63ee",
      "name": "Section Look Up Increment",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1040,
        96
      ]
    },
    {
      "parameters": {
        "content": "## Save the counter and respond",
        "height": 376,
        "width": 544,
        "color": 7
      },
      "id": "fe2bb9c7-aa5b-409e-bfe8-1968b2c72255",
      "name": "Section Save And Respond",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1584,
        96
      ]
    }
  ],
  "connections": {
    "When Number Is Requested": {
      "main": [
        [
          {
            "node": "Read Sequence Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Sequence Request": {
      "main": [
        [
          {
            "node": "Get Sequence Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Sequence Row": {
      "main": [
        [
          {
            "node": "Compute Next Number",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Next Number": {
      "main": [
        [
          {
            "node": "Save Sequence Counter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Sequence Counter": {
      "main": [
        [
          {
            "node": "Return Issued Number",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}