AutomationFlowsSocial Media › Create Event Recap Instagram Carousels From Photo Dumps Using Upload to URL

Create Event Recap Instagram Carousels From Photo Dumps Using Upload to URL

ByJitesh Dugar @jiteshdugar on n8n.io

Automate your post-event Instagram carousel using a fan-out and merge pattern. One Code node splits the photos array into individual n8n items. Every photo then flows through HTTP Fetch, Upload to URL, and a child container Code node independently. A Merge node collects all…

Webhook trigger★★★★☆ complexity18 nodesHTTP RequestN8N Nodes UploadtourlAirtableSlack
Social Media Trigger: Webhook Nodes: 18 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #14656 — 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
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "b154d5fd-2503-48fe-8058-e8bd80d8102c",
      "name": "Template Overview1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -384,
        2400
      ],
      "parameters": {
        "width": 460,
        "height": 504,
        "content": "Event Recap Photo Dump \u2013 Instagram Carousel\n\nAutomatically creates and publishes an Instagram carousel from event photos. Receives event data via webhook, splits photos into items, uploads each via UploadToURL to get public CDN links, creates IG child containers, merges them into a carousel, publishes the post, logs it in Airtable, and notifies the team on Slack.\n\nFlow:\nWebhook \u2192 Split photos \u2192 Fetch images \u2192 Upload \u2192 Create IG containers \u2192 Merge \u2192 Publish \u2192 Log \u2192 Notify\n\nRequired:\nIG_USER_ID, IG_ACCESS_TOKEN, SLACK_CHANNEL_ID, AIRTABLE_BASE_ID\n\nPayload:\neventName, eventDate, location, attendees,\ncaption, highlights[], nextEventDate, hashtags[],\nphotos[]: { url, label }"
      },
      "typeVersion": 1
    },
    {
      "id": "f3ff8f1c-ca71-4243-bc1c-9764485c448a",
      "name": "Sticky Nodes 1-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        2896
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 532,
        "content": "Webhook \u2013 Receive Event Payload: Accepts POST requests at /ig-event-recap and returns a final status once completed.\n\nCode \u2013 Validate & Caption: Verifies 2\u201310 HTTPS photo URLs and generates a full storytelling caption from event metadata.\n\nFan-Out Execution: Converts the photo array into individual items to automatically trigger downstream processing for each image."
      },
      "typeVersion": 1
    },
    {
      "id": "b29ada57-7653-4700-b93d-c08d1d51a071",
      "name": "Sticky Nodes 3-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        2912
      ],
      "parameters": {
        "color": 7,
        "width": 808,
        "height": 500,
        "content": "HTTP \u2013 Fetch Photo Binary: Downloads each photoUrl as a binary file; executes once for every image in the set.\n\nUpload to URL: Uploads each binary to a CDN to generate the mandatory public HTTPS URL required by Instagram.\n\nCode \u2013 Create Child Container: Generates an Instagram child media container for each photo, returning a childContainerId and preserving the slide order.\n\nMerge: Acts as the \"fan-in\" point, waiting for all individual photo processes to finish before collecting all container IDs into a single execution."
      },
      "typeVersion": 1
    },
    {
      "id": "f5af5d14-a51d-4e48-bea1-d1889cdde497",
      "name": "Sticky Nodes 7-",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        2912
      ],
      "parameters": {
        "color": 7,
        "width": 1732,
        "height": 500,
        "content": "Code \u2013 Build Children Param: Sorts all merged items by slideIndex and joins the container IDs into a comma-separated string to preserve photo order for the parent API call.\n\nHTTP \u2013 Create Carousel Container: Submits a POST request to Instagram with the CAROUSEL media type, the sorted children string, and the full storytelling caption.\n\nWait 8s & Publish: Provides a processing buffer before calling /media_publish to finalize the post and retrieve the official Post ID.\n\nLog & Notify: Fetches post metadata, records the event details in Airtable, alerts the team via Slack, and returns a final JSON success payload to the webhook caller."
      },
      "typeVersion": 1
    },
    {
      "id": "f04692ad-4470-497d-921b-7091baae6f60",
      "name": "Webhook Receive Event1",
      "type": "n8n-nodes-base.webhook",
      "position": [
        256,
        3200
      ],
      "parameters": {
        "path": "ig-event-recap",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "b581b157-1175-4d61-b9b8-00516dbe59b9",
      "name": "Code Validate Caption and Split Photos1",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        3200
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\n\nif (!item.eventName) throw new Error('eventName is required');\nif (!item.photos || !Array.isArray(item.photos) || item.photos.length < 2) {\n  throw new Error('photos array with at least 2 items is required');\n}\n\nvar seen = {};\nvar photos = item.photos\n  .map(function(p) { return { url: (p.url || p).trim(), label: p.label || '' }; })\n  .filter(function(p) {\n    if (!p.url.startsWith('https://') || seen[p.url]) return false;\n    seen[p.url] = true;\n    return true;\n  })\n  .slice(0, 10);\n\nif (photos.length < 2) throw new Error('Need at least 2 valid unique HTTPS photo URLs');\n\nvar rawTags = item.hashtags || ['eventrecap', 'photodump', 'nightout', 'memories'];\nvar hashtagBlock = (Array.isArray(rawTags) ? rawTags : [rawTags])\n  .join(' ').split(/[,\\s]+/).filter(Boolean)\n  .map(function(h) { return h.startsWith('#') ? h : '#' + h; }).join(' ');\n\nvar parts = [item.eventName + ' - swipe to relive the night ->\\n'];\nif (item.location && item.eventDate) parts.push(item.location + '  -  ' + item.eventDate + '\\n');\nelse if (item.location)  parts.push(item.location + '\\n');\nelse if (item.eventDate) parts.push(item.eventDate + '\\n');\nif (item.attendees) parts.push(Number(item.attendees).toLocaleString() + ' people in the building\\n');\nif (item.caption)   parts.push(item.caption.trim() + '\\n');\nif (item.highlights && item.highlights.length) {\n  var hl = Array.isArray(item.highlights) ? item.highlights : [item.highlights];\n  parts.push('Highlights:\\n' + hl.slice(0,5).map(function(h,i){return (i+1)+'. '+h;}).join('\\n') + '\\n');\n}\nif (item.nextEventDate) parts.push('Next one: ' + item.nextEventDate + ' - stay locked.\\n');\nparts.push(hashtagBlock);\n\nvar fullCaption = parts.join('\\n').trim();\nif (fullCaption.length > 2200) fullCaption = fullCaption.substring(0, 2196) + '...';\n\nreturn photos.map(function(photo, idx) {\n  return {\n    json: {\n      photoUrl:    photo.url,\n      photoLabel:  photo.label,\n      slideIndex:  idx + 1,\n      slideCount:  photos.length,\n      fullCaption: fullCaption,\n      eventName:   item.eventName,\n      igUserId:    $env.IG_USER_ID,\n      accessToken: $env.IG_ACCESS_TOKEN\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "70ef02a0-6a10-4742-bc96-dd11cdbe256f",
      "name": "HTTP Fetch Photo Binary1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        752,
        3200
      ],
      "parameters": {
        "url": "={{ $json.photoUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d9fb2aeb-b38e-4494-8e06-f5398b8d030a",
      "name": "Upload to URL1",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        928,
        3200
      ],
      "parameters": {},
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "472ef619-01a0-42ac-a995-f58af354fba0",
      "name": "Code Create Child Container1",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        3200
      ],
      "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 for slide ' + item.slideIndex + '. 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 (slide ' + item.slideIndex + '): ' + data.error.message);\n}\n\nreturn {\n  ...item,\n  childContainerId: data.id,\n  cdnUrl: publicUrl\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "dd40c79d-d564-4e7b-8976-033b45571c44",
      "name": "Merge All Child Items1",
      "type": "n8n-nodes-base.merge",
      "position": [
        1328,
        3200
      ],
      "parameters": {},
      "typeVersion": 3
    },
    {
      "id": "621378aa-2da7-4291-a730-5a8810bc9ad2",
      "name": "Code Build Children Param1",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        3200
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\n\nvar sorted = allItems\n  .map(function(i) { return i.json; })\n  .filter(function(d) { return d.childContainerId; })\n  .sort(function(a, b) { return a.slideIndex - b.slideIndex; });\n\nif (sorted.length < 2) {\n  throw new Error('Need at least 2 child containers. Got: ' + sorted.length);\n}\n\nvar first = sorted[0];\n\nreturn {\n  childrenParam: sorted.map(function(d) { return d.childContainerId; }).join(','),\n  slideLog:      sorted.map(function(d) { return { slide: d.slideIndex, id: d.childContainerId, url: d.cdnUrl }; }),\n  slideCount:    sorted.length,\n  fullCaption:   first.fullCaption,\n  eventName:     first.eventName,\n  igUserId:      first.igUserId,\n  accessToken:   first.accessToken\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4d381eaf-9796-40c3-974a-472593737801",
      "name": "IG Create Carousel Container1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1808,
        3200
      ],
      "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": "e3c529b1-aa49-4819-87d6-191e8663b843",
      "name": "Wait IG Buffer1",
      "type": "n8n-nodes-base.wait",
      "position": [
        2016,
        3200
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 8
      },
      "typeVersion": 1
    },
    {
      "id": "c9a1e018-f70a-4e4c-9168-77bb0f54c104",
      "name": "IG Publish Carousel1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2240,
        3200
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $('Code Build Children Param1').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": "cfa69ba2-8966-4aef-945f-7d6d34f3c878",
      "name": "HTTP Fetch Post Metadata1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2464,
        3200
      ],
      "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": "df8b215c-6d74-4e53-83de-f4e44cfa9cb7",
      "name": "Airtable Log Published Post1",
      "type": "n8n-nodes-base.airtable",
      "position": [
        2688,
        3200
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.AIRTABLE_BASE_ID }}"
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "Event Posts Log"
        },
        "columns": {
          "value": {
            "Status": "Published",
            "Post ID": "={{ $('IG Publish Carousel1').item.json.id }}",
            "Permalink": "={{ $('HTTP Fetch Post Metadata1').item.json.permalink }}",
            "Event Name": "={{ $('Code Build Children Param1').item.json.eventName }}",
            "Slide Count": "={{ $('Code Build Children Param1').item.json.slideCount }}",
            "Published At": "={{ $('HTTP Fetch Post Metadata1').item.json.timestamp }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "98d979fb-e6c0-44fa-8e84-b188b4094363",
      "name": "Slack Notify Team1",
      "type": "n8n-nodes-base.slack",
      "position": [
        2896,
        3200
      ],
      "parameters": {
        "text": "Event Recap Carousel Published!\n\nEvent: {{ $('Code Build Children Param').item.json.eventName }}\nSlides: {{ $('Code Build Children Param').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"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4bcdc955-b9d3-42d5-a662-262aedd9c31e",
      "name": "Respond to Webhook1",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3120,
        3200
      ],
      "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 Build Children Param1').item.json.slideCount, eventName: $('Code Build Children Param1').item.json.eventName, publishedAt: $('HTTP Fetch Post Metadata1').item.json.timestamp }) }}"
      },
      "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
          }
        ]
      ]
    },
    "Slack Notify Team1": {
      "main": [
        [
          {
            "node": "Respond to Webhook1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IG Publish Carousel1": {
      "main": [
        [
          {
            "node": "HTTP Fetch Post Metadata1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Child Items1": {
      "main": [
        [
          {
            "node": "Code Build Children Param1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Receive Event1": {
      "main": [
        [
          {
            "node": "Code Validate Caption and Split Photos1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Fetch Photo Binary1": {
      "main": [
        [
          {
            "node": "Upload to URL1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Fetch Post Metadata1": {
      "main": [
        [
          {
            "node": "Airtable Log Published Post1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Build Children Param1": {
      "main": [
        [
          {
            "node": "IG Create Carousel Container1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable Log Published Post1": {
      "main": [
        [
          {
            "node": "Slack Notify Team1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Create Child Container1": {
      "main": [
        [
          {
            "node": "Merge All Child Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IG Create Carousel Container1": {
      "main": [
        [
          {
            "node": "Wait IG Buffer1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code Validate Caption and Split Photos1": {
      "main": [
        [
          {
            "node": "HTTP Fetch Photo Binary1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

Automate your post-event Instagram carousel using a fan-out and merge pattern. One Code node splits the photos array into individual n8n items. Every photo then flows through HTTP Fetch, Upload to URL, and a child container Code node independently. A Merge node collects all…

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

More Social Media workflows → · Browse all categories →

Related workflows

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

Social Media

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

N8N Nodes Uploadtourl, Airtable, HTTP Request +1
Social Media

Turn your best 5-star reviews into a daily stream of branded social proof content -- fully automated. This workflow pulls the oldest unposted 5-star review from Google Sheets, generates a custom quote

Google Sheets, HTTP Request, N8N Nodes Uploadtourl +1
Social Media

📦 Automated Instagram Product Drop via uploadtourl

HTTP Request, Airtable, Slack +1
Social Media

This workflow automatically tracks Facebook Event RSVPs, stores attendee data in Airtable, checks event capacity and notifies your team on Slack when the event becomes full. It eliminates manual RSVP

Airtable, Slack
Social Media

Instagram - Fluxo de mensagens. Uses rabbitmq, rabbitmqTrigger, googleSheets, httpRequest. Webhook trigger; 74 nodes.

Rabbitmq, Rabbitmq Trigger, Google Sheets +1