{
  "nodes": [
    {
      "id": "e2c7ef94-f917-4ac0-8553-68ed48d514af",
      "name": "Sticky \u2014 Webhook + Normalize",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1104,
        1824
      ],
      "parameters": {
        "color": 7,
        "width": 688,
        "height": 740,
        "content": "Webhook Trigger: Listens for POST payloads (Shopify, Airtable, or manual). It uses a Webhook Respond node to immediately return a 200 ACK, preventing timeout errors from the sender.\n\nSet (Normalize Fields): Sanitizes raw data into a consistent schema. It maps nested fields (e.g., Shopify\u2019s body.title) to flat variables, applies fallback defaults for missing captions/hashtags, and pulls the ig_account_id from environment variables if the payload is empty."
      },
      "typeVersion": 1
    },
    {
      "id": "864aab29-bfa0-4ca8-a487-d67b1948c65e",
      "name": "Sticky \u2014 Fetch + Upload",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        1824
      ],
      "parameters": {
        "color": 7,
        "width": 518,
        "height": 472,
        "content": "HTTP \u2014 Fetch Product Image: Downloads the image from image_url as a binary file (imageData). It supports any public source, including Shopify CDNs or S3 buckets.\n\nUpload to URL: Uploads the binary to a media host and returns a public CDN URL."
      },
      "typeVersion": 1
    },
    {
      "id": "4984fd3c-c727-49b8-b6e1-699e6bf7d750",
      "name": "Sticky \u2014 Caption + Container",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        1840
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 380,
        "content": "Code \u2014 Build Caption: Assembles the final text using the product name, price, and links. It automatically truncates to 2,200 characters to meet Instagram\u2019s API limits and outputs a single final_caption string.\n\nIG \u2014 Create Media Container: Initiates the first half of Instagram\u2019s two-step publishing flow. It sends the image_url and final_caption to the Graph API, which processes the image asynchronously and returns a container_id."
      },
      "typeVersion": 1
    },
    {
      "id": "aff2900c-09af-4369-8133-b2bb7f347f30",
      "name": "Sticky \u2014 Wait + Publish",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        1840
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 396,
        "content": "Wait (5s Buffer): Pauses the workflow to ensure Instagram completes its asynchronous image processing. For high-resolution files, this can be extended or replaced with a status-check polling loop.\n\nIG \u2014 Publish Container: Executes the final media_publish call using the container_id. Once successful, Instagram returns a Live Post ID, confirming the content is officially on the feed."
      },
      "typeVersion": 1
    },
    {
      "id": "91928124-30f5-4fd0-a9fa-c6ba9d35b7d0",
      "name": "Sticky \u2014 Log + Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        1840
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 384,
        "content": "Airtable \u2014 Log Post: Records the Post ID, product name, and timestamp into a tracking base for audits and analytics. Easily swappable with Google Sheets.\n\nSlack \u2014 Notify Team: Sends a formatted alert confirming the post is live. Includes the live link and trigger source. Can be replaced with Email or Discord nodes as needed."
      },
      "typeVersion": 1
    },
    {
      "id": "3e9bb439-0467-407e-acec-84096aa3d3ba",
      "name": "\ud83d\udccb Template Overview1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2000,
        1088
      ],
      "parameters": {
        "width": 708,
        "height": 724,
        "content": "## \ud83d\udce6 Brand Product Drop \u2014 Single Image Scheduler\n\n**What this workflow does:**\nAutomates the full Instagram product announcement pipeline \u2014 from a new product trigger (Shopify, Airtable, or manual webhook) all the way to a published Instagram post with a structured caption and hashtag block.\n\n**Flow Overview:**\n1. \ud83d\udd14 **Webhook Trigger** \u2014 Receives product data (name, image URL, price, hashtags, caption intro)\n2. \ud83e\uddf9 **Set / Normalize Fields** \u2014 Cleans and structures the incoming payload into consistent fields\n3. \ud83c\udf10 **Fetch Product Image** \u2014 Downloads the raw image binary from the external product image URL\n4. \u2601\ufe0f **Upload to URL** \u2014 Uploads the image binary and returns a public CDN URL\n5. \u270d\ufe0f **Build Caption** \u2014 Constructs a brand-ready caption with emoji, price, CTA, and hashtag block\n6. \ud83d\udcf8 **IG: Create Media Container** \u2014 Calls Instagram Graph API to create a media container using the public image URL\n7. \u23f3 **Wait** \u2014 Pauses 5 seconds to allow Instagram to process the media container\n8. \u2705 **IG: Publish Container** \u2014 Publishes the approved container to the Instagram feed\n9. \ud83d\udcca **Log to Airtable** \u2014 Records post ID, timestamp, caption, and image URL for tracking\n10. \ud83d\udd14 **Notify via Slack** \u2014 Sends a confirmation message with the published post details\n\n**Prerequisites:**\n- Instagram Graph API access token (Business or Creator account)\n- Instagram Business Account ID\n- uploadtourl node credentials configured in n8n\n- (Optional) Airtable API key + Base ID for post logging\n- (Optional) Slack API credentials for team notifications\n\n**Supported Trigger Sources:**\n- Shopify `products/create` webhook\n- Airtable automation webhook\n- Manual HTTP POST for testing"
      },
      "typeVersion": 1
    },
    {
      "id": "86e26a7e-4da6-4ad7-b3d7-caa2ef228265",
      "name": "Webhook \u2014 Product Trigger1",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1040,
        2016
      ],
      "parameters": {
        "path": "product-drop",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "b4b02df3-5716-4752-91c1-729a7f9745d0",
      "name": "Webhook Response1",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -912,
        2400
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={ \"status\": \"received\", \"message\": \"Product drop workflow triggered successfully\" }"
      },
      "typeVersion": 1
    },
    {
      "id": "d2efad45-7d2d-475f-99c1-a345b9761495",
      "name": "Set \u2014 Normalize Fields1",
      "type": "n8n-nodes-base.set",
      "position": [
        -656,
        2016
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f1",
              "name": "product_name",
              "type": "string",
              "value": "={{ $json.body.product_name ?? $json.body.title ?? 'New Product' }}"
            },
            {
              "id": "f2",
              "name": "image_url",
              "type": "string",
              "value": "={{ $json.body.image_url ?? $json.body.images?.[0]?.src ?? '' }}"
            },
            {
              "id": "f3",
              "name": "price",
              "type": "string",
              "value": "={{ $json.body.price ?? $json.body.variants?.[0]?.price ?? '' }}"
            },
            {
              "id": "f4",
              "name": "caption_intro",
              "type": "string",
              "value": "={{ $json.body.caption_intro ?? 'Something new just dropped.' }}"
            },
            {
              "id": "f5",
              "name": "hashtags",
              "type": "string",
              "value": "={{ $json.body.hashtags ?? '#newdrop #product #brand' }}"
            },
            {
              "id": "f6",
              "name": "ig_account_id",
              "type": "string",
              "value": "={{ $json.body.ig_account_id ?? $env.IG_ACCOUNT_ID }}"
            },
            {
              "id": "f7",
              "name": "trigger_source",
              "type": "string",
              "value": "={{ $json.body.source ?? 'webhook' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a2ac043b-f271-418e-aaad-908f1c8f7d73",
      "name": "HTTP \u2014 Fetch Product Image1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -256,
        2016
      ],
      "parameters": {
        "url": "={{ $json.image_url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "imageData"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ba514950-4bae-4010-a35b-4b11a57cea78",
      "name": "Code \u2014 Build Caption1",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        2016
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\nconst norm = $('Set \u2014 Normalize Fields1').item.json;\n\nconst productName = norm.product_name || 'New Drop';\nconst price = norm.price ? `\ud83d\udcb0 ${norm.price}` : '';\nconst captionIntro = norm.caption_intro || 'Something new just dropped.';\nconst hashtags = norm.hashtags || '#newdrop';\n\nconst lines = [\n  captionIntro,\n  '',\n  `\ud83d\udecd\ufe0f ${productName}`,\n  price,\n  '',\n  '\ud83d\udc49 Link in bio to shop now.',\n  '.',\n  '.',\n  '.',\n  hashtags\n].filter(l => l !== undefined && l !== null);\n\nlet finalCaption = lines.join('\\n');\n\nif (finalCaption.length > 2200) {\n  finalCaption = finalCaption.substring(0, 2196) + '...';\n}\n\nreturn {\n  ...item,\n  final_caption: finalCaption,\n  public_image_url: item.public_url || item.url || item.file_url || item.cdn_url\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "15aae634-3e02-4adb-826e-e7b9edeab8cf",
      "name": "IG \u2014 Create Media Container1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        576,
        2016
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $('Set \u2014 Normalize Fields1').item.json.ig_account_id }}/media",
        "method": "POST",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "image_url",
              "value": "={{ $json.public_image_url }}"
            },
            {
              "name": "caption",
              "value": "={{ $json.final_caption }}"
            },
            {
              "name": "access_token",
              "value": "={{ $credentials.instagramGraphApi.accessToken }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "58cd9531-c677-47e6-a5ee-eeb226809ee8",
      "name": "Wait \u2014 5s Processing Buffer1",
      "type": "n8n-nodes-base.wait",
      "position": [
        1024,
        2016
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 5
      },
      "typeVersion": 1
    },
    {
      "id": "96b03332-c1ba-4e97-8448-038836efdaa1",
      "name": "IG \u2014 Publish Container1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1344,
        2016
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v19.0/{{ $('Set \u2014 Normalize Fields1').item.json.ig_account_id }}/media_publish",
        "method": "POST",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "creation_id",
              "value": "={{ $('IG \u2014 Create Media Container1').item.json.id }}"
            },
            {
              "name": "access_token",
              "value": "={{ $credentials.instagramGraphApi.accessToken }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d1c572eb-4e70-4938-8daa-ec2e6bf8d2ec",
      "name": "Airtable \u2014 Log Post1",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1664,
        2016
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.AIRTABLE_BASE_ID }}"
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "IG Post Log"
        },
        "columns": {
          "value": {
            "Status": "Published",
            "Caption": "={{ $('Code \u2014 Build Caption1').item.json.final_caption }}",
            "Post ID": "={{ $('IG \u2014 Publish Container1').item.json.id }}",
            "Image URL": "={{ $('Code \u2014 Build Caption1').item.json.public_image_url }}",
            "Product Name": "={{ $('Set \u2014 Normalize Fields1').item.json.product_name }}",
            "Published At": "={{ new Date().toISOString() }}",
            "Trigger Source": "={{ $('Set \u2014 Normalize Fields1').item.json.trigger_source }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "create"
      },
      "typeVersion": 2.1
    },
    {
      "id": "b112898f-c983-4126-b2f1-21d420d2d1a8",
      "name": "Slack \u2014 Notify Team1",
      "type": "n8n-nodes-base.slack",
      "position": [
        2128,
        2016
      ],
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.SLACK_CHANNEL_ID }}"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.3
    },
    {
      "id": "ff9789d6-0c7a-4d4d-aa7a-43e1add05b8c",
      "name": "Upload a File",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        0,
        2016
      ],
      "parameters": {},
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Upload a File": {
      "main": [
        [
          {
            "node": "Code \u2014 Build Caption1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable \u2014 Log Post1": {
      "main": [
        [
          {
            "node": "Slack \u2014 Notify Team1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Build Caption1": {
      "main": [
        [
          {
            "node": "IG \u2014 Create Media Container1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IG \u2014 Publish Container1": {
      "main": [
        [
          {
            "node": "Airtable \u2014 Log Post1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set \u2014 Normalize Fields1": {
      "main": [
        [
          {
            "node": "HTTP \u2014 Fetch Product Image1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook \u2014 Product Trigger1": {
      "main": [
        [
          {
            "node": "Webhook Response1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Set \u2014 Normalize Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP \u2014 Fetch Product Image1": {
      "main": [
        [
          {
            "node": "Upload a File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IG \u2014 Create Media Container1": {
      "main": [
        [
          {
            "node": "Wait \u2014 5s Processing Buffer1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait \u2014 5s Processing Buffer1": {
      "main": [
        [
          {
            "node": "IG \u2014 Publish Container1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}