{
  "name": "School 5 · Weekly leads and sales by source from amoCRM → Telegram with an AI summary",
  "nodes": [
    {
      "id": "b72cfaa1-0b20-4022-a3ba-8a49a7ccb2ab",
      "name": "About",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -640,
        -480
      ],
      "parameters": {
        "content": "## Weekly leads and sales by source, amoCRM → Telegram with an AI summary\nDemo recipe for the portfolio, not a client project.\n\n**RU.** Каждый понедельник берёт сделки из amoCRM за две недели, считает заявки и успешные сделки по источнику, сравнивает с прошлой неделей и присылает цифры и короткое резюме от Claude в Telegram.\n**EN.** Every Monday it pulls two weeks of amoCRM deals, counts leads and won deals by source, compares with the week before and sends the numbers plus a short Claude summary to Telegram.\n\n**Setup**\n1. Header Auth credential for amoCRM: name `Authorization`, value `Bearer <long-lived token>`. Replace YOUR_SUBDOMAIN in both requests.\n2. Header Auth credential for Anthropic: name `x-api-key`, value your API key.\n3. Connect a Telegram bot and set the chat ID.\nSource = the UTM_SOURCE field of the deal; deals without it count as \"без метки\". Won = status 142.\nThe Claude request opts into server-side fallback (fallbacks: \"default\"): if the model declines, the API retries on a fallback model.\n\nMax Igoshev, igoshevmax@gmail.com, igoshevmax.com",
        "width": 560,
        "height": 560
      }
    },
    {
      "id": "f2bb7639-686a-4097-aef1-914be937aebf",
      "name": "Monday 9:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      }
    },
    {
      "id": "88b2f08a-b331-4ece-bead-75385c13e2b9",
      "name": "Period",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "jsCode": "const now = Math.floor(Date.now() / 1000);\nconst week = 7 * 24 * 3600;\nreturn [{ json: { now, weekStart: now - week, from: now - 2 * week } }];"
      }
    },
    {
      "id": "88c653e7-36c5-4b59-aaa3-4ed48115985e",
      "name": "Deals created in 2 weeks",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        0
      ],
      "parameters": {
        "options": {
          "pagination": {
            "pagination": {
              "paginationMode": "updateAParameterInEachRequest",
              "parameters": {
                "parameters": [
                  {
                    "type": "qs",
                    "name": "page",
                    "value": "={{ $pageCount + 1 }}"
                  }
                ]
              },
              "paginationCompleteWhen": "receiveSpecificStatusCodes",
              "statusCodesWhenComplete": "204",
              "limitPagesFetched": true,
              "maxRequests": 40
            }
          }
        },
        "method": "GET",
        "url": "https://YOUR_SUBDOMAIN.amocrm.ru/api/v4/leads",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "filter[created_at][from]",
              "value": "={{ $json.from }}"
            },
            {
              "name": "limit",
              "value": "250"
            }
          ]
        }
      },
      "alwaysOutputData": true
    },
    {
      "id": "79998810-3c73-4c7f-93f7-f40f0c35bbe6",
      "name": "Deals closed in 2 weeks",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        0
      ],
      "parameters": {
        "options": {
          "pagination": {
            "pagination": {
              "paginationMode": "updateAParameterInEachRequest",
              "parameters": {
                "parameters": [
                  {
                    "type": "qs",
                    "name": "page",
                    "value": "={{ $pageCount + 1 }}"
                  }
                ]
              },
              "paginationCompleteWhen": "receiveSpecificStatusCodes",
              "statusCodesWhenComplete": "204",
              "limitPagesFetched": true,
              "maxRequests": 40
            }
          }
        },
        "method": "GET",
        "url": "https://YOUR_SUBDOMAIN.amocrm.ru/api/v4/leads",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "filter[closed_at][from]",
              "value": "={{ $('Period').first().json.from }}"
            },
            {
              "name": "limit",
              "value": "250"
            }
          ]
        }
      },
      "alwaysOutputData": true,
      "executeOnce": true
    },
    {
      "id": "38737352-e728-488f-b8bf-3d30359d7588",
      "name": "Count by source",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        0
      ],
      "parameters": {
        "jsCode": "const { weekStart, from } = $('Period').first().json;\nconst deals = new Map();\nfor (const node of ['Deals created in 2 weeks', 'Deals closed in 2 weeks'])\n  for (const { json } of $(node).all())\n    for (const d of json?._embedded?.leads ?? []) deals.set(d.id, d);\nconst sourceOf = d => (d.custom_fields_values ?? []).find(f => f.field_code === 'UTM_SOURCE')?.values?.[0]?.value || 'без метки';\nconst by = {};\nconst row = s => (by[s] ??= { leads: 0, leadsPrev: 0, won: 0, wonPrev: 0, wonSum: 0 });\nfor (const d of deals.values()) {\n  const r = row(sourceOf(d));\n  if (d.created_at >= weekStart) r.leads++; else if (d.created_at >= from) r.leadsPrev++;\n  if (d.status_id === 142 && d.closed_at) {\n    if (d.closed_at >= weekStart) { r.won++; r.wonSum += d.price || 0; }\n    else if (d.closed_at >= from) r.wonPrev++;\n  }\n}\nconst table = Object.entries(by).sort((a, b) => b[1].leads - a[1].leads)\n  .map(([s, r]) => ({ source: s, ...r }));\nconst lines = table.map(r => r.source + ': заявки ' + r.leads + ' (было ' + r.leadsPrev + '), продажи ' + r.won + ' (было ' + r.wonPrev + ')');\nreturn [{ json: { table, numbers: lines.join('\\n') || 'Сделок за период нет' } }];"
      }
    },
    {
      "id": "53f2f90f-c373-438e-8cc2-b943c9ae0d65",
      "name": "Claude summary",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1200,
        0
      ],
      "parameters": {
        "options": {},
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "anthropic-beta",
              "value": "server-side-fallback-2026-07-01"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'claude-sonnet-5-5', max_tokens: 2000, output_config: { effort: 'low' }, fallbacks: 'default', messages: [{ role: 'user', content: 'Ты помогаешь руководителю онлайн-школы. Ниже заявки и продажи по источникам за эту неделю и за прошлую. Напиши пять коротких строк простым языком: что выросло, что просело, какой источник приносит заявки без продаж, на что посмотреть на этой неделе. Не придумывай причин, которых нет в цифрах. Данные: ' + JSON.stringify($json.table) }] }) }}"
      },
      "continueOnFail": true
    },
    {
      "id": "7e54e0be-e724-459f-befd-270a97d70c1a",
      "name": "Report to Telegram",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1440,
        0
      ],
      "webhookId": "3a7297b3-c2f7-438b-bf22-2c4129808289",
      "parameters": {
        "chatId": "YOUR_REPORT_CHAT_ID",
        "text": "=Заявки и продажи за неделю по источникам\n{{ $('Count by source').first().json.numbers }}\n\n{{ ($json.content ?? []).find(b => b.type === 'text')?.text ?? 'Резюме не получено, цифры выше.' }}",
        "additionalFields": {
          "appendAttribution": false
        }
      }
    }
  ],
  "connections": {
    "Monday 9:00": {
      "main": [
        [
          {
            "node": "Period",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Period": {
      "main": [
        [
          {
            "node": "Deals created in 2 weeks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deals created in 2 weeks": {
      "main": [
        [
          {
            "node": "Deals closed in 2 weeks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deals closed in 2 weeks": {
      "main": [
        [
          {
            "node": "Count by source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Count by source": {
      "main": [
        [
          {
            "node": "Claude summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude summary": {
      "main": [
        [
          {
            "node": "Report to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": []
}
