AutomationFlowsWeb Scraping › Score Instagram Hashtag Engagement with Apify, Telegram, and Airtable

Score Instagram Hashtag Engagement with Apify, Telegram, and Airtable

ByEmel @emelr on n8n.io

This workflow runs daily to fetch Instagram hashtag posts via the Apify API, computes a weighted engagement score, then sends a top-10 HTML report to Telegram and upserts the scored posts into Airtable. Runs on a 24-hour schedule. Loads a predefined list of hashtags to monitor…

Cron / scheduled trigger★★★★☆ complexity14 nodesHTTP RequestTelegramAirtable
Web Scraping Trigger: Cron / scheduled Nodes: 14 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16287 — we link there as the canonical source.

This workflow follows the Airtable → HTTP Request 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 →

Download .json
{
  "id": "0cz5L9CiI3gZ7XOg",
  "name": "Discover Instagram posts by hashtag with engagement scoring",
  "tags": [],
  "nodes": [
    {
      "id": "a942a075-e818-47e2-95a2-b765943683d0",
      "name": "Daily check for trending posts",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1536,
        96
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a7dba99a-7510-41a6-ad6a-6de897d103a7",
      "name": "Load monitoring hashtags",
      "type": "n8n-nodes-base.code",
      "position": [
        -1312,
        96
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 CONFIGURE YOUR HASHTAGS HERE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst HASHTAGS = [ \n  'retrogames', \n  'ps2games'\n];\n\nconst MAX_POSTS_PER_TAG = 10; // how many posts to fetch per hashtag\n\nreturn HASHTAGS.map(tag => ({\n  json: { hashtag: tag, maxPosts: MAX_POSTS_PER_TAG }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "aa5ea33e-fbe8-42f5-a280-b563b32607df",
      "name": "Scrape Instagram posts via Apify API",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "NOTE: Instagram's private API blocks unauthenticated requests.\nFor production use, replace this node with:\n  \u2022 Apify Instagram Hashtag Scraper actor\n  \u2022 RapidAPI Instagram Scraper\n  \u2022 Your own authenticated session cookies\nSee README comment in 'Parse Posts' node for cookie auth approach.",
      "position": [
        -1088,
        96
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/acts/apify~instagram-hashtag-scraper/run-sync-get-dataset-items?token={{ env.APIFY_TOKEN }}",
        "options": {
          "timeout": 15000
        },
        "jsonBody": "={\n  \"hashtags\": [\"{{ $json.hashtag }}\"],\n  \"resultsLimit\": 10,\n  \"proxy\": {\n    \"useApifyProxy\": true,\n    \"apifyProxyGroups\": [\"RESIDENTIAL\"]\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "da04c938-17fb-408b-909b-749b9e8a41e4",
      "name": "Extract and normalize post data",
      "type": "n8n-nodes-base.code",
      "position": [
        -816,
        96
      ],
      "parameters": {
        "jsCode": "// 1. Get ALL incoming items that fetched from the API\nconst allInputItems = $input.all();\nconst hashtag = $('Set Hashtags').item.json.hashtag; // Fallback fallback tag tracking\n\nlet allParsedPosts = [];\n\n// 2. Loop through each API result item safely\nfor (const item of allInputItems) {\n  const raw = item.json;\n  \n  let posts = [];\n  if (Array.isArray(raw)) {\n    posts = raw;\n  } else if (Array.isArray(raw?.items)) {\n    posts = raw.items;\n  } else if (Array.isArray(raw?.data)) {\n    posts = raw.data;\n  } else if (Array.isArray(raw?.results)) {\n    posts = raw.results;\n  } else if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n    posts = [raw];\n  }\n\n  if (posts.length === 0) {\n    continue;\n  }\n\n  // 3. Map individual entries to your formatting\n  const mapped = posts.map(post => ({\n    json: {\n      id:        post.id || post.shortCode || post.pk || '',\n      shortcode: post.shortCode || post.id || '',\n      url:       post.url || `https://www.instagram.com/p/${post.shortCode || post.id}/`,\n      thumbnail: post.displayUrl || post.thumbnailSrc || '',\n      caption:   (post.caption || post.accessibility_caption || '').slice(0, 200),\n      likes:     Number(post.likesCount || 0),\n      comments:  Number(post.commentsCount || 0),\n      saves:     Number(post.savesCount || 0),\n      reposts:   Number(post.videoViewCount || post.videoPlayCount || 0),\n      timestamp: post.timestamp || '',\n      author:    post.ownerUsername || post.ownerFullName || 'unknown',\n      hashtag:   hashtag, // Maps back to the workflow contextual tag\n      type:      post.type || post.productType || 'image'\n    }\n  }));\n\n  allParsedPosts.push(...mapped);\n}\n\nif (allParsedPosts.length === 0) {\n  return [{ json: { _empty: true, hashtag, _debug_raw: \"No items parsed from any run.\" } }];\n}\n\nreturn allParsedPosts;"
      },
      "typeVersion": 2
    },
    {
      "id": "e0c46425-fd04-408f-a967-8878eb8c08e8",
      "name": "Calculate engagement score and rank",
      "type": "n8n-nodes-base.code",
      "position": [
        -368,
        96
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500 ENGAGEMENT SCORE FORMULA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Weights reflect content signal value:\n//   reposts  = 4  (strongest signal \u2014 someone shares it)\n//   comments = 3  (high intent \u2014 someone typed)\n//   saves    = 2  (bookmarks = future intent)\n//   likes    = 1  (lowest friction)\n\nconst W = { reposts: 4, comments: 3, saves: 2, likes: 1 };\n\nconst items = $input.all();\n\n// Deduplicate by post ID (across hashtags)\nconst seen = new Set();\nconst unique = items.filter(item => {\n  const id = item.json.id || item.json.shortcode;\n  if (!id || seen.has(id)) return false;\n  seen.add(id);\n  return !item.json._empty;\n});\n\n// Score each post\nconst scored = unique.map(item => {\n  const p = item.json;\n  const score = Math.round(\n    (p.likes    || 0) * W.likes    +\n    (p.comments || 0) * W.comments +\n    (p.saves    || 0) * W.saves    +\n    (p.reposts  || 0) * W.reposts\n  );\n\n  // Normalise timestamp\n  let postedAt = '';\n  if (p.timestamp) {\n    const ts = typeof p.timestamp === 'number' ? p.timestamp * 1000 : new Date(p.timestamp).getTime();\n    postedAt = new Date(ts).toISOString();\n  }\n\n  return {\n    json: {\n      ...p,\n      score,\n      postedAt,\n      scannedAt: new Date().toISOString()\n    }\n  };\n});\n\n// Sort descending by score\nscored.sort((a, b) => b.json.score - a.json.score);\n\n// Return top 10\nreturn scored.slice(0, 10);"
      },
      "typeVersion": 2
    },
    {
      "id": "8eab1012-81a5-412f-9c87-834e30df2b4e",
      "name": "Format HTML report for Telegram",
      "type": "n8n-nodes-base.code",
      "position": [
        0,
        0
      ],
      "parameters": {
        "jsCode": "// Build a compact HTML report for Telegram (parse_mode: HTML)\nconst posts = $input.all();\nconst now = new Date().toUTCString();\n\nconst hashtags = [...new Set(posts.map(p => p.json.hashtag))]\n  .map(h => `#${h}`).join(' ');\n\nconst rows = posts.map((item, i) => {\n  const p = item.json;\n  const medal = ['\ud83e\udd47','\ud83e\udd48','\ud83e\udd49'][i] || `${i+1}.`;\n  const caption = p.caption ? p.caption.replace(/</g,'&lt;').replace(/>/g,'&gt;').slice(0,80) + '\u2026' : '(no caption)';\n  const postedDate = p.postedAt ? new Date(p.postedAt).toLocaleDateString('en-GB', { day:'numeric', month:'short' }) : '\u2014';\n\n  return (\n    `${medal} <a href=\"${p.url}\">@${p.author}</a>  <b>Score: ${p.score.toLocaleString()}</b>\\n` +\n    `   \ud83d\udc4d ${(p.likes||0).toLocaleString()}  \ud83d\udcac ${(p.comments||0).toLocaleString()}  \ud83d\udd16 ${(p.saves||0).toLocaleString()}  \ud83d\udd01 ${(p.reposts||0).toLocaleString()}\\n` +\n    `   \ud83d\udcc5 ${postedDate}  |  ${p.type || 'post'}\\n` +\n    `   <i>${caption}</i>\\n`\n  );\n}).join('\\n');\n\nconst message =\n  `<b>\ud83d\udcca Instagram Engagement Report</b>\\n` +\n  `${hashtags}\\n` +\n  `<i>Generated: ${now}</i>\\n\\n` +\n  rows +\n  `\\n<i>Top 10 by weighted score (reposts\u00d74 \u00b7 comments\u00d73 \u00b7 saves\u00d72 \u00b7 likes\u00d71)</i>`;\n\nreturn [{ json: { message } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "21593d8a-f9bc-48b3-90da-4dec77a5f0f4",
      "name": "Send engagement report to Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        208,
        0
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "={{ env.TELEGRAM_CHAT_ID }}",
        "additionalFields": {
          "parse_mode": "HTML",
          "disable_web_page_preview": false
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "020248c9-ec99-4bd7-9cd7-648c5798c61a",
      "name": "Save results to Airtable database",
      "type": "n8n-nodes-base.airtable",
      "position": [
        0,
        192
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ env.AIRTABLE_BASE_ID }}"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "={{ env.AIRTABLE_TABLE_ID }}",
          "cachedResultName": "Table 1"
        },
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "url",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "author",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "author",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "hashtag",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "hashtag",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "caption",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "caption",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "likes",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "likes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "comments",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "comments",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "saves",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "saves",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reposts",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "reposts",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "postedAt",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "postedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "scannedAt",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "scannedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "shortcode",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "shortcode",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "thumbnail",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "thumbnail",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "timestamp",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "type",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "upsert"
      },
      "typeVersion": 2.1
    },
    {
      "id": "8b092c47-9a1c-4c28-9c24-8914f07144bf",
      "name": "Remove empty API responses",
      "type": "n8n-nodes-base.filter",
      "position": [
        -592,
        96
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "no-empty",
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json._empty }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "132178f0-4eb2-425e-8423-0ff1cd5ec49a",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2096,
        -144
      ],
      "parameters": {
        "width": 432,
        "height": 736,
        "content": "## Discover Instagram posts by hashtag with engagement scoring\n\n### How it works\n\n1. Triggers a daily schedule to check for trending Instagram posts by hashtag.\n2. Loads the configured hashtags to monitor.\n3. Scrapes Instagram posts using the Apify API.\n4. Extracts and processes the post data, removing empty responses.\n5. Calculates engagement scores, then sends a report to Telegram and saves data to Airtable.\n\n### Setup steps\n\n- [ ] Configure the daily schedule trigger to set the desired time for checking posts.\n- [ ] Set hashtags in the 'Load monitoring hashtags' node.\n- [ ] Ensure Apify API credentials are configured in the 'Scrape Instagram posts via Apify API' node.\n- [ ] Set up Telegram credentials in the 'Send engagement report to Telegram' node.\n- [ ] Set Airtable credentials in the 'Save results to Airtable database' node.\n\n### Customization\n\nYou can adjust the engagement score formula in the 'Calculate engagement score and rank' node."
      },
      "typeVersion": 1
    },
    {
      "id": "6a809776-552f-4e62-96ce-b7e03bcf6498",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1600,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Scheduled hashtag check\n\nTriggers a daily schedule to load hashtags for monitoring."
      },
      "typeVersion": 1
    },
    {
      "id": "12106d24-09bd-49a9-b23f-ab06478859a0",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1168,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 320,
        "content": "## Scrape Instagram data\n\nFetches Instagram posts using Apify API after loading hashtags."
      },
      "typeVersion": 1
    },
    {
      "id": "bbfd204d-18a0-4c7f-a32a-687d92391413",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -864,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 320,
        "content": "## Process and filter data\n\nExtracts and normalizes post data, then removes empty responses."
      },
      "typeVersion": 1
    },
    {
      "id": "882b9c27-1639-4aef-8b07-e96ac3ee26fb",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 816,
        "height": 528,
        "content": "## Calculate and save scores\n\nCalculates engagement scores, formats HTML reports, sends to Telegram, and saves to Airtable."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "094a591e-3f7a-4495-b450-2c6f9e0f3ae1",
  "connections": {
    "Load monitoring hashtags": {
      "main": [
        [
          {
            "node": "Scrape Instagram posts via Apify API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove empty API responses": {
      "main": [
        [
          {
            "node": "Calculate engagement score and rank",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily check for trending posts": {
      "main": [
        [
          {
            "node": "Load monitoring hashtags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract and normalize post data": {
      "main": [
        [
          {
            "node": "Remove empty API responses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format HTML report for Telegram": {
      "main": [
        [
          {
            "node": "Send engagement report to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate engagement score and rank": {
      "main": [
        [
          {
            "node": "Format HTML report for Telegram",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save results to Airtable database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Instagram posts via Apify API": {
      "main": [
        [
          {
            "node": "Extract and normalize post data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs daily to fetch Instagram hashtag posts via the Apify API, computes a weighted engagement score, then sends a top-10 HTML report to Telegram and upserts the scored posts into Airtable. Runs on a 24-hour schedule. Loads a predefined list of hashtags to monitor…

Source: https://n8n.io/workflows/16287/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

Women creators, homemakers-turned-entrepreneurs, and feminine lifestyle brands who want a graceful, low-lift way to keep an eye on competitor content and spark weekly ideas.

HTTP Request, Google Sheets, Email Send +1
Web Scraping

Reply to unanswered Skool posts with an LLM (human-approved). Uses @apify/n8n-nodes-apify, httpRequest, telegram. Scheduled trigger; 16 nodes.

@Apify/N8N Nodes Apify, HTTP Request, Telegram
Web Scraping

🔥 Automated Daily Firecrawl Scraper with Telegram Alerts Get structured insights scraped daily from the web using Firecrawl’s AI extraction engine — then send them directly to your Telegram chat.

HTTP Request, Telegram
Web Scraping

This workflow runs twice daily to fetch remaining credits and usage from FAL AI, OpenRouter, and Apify, then compiles a formatted spend report and sends it to a Telegram chat. Runs on a schedule at tw

HTTP Request, Telegram
Web Scraping

How it works

HTTP Request, Html Extract, Telegram