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
This workflow captures buying-signal leads via a webhook (or optionally polls a HubSpot list), generates a personalized LinkedIn intro and follow-up sequence with Twain, and posts the ready-to-send dr
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
generación_publicaciones (workflow 3) webhook all platforms decrypt publicar git. Uses dataTable, linkedIn, httpRequest. Webhook trigger; 50 nodes.