This workflow corresponds to n8n.io template #14643 — we link there as the canonical source.
This workflow follows the HTTP Request → Slack 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 →
{
"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
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automate your entire Instagram carousel publishing pipeline from a single webhook call. This workflow receives a product collection payload, loops through each slide image, uploads every asset via Upload to URL to generate stable public CDN URLs, creates individual Instagram…
Source: https://n8n.io/workflows/14643/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
📦 Automated Instagram Product Drop via uploadtourl
Convert your customer satisfaction into high-converting social media content with this fully automated social proof pipeline. This workflow scans your database for top-tier reviews, generates a brande
This workflow automates the post-publish process for YouTube videos, combining advanced SEO optimization, cross-platform promotion, and analytics reporting. It is designed for creators, marketers, and
This template is ideal for sales teams, recruiters, business development professionals, and relationship managers who need to monitor changes in their network's LinkedIn profiles. Perfect for agencies
Automatically discovers trending topics in your niche and generates ready-to-use content ideas with AI. Twitter/X trending topics and hashtags Reddit hot posts from niche subreddits Google Trends dail