This workflow corresponds to n8n.io template #13877 — we link there as the canonical source.
This workflow follows the Form → Form Trigger recipe pattern — see all workflows that pair these two integrations.
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 →
{
"id": "rz2O0VauVNSjO5tw",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Send automated reminders to multiple clients via Telegram using webhook and schedule triggers",
"tags": [],
"nodes": [
{
"id": "9006229a-197a-46fd-b5b2-a1ad1abccd52",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
-16
],
"parameters": {
"color": 3,
"width": 584,
"height": 884,
"content": "## Multi-tenant reminder system\n\n## How it works\nThis workflow has **3 entry points**:\n\n**1. Webhook trigger** \u2014 an external system sends an event (appointment, deadline, order) and the reminder fires immediately based on the tenant's configured rules.\n\n**2. Schedule trigger** \u2014 runs every minute, queries events approaching their deadline window, and sends reminders automatically. Uses idempotency to ensure the same reminder is never sent twice.\n\n**3. Registration form** \u2014 a built-in n8n form to register new tenants with their channel config and message template. No external backend needed.\n\nAfter the template is rendered with the event variables, the workflow routes to the correct channel (currently Telegram, easily extendable to WhatsApp, email, etc.). Every send \u2014 success or error \u2014 is logged to the database.\n\n## Setup steps\n1. Add your **PostgreSQL credentials** to all Postgres nodes (~2 min)\n2. Add your **Telegram credentials** to the Send Message node (~2 min)\n3. Create the required tables using the SQL schema in the sticky note below (~10 min)\n4. Register your first tenant at `/form/multi-tenant-register`\n5. Send events via `POST /webhook/multi-tenant-webhook` with `x-tenant-token` header"
},
"typeVersion": 1
},
{
"id": "4d459efd-cd19-45a7-a3ef-2aab53c3850a",
"name": "Database Schema",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
-16
],
"parameters": {
"width": 552,
"height": 884,
"content": "## Required database schema\n\n```sql\nCREATE TABLE tenants (\n id SERIAL PRIMARY KEY,\n name VARCHAR(255) NOT NULL,\n channel VARCHAR(50) NOT NULL,\n channel_config JSONB NOT NULL,\n api_token VARCHAR(255),\n active BOOLEAN DEFAULT true,\n created_at TIMESTAMP DEFAULT NOW()\n);\n\nCREATE TABLE tenant_rules (\n id SERIAL PRIMARY KEY,\n tenant_id INTEGER REFERENCES tenants(id),\n trigger_type VARCHAR(20) NOT NULL,\n timing_minutes_before INTEGER,\n message_template TEXT NOT NULL,\n active BOOLEAN DEFAULT true\n);\n\nCREATE TABLE tenant_events (\n id SERIAL PRIMARY KEY,\n tenant_id INTEGER REFERENCES tenants(id),\n entity_name VARCHAR(255),\n entity_contact VARCHAR(255),\n event_datetime TIMESTAMP NOT NULL,\n extra_data JSONB,\n status VARCHAR(20) DEFAULT 'pending'\n);\n\nCREATE TABLE tenant_reminders_sent (\n id SERIAL PRIMARY KEY,\n tenant_id INTEGER,\n rule_id INTEGER,\n event_id INTEGER,\n sent_at TIMESTAMP DEFAULT NOW()\n);\n\nCREATE TABLE tenant_logs (\n id SERIAL PRIMARY KEY,\n tenant_id INTEGER,\n rule_id INTEGER,\n event_id INTEGER,\n status VARCHAR(20),\n message_sent TEXT,\n error_message TEXT,\n created_at TIMESTAMP DEFAULT NOW()\n);\n```"
},
"typeVersion": 1
},
{
"id": "67d9c4dc-0d45-4a3a-845a-8054ae568403",
"name": "Webhook flow group",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
880
],
"parameters": {
"color": 7,
"width": 1148,
"height": 524,
"content": "## Webhook flow\nReceives events from external systems.\nValidates the tenant token via `x-tenant-token` header,\nfetches tenant config from the database,\nand fires the reminder immediately."
},
"typeVersion": 1
},
{
"id": "7b2b0ff0-bcd0-4ce0-a5c7-3b982299f275",
"name": "Schedule flow group",
"type": "n8n-nodes-base.stickyNote",
"position": [
-224,
880
],
"parameters": {
"color": 7,
"width": 212,
"height": 260,
"content": "## Schedule flow\nRuns every minute.\nQueries events within the configured timing window per tenant.\nIdempotency via `tenant_reminders_sent` \u2014 same reminder is never sent twice."
},
"typeVersion": 1
},
{
"id": "96a8d879-807a-48ec-8fe1-416b9d208cac",
"name": "Registration group",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
1424
],
"parameters": {
"color": 7,
"width": 1252,
"height": 312,
"content": "## Tenant registration\nBuilt-in n8n form to register new tenants.\nNo external backend needed.\nAccess at `/form/multi-tenant-register`"
},
"typeVersion": 1
},
{
"id": "48a46eb1-67d8-4d98-8cf0-16840e4d4a07",
"name": "Send and log group",
"type": "n8n-nodes-base.stickyNote",
"position": [
1152,
800
],
"parameters": {
"color": 7,
"width": 760,
"height": 604,
"content": "## Send and log\nRoutes to the correct channel based on tenant config.\nLogs every attempt \u2014 success and error \u2014 to the database.\n\nTo add a new channel: duplicate the IF node\nand add the corresponding send node."
},
"typeVersion": 1
},
{
"id": "e977f872-9060-4743-b7c1-0274d58bf25f",
"name": "Every minute - check due events",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
544,
1232
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes"
}
]
}
},
"typeVersion": 1.3
},
{
"id": "81af22b5-b58e-4562-9208-c68844895011",
"name": "Log success to database",
"type": "n8n-nodes-base.postgres",
"position": [
1664,
1024
],
"parameters": {
"query": "INSERT INTO tenant_logs (tenant_id, rule_id, event_id, status, message_sent)\nVALUES (\n {{ $('Render Template').item.json.tenants_id }},\n {{ $('Render Template').item.json.rule_id }},\n {{ $('Render Template').item.json.event_id || null }},\n 'success',\n '{{ $('Render Template').item.json.message }}'\n);",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "4fa680a0-ec02-44a9-93ef-963a95de85f0",
"name": "Mark reminder as sent",
"type": "n8n-nodes-base.postgres",
"position": [
1664,
832
],
"parameters": {
"query": "INSERT INTO tenant_reminders_sent (tenant_id, rule_id, event_id)\nVALUES (\n {{ $('Render Template').item.json.tenants_id }},\n {{ $('Render Template').item.json.rule_id }},\n {{ $('Render Template').item.json.event_id || null }}\n);",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "a442acce-3f97-4e0d-bf8a-2e3b6de81ce3",
"name": "Render Template",
"type": "n8n-nodes-base.code",
"position": [
992,
1136
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\n\nconst eventDate = new Date(item.event_datetime);\nconst event_time = eventDate.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n timeZone: 'America/Sao_Paulo'\n});\n\nconst event_date = eventDate.toLocaleDateString('en-US', {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n timeZone: 'America/Sao_Paulo'\n});\n\nconst variables = {\n entity_name: item.entity_name,\n entity_contact: item.entity_contact,\n tenants_id: item.tenants_id,\n event_time,\n event_date,\n ...item.extra_data\n};\n\nlet message = item.message_template;\nfor (const [key, value] of Object.entries(variables)) {\n message = message.replaceAll(`{{${key}}}`, value);\n}\n\nreturn {\n event_id: item.event_id,\n rule_id: item.rule_id,\n tenants_id: item.tenants_id,\n channel: item.channel,\n channel_config: item.channel_config,\n message\n};"
},
"typeVersion": 2
},
{
"id": "1703a6ab-1e8e-493d-919f-4d2c85f42be0",
"name": "Log error to database",
"type": "n8n-nodes-base.postgres",
"position": [
1664,
1232
],
"parameters": {
"query": "INSERT INTO tenant_logs (tenant_id, rule_id, event_id, status, error_message)\nVALUES (\n {{ $json.tenants_id }},\n {{ $json.rule_id }},\n {{ $json.event_id }},\n 'error',\n '{{ $json.error }}'\n);",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "c8ef902b-34d9-4cf6-98fb-d7209efb25a9",
"name": "Send reminder via Telegram",
"type": "n8n-nodes-base.telegram",
"onError": "continueErrorOutput",
"position": [
1440,
1024
],
"parameters": {
"text": "={{ $json.message }}",
"chatId": "={{ $json.channel_config.chat_id }}",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"retryOnFail": false,
"typeVersion": 1.2
},
{
"id": "a961253c-5768-4223-bc3f-3a28bd902e67",
"name": "Receive event via webhook",
"type": "n8n-nodes-base.webhook",
"position": [
96,
1024
],
"parameters": {
"path": "multi-tenant-webhook",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "6f926efe-a55f-463d-84cb-002d563c06c7",
"name": "Validate tenant token",
"type": "n8n-nodes-base.code",
"position": [
320,
1024
],
"parameters": {
"jsCode": "const token = $input.item.json.headers['x-tenant-token'];\n\nif (!token) {\n throw new Error('Missing x-tenant-token header');\n}\n\nreturn {\n token,\n entity_name: $input.item.json.body.entity_name,\n event_datetime: $input.item.json.body.event_datetime,\n extra_data: $input.item.json.body.extra_data || {}\n};"
},
"typeVersion": 2
},
{
"id": "ee1e6760-a1d1-4dd6-9b3d-27be8ed30ab4",
"name": "Map webhook fields",
"type": "n8n-nodes-base.set",
"position": [
768,
1024
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e3a86613-47f7-4b30-9275-53f2549ede8c",
"name": "tenants_id",
"type": "number",
"value": "={{ $json.tenant_id }}"
},
{
"id": "b81a3869-165e-4e41-a4d0-0aa7993533a2",
"name": "rule_id",
"type": "number",
"value": "={{ $json.rule_id }}"
},
{
"id": "2acf59fa-7fbf-4124-ba75-0e76f0e97d8c",
"name": "channel",
"type": "string",
"value": "={{ $json.channel }}"
},
{
"id": "c51302f8-03a2-423d-8cc6-f55274e78d5f",
"name": "chat_id",
"type": "string",
"value": "={{ $json.channel_config.chat_id }}"
},
{
"id": "60390c8b-732a-4701-b028-dec8c7c09ac2",
"name": "message_template",
"type": "string",
"value": "={{ $json.message_template }}"
},
{
"id": "46312d62-5af1-46d5-a742-f5b50a66742b",
"name": "entity_name",
"type": "string",
"value": "={{ $('Validate tenant token').item.json.entity_name }}"
},
{
"id": "a9280f5a-d4d9-4b61-8a53-07917f51660a",
"name": "event_datetime",
"type": "string",
"value": "={{ $('Validate tenant token').item.json.event_datetime }}"
},
{
"id": "f98d1dfb-aced-495f-8b2d-41b8b052f61f",
"name": "extra_data",
"type": "object",
"value": "={{ $('Validate tenant token').item.json.extra_data }}"
},
{
"id": "f6fb5026-5493-49f4-84af-83d270cbc344",
"name": "channel_config",
"type": "object",
"value": "={{ $json.channel_config }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "bd57f3b8-642a-4e5f-9bc4-6d12ab86f06f",
"name": "Tenant registration form",
"type": "n8n-nodes-base.formTrigger",
"position": [
80,
1552
],
"parameters": {
"options": {},
"formTitle": "Register New Tenant",
"formFields": {
"values": [
{
"fieldLabel": "Client name",
"requiredField": true
},
{
"fieldType": "dropdown",
"fieldLabel": "Channel",
"fieldOptions": {
"values": [
{
"option": "telegram"
},
{
"option": "whatsapp"
},
{
"option": "email"
}
]
}
},
{
"fieldLabel": "Chat ID / Contact",
"requiredField": true
},
{
"fieldLabel": "API Token"
},
{
"fieldType": "dropdown",
"fieldLabel": "Trigger type",
"fieldOptions": {
"values": [
{
"option": "schedule"
},
{
"option": "webhook"
}
]
}
},
{
"fieldType": "number",
"fieldLabel": "Minutes before event"
},
{
"fieldType": "textarea",
"fieldLabel": "Message template",
"requiredField": true
}
]
},
"responseMode": "lastNode"
},
"typeVersion": 2.3
},
{
"id": "aea01fd8-2a93-4951-aaba-021588b14ef4",
"name": "Organize form data",
"type": "n8n-nodes-base.code",
"position": [
304,
1552
],
"parameters": {
"jsCode": "const item = $input.item.json;\n\nconst channelConfig = JSON.stringify({\n chat_id: item['Chat ID / Contact']\n});\n\nreturn {\n name: item['Client name'],\n channel: item['Channel'],\n channel_config: channelConfig,\n api_token: item['API Token'],\n trigger_type: item['Trigger type'],\n timing_minutes_before: item['Minutes before event'] || null,\n message_template: item['Message template']\n};"
},
"typeVersion": 2
},
{
"id": "5b53a74a-858f-47fe-978e-684ebc260dbc",
"name": "Show registration success",
"type": "n8n-nodes-base.form",
"position": [
976,
1552
],
"parameters": {
"options": {},
"operation": "completion",
"completionTitle": "=Tenant registered! ID: {{ $('Insert tenant').item.json.id }}"
},
"typeVersion": 2.3
},
{
"id": "4aeb37bb-2fc3-4569-8393-b7af7d542af3",
"name": "Fetch events due for reminder",
"type": "n8n-nodes-base.postgres",
"position": [
768,
1232
],
"parameters": {
"query": "SELECT \n e.id as event_id,\n e.entity_name,\n e.entity_contact,\n e.event_datetime,\n e.extra_data,\n r.id as rule_id,\n r.message_template,\n r.timing_minutes_before,\n t.channel,\n t.channel_config,\n t.id as tenants_id\nFROM tenant_rules r\nJOIN tenants t ON t.id = r.tenant_id\nJOIN tenant_events e ON e.tenant_id = r.tenant_id\nWHERE r.trigger_type = 'schedule'\n AND r.active = true\n AND t.active = true\n AND e.status = 'pending'\n AND e.event_datetime BETWEEN NOW() + (r.timing_minutes_before * INTERVAL '1 minute') - INTERVAL '5 minutes'\n AND NOW() + (r.timing_minutes_before * INTERVAL '1 minute') + INTERVAL '5 minutes'\n AND NOT EXISTS (\n SELECT 1 FROM tenant_reminders_sent rs\n WHERE rs.rule_id = r.id AND rs.event_id = e.id\n )",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "7dcfdf57-bc38-47e3-aa19-95100c9e9426",
"name": "Fetch tenant config",
"type": "n8n-nodes-base.postgres",
"position": [
544,
1024
],
"parameters": {
"query": "SELECT \n t.id as tenant_id,\n t.name,\n t.channel,\n t.channel_config,\n r.id as rule_id,\n r.message_template\nFROM tenants t\nJOIN tenant_rules r ON r.tenant_id = t.id\nWHERE t.api_token = '{{ $json.token }}'\n AND r.trigger_type = 'webhook'\n AND r.active = true\n AND t.active = true",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "c45c91c4-585d-45b4-82c2-0fd47b52b283",
"name": "Insert tenant rule",
"type": "n8n-nodes-base.postgres",
"position": [
752,
1552
],
"parameters": {
"query": "INSERT INTO tenant_rules (tenant_id, trigger_type, timing_minutes_before, message_template, active)\nVALUES (\n {{ $json.id }},\n '{{ $('Organize form data').item.json.trigger_type }}',\n {{ $('Organize form data').item.json.timing_minutes_before || 'NULL' }},\n '{{ $('Organize form data').item.json.message_template }}',\n true\n);",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "f1215f58-6250-43df-8188-207747655186",
"name": "Insert tenant",
"type": "n8n-nodes-base.postgres",
"position": [
528,
1552
],
"parameters": {
"query": "INSERT INTO tenants (name, channel, channel_config, api_token, active)\nVALUES (\n '{{ $json.name }}',\n '{{ $json.channel }}',\n '{{ $json.channel_config }}',\n '{{ $json.api_token }}',\n true\n)\nRETURNING id;",
"options": {},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "d9402bd4-ea40-451a-a324-0d69abec2c09",
"name": "No events due - skip",
"type": "n8n-nodes-base.noOp",
"position": [
1440,
1232
],
"parameters": {},
"typeVersion": 1
},
{
"id": "093748d1-c55f-4e6c-9be6-b767f6da243b",
"name": "Route by channel type",
"type": "n8n-nodes-base.if",
"position": [
1216,
1136
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "822236b6-3165-4d71-ae54-2797e1e6a226",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.channel }}",
"rightValue": "telegram"
}
]
}
},
"typeVersion": 2.2
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "96bcd4d0-658a-4236-8d66-4e7e9af1a401",
"connections": {
"Insert tenant": {
"main": [
[
{
"node": "Insert tenant rule",
"type": "main",
"index": 0
}
]
]
},
"Render Template": {
"main": [
[
{
"node": "Route by channel type",
"type": "main",
"index": 0
}
]
]
},
"Insert tenant rule": {
"main": [
[
{
"node": "Show registration success",
"type": "main",
"index": 0
}
]
]
},
"Map webhook fields": {
"main": [
[
{
"node": "Render Template",
"type": "main",
"index": 0
}
]
]
},
"Organize form data": {
"main": [
[
{
"node": "Insert tenant",
"type": "main",
"index": 0
}
]
]
},
"Fetch tenant config": {
"main": [
[
{
"node": "Map webhook fields",
"type": "main",
"index": 0
}
]
]
},
"Route by channel type": {
"main": [
[
{
"node": "Send reminder via Telegram",
"type": "main",
"index": 0
}
],
[
{
"node": "No events due - skip",
"type": "main",
"index": 0
}
]
]
},
"Validate tenant token": {
"main": [
[
{
"node": "Fetch tenant config",
"type": "main",
"index": 0
}
]
]
},
"Tenant registration form": {
"main": [
[
{
"node": "Organize form data",
"type": "main",
"index": 0
}
]
]
},
"Receive event via webhook": {
"main": [
[
{
"node": "Validate tenant token",
"type": "main",
"index": 0
}
]
]
},
"Send reminder via Telegram": {
"main": [
[
{
"node": "Mark reminder as sent",
"type": "main",
"index": 0
},
{
"node": "Log success to database",
"type": "main",
"index": 0
}
],
[
{
"node": "Log error to database",
"type": "main",
"index": 0
}
]
]
},
"Fetch events due for reminder": {
"main": [
[
{
"node": "Render Template",
"type": "main",
"index": 0
}
]
]
},
"Every minute - check due events": {
"main": [
[
{
"node": "Fetch events due for reminder",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
postgrestelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Any external system triggers a reminder via webhook with a tenant token — the workflow validates the token, fetches the tenant's channel config and message template from PostgreSQL, renders the message with event variables, and sends it immediately A schedule trigger runs every…
Source: https://n8n.io/workflows/13877/ — 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.
This n8n workflow automatically monitors GeekHack forum RSS feeds every hour for new keyboard posts in Interest Checks and Group Buys sections. When it finds a new thread (not replies), it: Monitors R
[CloudFly] Import Workflows, Credentials. Uses formTrigger, executeCommand, telegram, readWriteFile. Event-driven trigger; 18 nodes.
ETHERSCAN. Uses httpRequest, postgres, telegram. Scheduled trigger; 10 nodes.
DevRel Anomaly Detection (anomaly-detection). Uses postgres, telegram. Scheduled trigger; 5 nodes.
This workflow automatically pulls daily signup stats from your PostgreSQL database and shares them with your team across multiple channels. Every morning, it counts the number of new signups in the la