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": "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": []
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Issue gapless sequential invoice and ticket numbers from a Data Table. Uses dataTable. Webhook trigger; 10 nodes.
Source: https://github.com/exekyute/n8n-exekyute-templates/blob/main/published/n8n-gapless-number-issuer/workflow.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.
AP Invoice — 01 Orchestrator. Uses postgres, httpRequest, quickbooks, slack. Webhook trigger; 42 nodes.
This workflow automates the entire process of receiving a product/service order, checking or creating a customer in QuickBooks Online (QBO), generating an invoice, and emailing it — all triggered by a
Tired of the standard, boring invoices from QuickBooks Online? This workflow completely automates the process of creating beautiful, custom-branded PDF invoices and emailing them directly to your clie
Automated QuickBooks Invoice to Custom PDF & Email
This workflow automates the full invoicing and payment process using n8n and Xero. It allows businesses to generate invoices, track payments, send WhatsApp notifications, and keep records synced autom