{
  "id": "6336I5qyz7eI9Awi",
  "name": "Generate Instagram hashtags using OpenAI and Graph API",
  "tags": [],
  "nodes": [
    {
      "id": "af47f63f-2c54-4e11-9be2-66f9a3983049",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        1664,
        2752
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "97f03e91-0108-4fd8-bd0f-c0bf691dab6b",
      "name": "Set Dummy Caption",
      "type": "n8n-nodes-base.set",
      "position": [
        1888,
        2752
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "caption-assign",
              "name": "caption",
              "type": "string",
              "value": "Just built an amazing automation workflow with n8n! \ud83d\ude80 It connects my apps and saves me hours every week."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "51119ee4-4be2-454e-ae24-713849ebb3c5",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        2144,
        2912
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "1cd632c4-cee8-46eb-b334-935dc640d525",
      "name": "Generate Initial Hashtags",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2144,
        2576
      ],
      "parameters": {
        "text": "=Please suggest 10 English hashtags relevant to the following post caption.\nInclude a balance of big words (high post volume) and niche ones.\n\nCaption:\n{{ $json.caption }}\n\nOutput ONLY in the following JSON format.\n[\"keyword1\", \"keyword2\", ...]",
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.4
    },
    {
      "id": "e47315ec-3fa6-48d8-b404-08ab28d165e5",
      "name": "Parse & Clean Tags",
      "type": "n8n-nodes-base.code",
      "position": [
        2448,
        2576
      ],
      "parameters": {
        "jsCode": "const content = $input.first().json.text;\n// Extract JSON array from text\nconst match = content.match(/\\[.*\\]/s);\nif (!match) throw new Error(\"No JSON array found\");\nconst tags = JSON.parse(match[0]);\n\nreturn tags.map(tag => ({ json: { tag: tag.replace('#', '') } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "e9f84e2b-ce3b-4078-b1a1-3630f65c8715",
      "name": "Fetch Cached Hashtags",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        2448,
        2832
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_SHEET_NAME_OR_ID"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "3518e6e7-65ba-45df-b6a8-3e2dcabd741d",
      "name": "Split In Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        3408,
        2576
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "b4336549-b98d-4c94-ada7-c06671456392",
      "name": "Check if Tag Exists",
      "type": "n8n-nodes-base.if",
      "position": [
        3840,
        2144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "check-id-exists",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.data && $json.data.length > 0 }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1fc9bb2a-9e23-49c5-8ca9-1f93011e96d4",
      "name": "Save to Cache",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4880,
        2064
      ],
      "parameters": {
        "columns": {
          "value": {
            "tag": "={{ $json.tag }}",
            "average_likes": "={{ $json.average_likes }}",
            "average_comments": "={{ $json.average_comments }}"
          },
          "schema": [
            {
              "id": "tag",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "tag",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "average_likes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "average_likes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "average_comments",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "average_comments",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_SHEET_NAME_OR_ID"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "db82c644-e0fe-4142-847c-f9f189ac1607",
      "name": "Rate Limit Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        5104,
        2256
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1
    },
    {
      "id": "a9f06fe5-9133-42eb-976b-aa9b37a0f47d",
      "name": "Merge Cache & Fresh",
      "type": "n8n-nodes-base.merge",
      "position": [
        3696,
        2704
      ],
      "parameters": {},
      "typeVersion": 2.1
    },
    {
      "id": "4a59c75e-5769-4496-969e-30a16384b156",
      "name": "Aggregate & Rank Candidates",
      "type": "n8n-nodes-base.code",
      "position": [
        3872,
        2704
      ],
      "parameters": {
        "jsCode": "// Get data from previous nodes\n// Cached items\nconst cachedItems = $input.all().map(i => i.json).filter(i => i.source === 'cache');\n\n// Fresh items\nlet freshItems = [];\ntry {\n    freshItems = $('Format New Hashtag Data').all().map(i => i.json);\n} catch (e) {}\n\nconst allItems = [...cachedItems, ...freshItems];\n\n// Sort by average_likes desc\nallItems.sort((a, b) => (b.average_likes || 0) - (a.average_likes || 0));\n\n// Pick Top 15 candidates for AI to choose from\nconst candidates = allItems.slice(0, 15).map(i => ({\n    tag: i.tag || i.name,\n    likes: i.average_likes || 0,\n    comments: i.average_comments || 0\n}));\n\n// Get original caption for context. Handle both manual trigger and workflow trigger paths.\nlet caption = '';\ntry {\n    caption = $('Set Dummy Caption').first().json.caption;\n} catch(e) {}\n\nif (!caption) {\n    try {\n        caption = $('Workflow Trigger').first().json.caption;\n    } catch(e) {}\n}\n\nreturn { json: { caption, candidates } };"
      },
      "typeVersion": 2
    },
    {
      "id": "cf3d233b-5c53-4977-91af-2b462767a4c3",
      "name": "Format New Hashtag Data",
      "type": "n8n-nodes-base.set",
      "position": [
        4656,
        2064
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "tag-mapping",
              "name": "tag",
              "type": "string",
              "value": "={{ $('Split In Batches').item.json.tag }}"
            },
            {
              "id": "count-mapping",
              "name": "average_likes",
              "type": "number",
              "value": "={{ $json.average_likes }}"
            },
            {
              "id": "4ba7ae2a-11f6-436d-bf9d-005d3c7c8265",
              "name": "average_comments",
              "type": "number",
              "value": "={{ $json.average_comments }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "30863e06-c7f4-4711-85b1-d3d8dff49c3b",
      "name": "Check Cache Status",
      "type": "n8n-nodes-base.code",
      "inputs": [
        "main"
      ],
      "output": [
        "Uncached",
        "Cached"
      ],
      "position": [
        2960,
        2736
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nconst sheetRows = allItems.filter(i => i.json.tag && (i.json.average_likes !== undefined || i.json.media_count !== undefined)).map(i => i.json);\nconst aiTags = allItems.filter(i => !i.json.average_likes && !i.json.media_count && i.json.tag).map(i => i.json.tag);\n\nconst sheetMap = new Map();\nsheetRows.forEach(row => {\n    if(row.tag) {\n        // Convert to number if possible, undefined only if empty/invalid\n        const likes = row.average_likes !== '' && row.average_likes !== null ? Number(row.average_likes) : undefined;\n        const comments = row.average_comments !== '' && row.average_comments !== null ? Number(row.average_comments) : undefined;\n        const media = row.media_count !== '' && row.media_count !== null ? Number(row.media_count) : undefined;\n\n        // Normalize cache keys to lowercase\n        const cleanTag = row.tag.replace('#','').toLowerCase();\n        sheetMap.set(cleanTag, {\n            average_likes: !isNaN(likes) ? likes : undefined,\n            average_comments: !isNaN(comments) ? comments : undefined,\n            media_count: !isNaN(media) ? media : undefined\n        });\n    }\n});\n\nconst results = [];\naiTags.forEach(tag => {\n    // Normalize input to lowercase\n    const cleanTag = tag.replace('#','').toLowerCase();\n    const cache = sheetMap.get(cleanTag);\n    // Only treat as cache if we have the new metric (average_likes) as a valid number\n    if (cache && typeof cache.average_likes === 'number') {\n        results.push({ json: { \n            tag: cleanTag, \n            ...cache,\n            source: 'cache' \n        }});\n    } else {\n        results.push({ json: { tag: cleanTag, source: 'new' } });\n    }\n});\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "684590af-e173-4e7d-8275-8755b053ce65",
      "name": "Merge Input",
      "type": "n8n-nodes-base.merge",
      "position": [
        2784,
        2736
      ],
      "parameters": {},
      "typeVersion": 2.1
    },
    {
      "id": "c4accef3-aa36-4f62-8105-2aa5dacad19d",
      "name": "Route Cached vs New",
      "type": "n8n-nodes-base.if",
      "position": [
        3136,
        2736
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "check-cache-source",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.source }}",
              "rightValue": "cache"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9257d4bd-8f58-46d4-b076-626c70985a58",
      "name": "Get Hashtag Info",
      "type": "n8n-nodes-base.facebookGraphApi",
      "onError": "continueErrorOutput",
      "position": [
        3600,
        2224
      ],
      "parameters": {
        "node": "ig_hashtag_search",
        "options": {
          "queryParameters": {
            "parameter": [
              {
                "name": "user_id",
                "value": "YOUR_INSTAGRAM_BUSINESS_ACCOUNT_ID"
              },
              {
                "name": "q",
                "value": "={{ $('Split In Batches').item.json.tag }}"
              }
            ]
          }
        },
        "graphApiVersion": "v18.0"
      },
      "credentials": {
        "facebookGraphApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4421b2d4-e25e-4f85-9cee-e62b009c1243",
      "name": "Get Hashtag Metrics",
      "type": "n8n-nodes-base.facebookGraphApi",
      "position": [
        4192,
        2064
      ],
      "parameters": {
        "edge": "top_media",
        "node": "={{ $json.data[0].id }}",
        "options": {
          "queryParameters": {
            "parameter": [
              {
                "name": "fields",
                "value": "like_count, comments_count"
              },
              {
                "name": "user_id",
                "value": "YOUR_INSTAGRAM_BUSINESS_ACCOUNT_ID"
              }
            ]
          }
        },
        "graphApiVersion": "v18.0"
      },
      "credentials": {
        "facebookGraphApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7ac65007-39c9-4167-8ad6-677f7eee0ca5",
      "name": "Calculate Average Metrics",
      "type": "n8n-nodes-base.code",
      "position": [
        4432,
        2064
      ],
      "parameters": {
        "jsCode": "// Get input data (Facebook API node \"data\" array)\nconst items = $input.all()[0].json.data;\n\nif (!items || items.length === 0) {\n  return { average_likes: 0, average_comments: 0 };\n}\n\nconst totalPosts = items.length;\nconst totalLikes = items.reduce((sum, post) => sum + (post.like_count || 0), 0);\nconst totalComments = items.reduce((sum, post) => sum + (post.comments_count || 0), 0);\n\n// Output specific format\nreturn {\n  average_likes: Math.round(totalLikes / totalPosts),\n  average_comments: Math.round(totalComments / totalPosts)\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b2314331-1173-4f24-868c-7f37d18d3181",
      "name": "AI Model Selector",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        4064,
        2880
      ],
      "parameters": {
        "model": "gpt-5-mini",
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "90d1db2f-ba32-4efe-a167-82e9d8a3338e",
      "name": "Select Top 5 Hashtags",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        4064,
        2704
      ],
      "parameters": {
        "text": "=You are an Instagram Professional.\n\nAnalyze the following post caption and the candidate hashtag list (with average likes). Select the **5 best hashtags** that balance **\"Relevance to the post\"** and **\"High Engagement (Likes)\"**.\n\n[Selection Criteria]\n1. Must accurately reflect the post content (Prioritize Relevance).\n2. Do NOT select irrelevant tags even if they have many likes.\n3. Include niche tags (fewer likes) if they are perfect for the content.\n\nCaption:\n{{ $json.caption }}\n\nCandidate List:\n{{ JSON.stringify($json.candidates) }}\n\nOutput Format (JSON Only):\n[\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]",
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.4
    },
    {
      "id": "12d15702-4af6-42f5-9262-e14c50dcf0c6",
      "name": "Format Output",
      "type": "n8n-nodes-base.set",
      "position": [
        4384,
        2704
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "hashtags-assign",
              "name": "hashtags",
              "type": "string",
              "value": "={{ JSON.parse($json.text.replace(/```json/g,'').replace(/```/g,'')).map(t => '#' + t.replace('#','')).join(' ') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "73f9ce86-0afc-4c95-a987-708952547f55",
      "name": "Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        1664,
        2576
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "caption"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "df67bfb9-558a-41a2-8120-9cae690cb1e3",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        2368
      ],
      "parameters": {
        "width": 643,
        "height": 734,
        "content": "## \ud83d\ude80 Instagram Hashtag Generator\nThis workflow automatically generates 5 optimized hashtags for your Instagram post by combining AI suggestions with real-time performance data (average likes/comments) fetched from the Instagram Graph API. It also caches results in Google Sheets to save API calls.\n\n### \ud83d\udee0\ufe0f Setup Steps\n1. **Credentials**: Connect your `OpenAI`, `Google Sheets`, and `Facebook Graph API` credentials in the respective nodes.\n2. **Google Sheets**: Create a sheet with columns: `tag` (string), `average_likes` (number), `average_comments` (number). \n   - Update the **Fetch Cached Hashtags** and **Save to Cache** nodes with your Spreadsheet ID.\n3. **Instagram**: Update the **Get Hashtag Info** and **Get Hashtag Metrics** nodes with your Instagram Business Account ID.\n\n### \ud83d\udd04 Usage Modes\n**1. Manual Mode**\n- Execute the workflow manually using the `Manual Trigger`.\n- Update the `Set Dummy Caption` node with your post caption.\n\n**2. Sub-workflow Mode**\n- Connect this workflow to another workflow using the `Execute Workflow` node.\n- Pass the JSON input: `{ \"caption\": \"Your post content here...\" }`.\n\n### \u2699\ufe0f How it works\n1. **Generate**: AI proposes 10 relevant hashtags.\n2. **Metrics**: Checks Google Sheet cache. If missing, fetches Avg Likes/Comments from Instagram API.\n3. **Select**: AI picks the top 5 tags balancing relevance and engagement."
      },
      "typeVersion": 1
    },
    {
      "id": "7c59ed3a-9f9e-4f26-b59f-d97b573c41d7",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        2368
      ],
      "parameters": {
        "color": 7,
        "width": 490,
        "height": 736,
        "content": "## 1. Input Processing\nAccepts caption from either Manual Trigger or another Workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "a239cd2d-02fa-463f-aff2-152e9f4ca955",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        2368
      ],
      "parameters": {
        "color": 7,
        "width": 526,
        "height": 736,
        "content": "## 2. Idea Generation\nAI suggests 10 initial candidates based on the caption."
      },
      "typeVersion": 1
    },
    {
      "id": "3e338bed-c67f-47d9-b08e-0574a5e36c02",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2656,
        2368
      ],
      "parameters": {
        "color": 7,
        "width": 654,
        "height": 732,
        "content": "## 3. Caching Logic\nChecks Google Sheets to avoid repeated API calls."
      },
      "typeVersion": 1
    },
    {
      "id": "ce9c3af1-2d4e-4b49-acc3-c1f396a01d49",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        1984
      ],
      "parameters": {
        "color": 7,
        "width": 1920,
        "height": 430,
        "content": "## 4. Instagram Data & Caching\nFetches fresh data for new tags and saves to Sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "715b97b5-a3f9-4ef1-abb1-502418716ed8",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        2464
      ],
      "parameters": {
        "color": 7,
        "width": 1304,
        "height": 610,
        "content": "## 5. Final Selection\nAggregates data and AI selects the best 5 tags."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "cbf49ca1-0e84-41f5-be07-1b1ea30e5e3e",
  "connections": {
    "Merge Input": {
      "main": [
        [
          {
            "node": "Check Cache Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save to Cache": {
      "main": [
        [
          {
            "node": "Rate Limit Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set Dummy Caption",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Wait": {
      "main": [
        [
          {
            "node": "Split In Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Hashtag Info": {
      "main": [
        [
          {
            "node": "Check if Tag Exists",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rate Limit Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split In Batches": {
      "main": [
        [
          {
            "node": "Merge Cache & Fresh",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Hashtag Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Workflow Trigger": {
      "main": [
        [
          {
            "node": "Generate Initial Hashtags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Model Selector": {
      "ai_languageModel": [
        [
          {
            "node": "Select Top 5 Hashtags",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Initial Hashtags",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Set Dummy Caption": {
      "main": [
        [
          {
            "node": "Generate Initial Hashtags",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Cached Hashtags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Cache Status": {
      "main": [
        [
          {
            "node": "Route Cached vs New",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Clean Tags": {
      "main": [
        [
          {
            "node": "Merge Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Tag Exists": {
      "main": [
        [
          {
            "node": "Get Hashtag Metrics",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rate Limit Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Hashtag Metrics": {
      "main": [
        [
          {
            "node": "Calculate Average Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Cache & Fresh": {
      "main": [
        [
          {
            "node": "Aggregate & Rank Candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Cached vs New": {
      "main": [
        [
          {
            "node": "Merge Cache & Fresh",
            "type": "main",
            "index": 1
          }
        ],
        [
          {
            "node": "Split In Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Cached Hashtags": {
      "main": [
        [
          {
            "node": "Merge Input",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Select Top 5 Hashtags": {
      "main": [
        [
          {
            "node": "Format Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format New Hashtag Data": {
      "main": [
        [
          {
            "node": "Save to Cache",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Average Metrics": {
      "main": [
        [
          {
            "node": "Format New Hashtag Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Initial Hashtags": {
      "main": [
        [
          {
            "node": "Parse & Clean Tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate & Rank Candidates": {
      "main": [
        [
          {
            "node": "Select Top 5 Hashtags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}