{
  "name": "School 4 · Traffic-source tags in a Telegram bot funnel + weekly report",
  "nodes": [
    {
      "id": "5fcc7e50-c933-4446-aefd-424e182f422a",
      "name": "About",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -520
      ],
      "parameters": {
        "content": "## Traffic-source tags in a Telegram bot funnel\nDemo recipe for the portfolio, not a client project. Works with any bot builder or your own bot.\n\n**RU.** У каждого канала своя ссылка вида t.me/your_bot?start=ads_vk_oct. Метка сохраняется при первом старте и не перезаписывается, шаги воронки отмечаются в той же строке, по понедельникам приходит отчёт по каналам.\n**EN.** Each channel gets its own link such as t.me/your_bot?start=ads_vk_oct. The tag is saved on the first start and never overwritten, funnel steps are stamped on the same row, and a report by channel arrives every Monday.\n\n**Tag format:** `<channel>_<campaign>`, letters, digits, _ and - only, up to 64 characters (Telegram's limit). Channels: ads, reels, tg, partner, blog.\n\n**Setup**\n1. Own bot: connect \"Own bot: /start\" to your bot token. Bot builder: make it POST user_id, username, start_param to /webhook/bot-start on the first message, and user_id + step (lead or paid) to /webhook/bot-step.\n2. Google Sheet columns: user_id, username, tag, channel, campaign, first_seen, last_tag, last_seen, lead_at, paid_at.\n3. \"Pass to amoCRM\" is disabled: enable it to create a deal with UTM fields from the tag.\n\nMax Igoshev, igoshevmax@gmail.com, igoshevmax.com",
        "width": 580,
        "height": 600
      }
    },
    {
      "id": "a6715191-5f62-4160-93d5-56bf754f137f",
      "name": "Own bot: /start",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        -100
      ],
      "webhookId": "f6c55ef3-865e-40c5-bf92-b91139afcfa5",
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      }
    },
    {
      "id": "e6dafaa3-9c8b-4cf6-aba9-18aab3fefa80",
      "name": "Bot builder: start",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        100
      ],
      "webhookId": "6ca61fab-1177-4667-86c1-e21b52f22531",
      "parameters": {
        "httpMethod": "POST",
        "path": "bot-start",
        "responseMode": "onReceived",
        "options": {}
      }
    },
    {
      "id": "2b22ff6f-4ad2-48b9-8361-91cfee6121ed",
      "name": "Read start tag",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "jsCode": "// Two shapes: a Telegram update from your own bot, or a POST from a bot builder.\nconst msg = $json.message;\nlet user_id, username, param;\nif (msg) {\n  const m = /^\\/start(?:\\s+(\\S+))?/.exec(msg.text ?? '');\n  if (!m) return []; // not a /start, nothing to tag\n  user_id = String(msg.from.id); username = msg.from.username ?? ''; param = m[1] ?? '';\n} else {\n  const b = $json.body ?? $json;\n  user_id = String(b.user_id ?? b.chat_id ?? ''); username = b.username ?? ''; param = b.start_param ?? b.tag ?? '';\n  if (!user_id) return [];\n}\nconst tag = /^[A-Za-z0-9_-]{1,64}$/.test(param) ? param : 'direct';\nconst [channel, ...rest] = tag.split('_');\nconst now = new Date().toISOString();\nreturn [{ json: { user_id, username, tag, channel, campaign: rest.join('_'), first_seen: now, last_tag: tag, last_seen: now } }];"
      }
    },
    {
      "id": "5b8237f7-9b9a-4344-9a1e-874986ed5334",
      "name": "Seen before?",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        480,
        0
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "user_id",
              "lookupValue": "={{ $json.user_id }}"
            }
          ]
        },
        "options": {}
      },
      "alwaysOutputData": true
    },
    {
      "id": "b109bf43-bf73-409c-989e-f655bd5a812a",
      "name": "First start?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        720,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "53eb3acb-405d-49f8-a9c3-eb8d7ac3114c",
              "leftValue": "={{ !$json.user_id }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "69d30b44-e37d-4a6c-9d5a-f73a6562a97f",
      "name": "Save first touch",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        960,
        -100
      ],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "user_id": "={{ $('Read start tag').first().json.user_id }}",
            "username": "={{ $('Read start tag').first().json.username }}",
            "tag": "={{ $('Read start tag').first().json.tag }}",
            "channel": "={{ $('Read start tag').first().json.channel }}",
            "campaign": "={{ $('Read start tag').first().json.campaign }}",
            "first_seen": "={{ $('Read start tag').first().json.first_seen }}",
            "last_tag": "={{ $('Read start tag').first().json.tag }}",
            "last_seen": "={{ $('Read start tag').first().json.last_seen }}"
          },
          "matchingColumns": []
        },
        "options": {}
      }
    },
    {
      "id": "8ea8435b-c748-49f5-a492-8f1a28d2b5a2",
      "name": "Repeat start: keep first tag",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        960,
        100
      ],
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "user_id": "={{ $('Read start tag').first().json.user_id }}",
            "last_tag": "={{ $('Read start tag').first().json.tag }}",
            "last_seen": "={{ $('Read start tag').first().json.last_seen }}"
          },
          "matchingColumns": [
            "user_id"
          ]
        },
        "options": {}
      }
    },
    {
      "id": "92da1b7c-dcf3-46a7-bcc6-1136787b2548",
      "name": "Pass to amoCRM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        -100
      ],
      "parameters": {
        "options": {},
        "method": "POST",
        "url": "https://YOUR_SUBDOMAIN.amocrm.ru/api/v4/leads/complex",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify([{ name: 'Бот: ' + ($('Read start tag').first().json.username || $('Read start tag').first().json.user_id), custom_fields_values: [ { field_code: 'UTM_SOURCE', values: [{ value: $('Read start tag').first().json.channel }] }, { field_code: 'UTM_CAMPAIGN', values: [{ value: $('Read start tag').first().json.campaign || $('Read start tag').first().json.tag }] } ], _embedded: { tags: [{ name: 'Бот' }], contacts: [{ first_name: $('Read start tag').first().json.username || 'Telegram ' + $('Read start tag').first().json.user_id }] } }]) }}"
      },
      "disabled": true
    },
    {
      "id": "de808c05-62d9-40bb-91d2-d69dcd657e75",
      "name": "Bot builder: funnel step",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        400
      ],
      "webhookId": "490a2c7f-cbe6-4848-8ed5-56a24dbfae82",
      "parameters": {
        "httpMethod": "POST",
        "path": "bot-step",
        "responseMode": "onReceived",
        "options": {}
      }
    },
    {
      "id": "11a6c960-e18c-42b5-b430-89a544713ac7",
      "name": "Stamp step",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        400
      ],
      "parameters": {
        "jsCode": "const b = $json.body ?? $json;\nconst user_id = String(b.user_id ?? b.chat_id ?? '');\nconst step = String(b.step ?? '');\nif (!user_id || !['lead', 'paid'].includes(step)) return [];\nreturn [{ json: { user_id, [step + '_at']: new Date().toISOString() } }];"
      }
    },
    {
      "id": "647f64e8-0f59-4c8f-bf9f-9d9ef047bc75",
      "name": "Save step",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        480,
        400
      ],
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "user_id"
          ]
        },
        "options": {}
      }
    },
    {
      "id": "f5b43040-b7b3-4c75-a648-7092d887e771",
      "name": "Monday 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        700
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      }
    },
    {
      "id": "e51f5506-9345-4693-b10d-7e3fd0163a46",
      "name": "Read all starts",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        240,
        700
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {}
      }
    },
    {
      "id": "7dcf0ced-ea85-4a85-a762-cf521e85d824",
      "name": "Count by channel",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        700
      ],
      "parameters": {
        "jsCode": "const now = Date.now(), W = 7 * 24 * 3600e3;\nconst inWeek = (v, back = 0) => { if (!v) return false; const t = new Date(v).getTime(); return t > now - W * (back + 1) && t <= now - W * back; };\nconst by = {};\nfor (const { json: r } of $input.all()) {\n  if (!r.user_id) continue;\n  const c = r.channel || 'direct';\n  by[c] ??= { starts: 0, prev: 0, leads: 0, paid: 0 };\n  if (inWeek(r.first_seen)) by[c].starts++;\n  if (inWeek(r.first_seen, 1)) by[c].prev++;\n  if (inWeek(r.lead_at)) by[c].leads++;\n  if (inWeek(r.paid_at)) by[c].paid++;\n}\nconst rows = Object.entries(by).sort((a, b) => b[1].starts - a[1].starts);\nconst lines = rows.map(([c, v]) => c + ': старты ' + v.starts + ' (неделей раньше ' + v.prev + '), заявки ' + v.leads + ', оплаты ' + v.paid);\nreturn [{ json: { text: 'Бот за неделю по каналам\\n' + (lines.join('\\n') || 'Новых стартов нет') } }];"
      }
    },
    {
      "id": "bd948260-fa24-40b0-a531-f03d89b8c920",
      "name": "Weekly report",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        720,
        700
      ],
      "webhookId": "e96c79f7-c5e4-4408-9ccc-520ac20c1ee1",
      "parameters": {
        "chatId": "YOUR_REPORT_CHAT_ID",
        "text": "={{ $json.text }}",
        "additionalFields": {
          "appendAttribution": false
        }
      }
    }
  ],
  "connections": {
    "Own bot: /start": {
      "main": [
        [
          {
            "node": "Read start tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bot builder: start": {
      "main": [
        [
          {
            "node": "Read start tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read start tag": {
      "main": [
        [
          {
            "node": "Seen before?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Seen before?": {
      "main": [
        [
          {
            "node": "First start?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "First start?": {
      "main": [
        [
          {
            "node": "Save first touch",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Repeat start: keep first tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save first touch": {
      "main": [
        [
          {
            "node": "Pass to amoCRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bot builder: funnel step": {
      "main": [
        [
          {
            "node": "Stamp step",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stamp step": {
      "main": [
        [
          {
            "node": "Save step",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Monday 9:00": {
      "main": [
        [
          {
            "node": "Read all starts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read all starts": {
      "main": [
        [
          {
            "node": "Count by channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Count by channel": {
      "main": [
        [
          {
            "node": "Weekly report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": []
}
