{
  "nodes": [
    {
      "id": "b9d296b0-f88a-4ec2-b8c2-599a9bd602d6",
      "name": "Template Overview1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        864
      ],
      "parameters": {
        "width": 440,
        "height": 464,
        "content": "Instagram Image Carousel Post \u2013 Product Collection Drop\n\nReceives a product collection via webhook, validates slides (2\u201310), and builds a carousel caption. Each image is fetched, uploaded via UploadToURL to get a public URL, then used to create Instagram child media containers. All slides are combined into a carousel, published, and a Slack notification is sent.\n\nFlow:\nWebhook \u2192 Validate \u2192 Build caption \u2192 Process slides \u2192 Upload images \u2192 Create IG containers \u2192 Publish carousel \u2192 Notify Slack\n\nRequired:\nIG_USER_ID, IG_ACCESS_TOKEN, SLACK_CHANNEL_ID\n\nPayload:\ncollectionName, caption, hook, cta, hashtags[],\nslides[]: { imageUrl, title, price, currency }"
      },
      "typeVersion": 1
    },
    {
      "id": "8b0214d3-a59d-488f-b7c8-27c8c6f82731",
      "name": "Sticky Nodes 1-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        1376
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 600,
        "content": "Webhook \u2013 Receive Payload: Accepts POST requests to /ig-carousel and provides an inline response upon workflow completion.\n\nCode \u2013 Validate: Verifies that the payload contains 2\u201310 valid HTTPS image URLs and required environment variables, failing fast with descriptive errors if criteria aren't met.\n\nCode \u2013 Build Caption: Construct a structured Instagram caption including hooks, product lists, and CTAs, ensuring a safe limit of 2,200 characters."
      },
      "typeVersion": 1
    },
    {
      "id": "b244676c-0e11-476f-b762-b295ed5f37a9",
      "name": "Sticky Nodes 4-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1856,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 852,
        "height": 768,
        "content": "Split In Batches: Iterates through the slides array one by one to trigger individual image processing, then routes to assembly once all items are processed.\n\nHTTP \u2013 Fetch Slide Image: Downloads each slide's image as a binary file to prepare it for hosting.\n\nUpload to URL: Uploads the binary to a CDN to generate a mandatory public HTTPS URL, which is required by Instagram's API for carousel items.\n\nCode \u2013 Create Child Container: Communicates with the Instagram Graph API to generate a specific \"child\" media container marked as a carousel item, returning its unique ID."
      },
      "typeVersion": 1
    },
    {
      "id": "f5bf755e-04bc-4421-9578-b083871abcb1",
      "name": "Sticky Nodes 8-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2736,
        1264
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 764,
        "content": "Code \u2013 Aggregate Child IDs: Collects all individual child container IDs from the loop and joins them into a comma-separated string required for the parent API call. It also re-attaches the final caption and metadata via cross-node references.\n\nHTTP \u2013 Create Carousel Container: Submits a POST request to the Instagram Graph API to create a parent container with the media_type set to CAROUSEL. The caption is applied exclusively to this parent container.\n\nWait 8s: Pauses the workflow for 8 seconds to allow Instagram sufficient time to validate and process the multiple assets within the carousel."
      },
      "typeVersion": 1
    },
    {
      "id": "1643314e-9c87-424f-8d2b-a98a8e750535",
      "name": "Sticky Nodes 11-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        1248
      ],
      "parameters": {
        "color": 7,
        "width": 844,
        "height": 772,
        "content": "Nodes 11-14: Publish, Metadata, Notify, Respond\n\nHTTP \u2013 Publish Carousel: Finalizes the process by calling the /media_publish endpoint with the parent container ID, returning the official live Instagram Post ID.\n\nHTTP \u2013 Fetch Post Metadata: Automatically retrieves the live permalink, timestamp, and media type to provide a direct URL for the published content.\n\nSlack \u2013 Notify Team: Dispatches a formatted alert to Slack containing the collection name, slide count, and a direct link to the new post.\n\nRespond to Webhook: Concludes the workflow by returning a JSON success payload to the initial caller, including the media ID, permalink, and slide count."
      },
      "typeVersion": 1
    },
    {
      "id": "f2f63b18-0389-4cde-b4a7-b2e1b92fc568",
      "name": "Webhook Receive Payload1",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1248,
        1680
      ],
      "parameters": {
        "path": "ig-carousel-drop",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "aa156470-a4a4-43c4-a37f-c1fa93ad42aa",
      "name": "Code Validate Payload1",
      "type": "n8n-nodes-base.code",
      "position": [
        1472,
        1680
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\n\nif (!item.slides || !Array.isArray(item.slides)) {\n  throw new Error('slides must be a non-empty array');\n}\nif (item.slides.length < 2) {\n  throw new Error('Carousel requires at least 2 slides. Got: ' + item.slides.length);\n}\nif (item.slides.length > 10) {\n  item.slides = item.slides.slice(0, 10);\n}\n\nitem.slides.forEach(function(s, i) {\n  if (!s.imageUrl) throw new Error('slides[' + i + '] missing imageUrl');\n  if (!s.imageUrl.startsWith('https://')) {\n    throw new Error('slides[' + i + '].imageUrl must be HTTPS');\n  }\n});\n\nif (!item.caption) throw new Error('caption is required');\n\nreturn { ...item };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "38320f0c-0fff-4f0c-a497-a2a15b25df87",
      "name": "Code Build Caption1",
      "type": "n8n-nodes-base.code",
      "position": [
        1696,
        1680
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\n\nconst collectionName = item.collectionName || 'New Collection';\nconst slides = item.slides;\nconst hook = item.hook || 'Swipe to see the full collection';\nconst cta = item.cta || 'Link in bio to shop';\n\nconst slideLines = slides.map(function(s, i) {\n  var line = (i + 1) + '. ' + (s.title || 'Item ' + (i + 1));\n  if (s.price) line = line + '  -  ' + (s.currency || '$') + s.price;\n  return line;\n}).join('\\n');\n\nvar rawTags = item.hashtags || ['newcollection', 'shopnow', 'carousel'];\nvar hashtagBlock = rawTags.map(function(h) {\n  return h.startsWith('#') ? h : '#' + h;\n}).join(' ');\n\nvar captionParts = [\n  collectionName + ' - ' + hook + ' ->',\n  '',\n  item.caption.trim(),\n  '',\n  'In this drop:',\n  slideLines,\n  '',\n  cta,\n  '',\n  hashtagBlock\n];\n\nvar fullCaption = captionParts.join('\\n').trim();\nif (fullCaption.length > 2200) {\n  fullCaption = fullCaption.substring(0, 2196) + '...';\n}\n\nreturn {\n  ...item,\n  collectionName: collectionName,\n  fullCaption: fullCaption,\n  slideCount: slides.length\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7a19b333-087d-4a9d-bf8b-4471b80a7dca",
      "name": "Split In Batches1",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1904,
        1680
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "d58566ac-ae06-4b41-b34d-021da20c5a17",
      "name": "HTTP Fetch Slide Image1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2128,
        1680
      ],
      "parameters": {
        "url": "={{ $('Code Build Caption1').item.json.slides[$('Split In Batches1').context.currentRunIndex].imageUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "0dc4fbc5-1f83-432b-9fe7-e455ac0fdf82",
      "name": "Upload to URL1",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        2352,
        1680
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "0370574a-7d92-4415-acbf-e160e703cc24",
      "name": "Code Create Child Container1",
      "type": "n8n-nodes-base.code",
      "position": [
        2576,
        1680
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\n\nconst publicUrl = item.public_url || item.url || item.file_url || item.cdn_url || '';\nif (!publicUrl) {\n  throw new Error('Upload to URL returned no public URL. Keys: ' + JSON.stringify(Object.keys(item)));\n}\n\nconst igUserId = $env.IG_USER_ID;\nconst accessToken = $env.IG_ACCESS_TOKEN;\n\nconst res = await fetch('https://graph.facebook.com/v19.0/' + igUserId + '/media', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    image_url: publicUrl,\n    media_type: 'IMAGE',\n    is_carousel_item: true,\n    access_token: accessToken\n  })\n});\n\nconst data = await res.json();\nif (data.error) {\n  throw new Error('IG child container error: ' + data.error.message);\n}\n\nreturn {\n  ...item,\n  childContainerId: data.id,\n  publicUrl: publicUrl\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "90bda58e-748e-4bb8-8dac-c11d47050ba2",
      "name": "Code Aggregate Child IDs1",
      "type": "n8n-nodes-base.code",
      "position": [
        2784,
        1680
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nconst prepData = $('Code Build Caption1').first().json;\n\nconst childContainerIds = allItems\n  .map(function(i) { return i.json.childContainerId; })\n  .filter(Boolean);\n\nif (childContainerIds.length < 2) {\n  throw new Error('Need at least 2 child containers. Got: ' + childContainerIds.length);\n}\n\nreturn {\n  childrenParam: childContainerIds.join(','),\n  fullCaption: prepData.fullCaption,\n  slideCount: prepData.slideCount,\n  collectionName: prepData.collectionName,\n  igUserId: $env.IG_USER_ID,\n  accessToken: $env.IG_ACCESS_TOKEN\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8dcffeb1-e2fe-4cd3-8f79-589839e73a24",
      "name": "IG Create Carousel Container1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3008,
        1680
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $json.igUserId }}/media",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ media_type: 'CAROUSEL', children: $json.childrenParam, caption: $json.fullCaption, access_token: $json.accessToken }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "2a9f1b6f-5c10-4805-87c9-41ca2b7255db",
      "name": "Wait IG Buffer1",
      "type": "n8n-nodes-base.wait",
      "position": [
        3232,
        1680
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 8
      },
      "typeVersion": 1
    },
    {
      "id": "824e89e7-ef9c-495f-9f73-65d91c8c538d",
      "name": "IG Publish Carousel1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3456,
        1680
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $('Code Aggregate Child IDs1').item.json.igUserId }}/media_publish",
        "method": "POST",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "creation_id",
              "value": "={{ $('IG Create Carousel Container1').item.json.id }}"
            },
            {
              "name": "access_token",
              "value": "={{ $env.IG_ACCESS_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "0f45b3c4-a1b0-4dfa-b277-6679a0a87b60",
      "name": "HTTP Fetch Post Metadata1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3664,
        1680
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $('IG Publish Carousel1').item.json.id }}",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "id,permalink,timestamp,media_type"
            },
            {
              "name": "access_token",
              "value": "={{ $env.IG_ACCESS_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8c12771a-8c3e-47eb-b37b-9b0a15baa160",
      "name": "Slack Notify Team1",
      "type": "n8n-nodes-base.slack",
      "position": [
        3888,
        1680
      ],
      "parameters": {
        "text": "Carousel Published!\n\nCollection: {{ $('Code Build Caption').item.json.collectionName }}\nSlides: {{ $('Code Build Caption').item.json.slideCount }}\nPost ID: {{ $('IG Publish Carousel').item.json.id }}\nLink: {{ $('HTTP Fetch Post Metadata').item.json.permalink }}\nPublished: {{ new Date().toLocaleString() }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_CHANNEL_ID }}"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "332f53ff-a50d-42bf-984d-cf5a29fdd3ea",
      "name": "Respond to Webhook1",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4112,
        1680
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, mediaId: $('IG Publish Carousel1').item.json.id, permalink: $('HTTP Fetch Post Metadata1').item.json.permalink, slideCount: $('Code Aggregate Child IDs1').item.json.slideCount, collectionName: $('Code Aggregate Child IDs1').item.json.collectionName }) }}"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Upload to URL1": {
      "main": [
        [
          {
            "node": "Code Create Child Container1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait IG Buffer1": {
      "main": [
        [
          {
            "node": "IG Publish Carousel1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split In Batches1": {
      "main": [
        [
          {
            "node": "HTTP Fetch Slide Image1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code Aggregate Child IDs1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Notify Team1": {
      "main": [
        [
          {
            "node": "Respond to Webhook1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Build Caption1": {
      "main": [
        [
          {
            "node": "Split In Batches1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IG Publish Carousel1": {
      "main": [
        [
          {
            "node": "HTTP Fetch Post Metadata1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Validate Payload1": {
      "main": [
        [
          {
            "node": "Code Build Caption1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Fetch Slide Image1": {
      "main": [
        [
          {
            "node": "Upload to URL1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Receive Payload1": {
      "main": [
        [
          {
            "node": "Code Validate Payload1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Aggregate Child IDs1": {
      "main": [
        [
          {
            "node": "IG Create Carousel Container1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Fetch Post Metadata1": {
      "main": [
        [
          {
            "node": "Slack Notify Team1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Create Child Container1": {
      "main": [
        [
          {
            "node": "Split In Batches1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IG Create Carousel Container1": {
      "main": [
        [
          {
            "node": "Wait IG Buffer1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}