This workflow corresponds to n8n.io template #15958 — we link there as the canonical source.
This workflow follows the Editimage → 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Publish brand-compliant Instagram posts from Google Sheets using AI",
"tags": [],
"nodes": [
{
"id": "1be553c8-b1bd-4eb4-ac9b-aab09b574f1c",
"name": "Sticky \u2014 Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2528,
-1328
],
"parameters": {
"color": 1,
"width": 480,
"height": 896,
"content": "## Publish Brand-Compliant Instagram Posts from Google Sheets Using AI\n\nReads your content calendar daily, generates brand-locked captions and visuals using AI, composites your logo dynamically, and publishes straight to Instagram \u2014 all without touching a design tool.\n\n### How it works\n1. Triggers daily, fetches your logo and master brand context file, then reads today's pending posts from Google Sheets.\n2. Loops through each post in scheduled order, waiting until the exact post time before processing.\n3. Generates a brand-compliant caption and a 1024\u00d71024 AI image using your brand context. Calculates logo placement coordinates and composites your logo onto the final graphic.\n4. Uploads the image to AWS S3, creates an Instagram media container via the Graph API, and publishes it live.\n5. Writes the result back to your sheet \u2014 status, caption, image URL, or error message \u2014 then moves to the next post.\n\n### Setup\n1. Add credentials for Google Sheets (OAuth2), AWS IAM, Facebook Graph API, and OpenAI in n8n.\n2. Open **Set Workflow Config** and paste your logo URL, Google Sheet URL, master context URL, S3 bucket name, and Instagram Business ID.\n3. Host a raw text or .md file containing your brand rules and paste the direct link into `masterContextUrl`.\n4. Set your preferred posting time in **Daily Schedule Trigger**.\n5. Activate the workflow.\n\n### Customization\n- Swap models in **Generate Post Caption** and **Generate AI Image** to adjust quality and cost.\n- Adjust logo dimensions in **Resize Logo Image** (default 170\u00d7130) to match your brand scale.\n- Connect a Slack or Gmail node after **Format Error Alert** to get notified when a post fails."
},
"typeVersion": 1
},
{
"id": "c0b6fa24-078b-4bee-95b9-ad36a4f5f146",
"name": "Sticky \u2014 Trigger and Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1968,
-1040
],
"parameters": {
"color": 7,
"width": 1120,
"height": 272,
"content": "## Trigger and Initial Configuration\n\nLoads your brand config, fetches the logo, and downloads the master context file on a daily schedule."
},
"typeVersion": 1
},
{
"id": "3cd70849-914b-42d3-a257-e243878a3a2a",
"name": "Sticky \u2014 Post Retrieval",
"type": "n8n-nodes-base.stickyNote",
"position": [
-640,
-1168
],
"parameters": {
"color": 7,
"width": 960,
"height": 288,
"content": "## Post Retrieval and Filtering\n\nReads today's pending posts from your content calendar and stops cleanly if nothing is scheduled."
},
"typeVersion": 1
},
{
"id": "1d9256d9-89fc-4ee7-91c0-47443ebb51b4",
"name": "Sticky \u2014 Content Generation",
"type": "n8n-nodes-base.stickyNote",
"position": [
416,
-1280
],
"parameters": {
"color": 7,
"width": 1984,
"height": 400,
"content": "## Content Generation and Formatting\n\nGenerates captions and images using AI, then cleans and formats the data for further processing."
},
"typeVersion": 1
},
{
"id": "5f73a118-4f1b-488a-9b64-ecc49d0f415e",
"name": "Sticky \u2014 Image Hosting",
"type": "n8n-nodes-base.stickyNote",
"position": [
2464,
-1280
],
"parameters": {
"color": 7,
"width": 256,
"height": 352,
"content": "## Image Hosting and Preparation\n\nUploads the processed image to AWS S3 and prepares it for Instagram."
},
"typeVersion": 1
},
{
"id": "8a501ddc-8ad4-4984-a951-e076680c711e",
"name": "Sticky \u2014 Instagram Publishing",
"type": "n8n-nodes-base.stickyNote",
"position": [
2752,
-1312
],
"parameters": {
"color": 7,
"width": 384,
"height": 304,
"content": "## Instagram Publishing\n\nCreates a container on Instagram and publishes the post."
},
"typeVersion": 1
},
{
"id": "187babbf-0910-419a-a10f-938aaa8ce691",
"name": "Sticky \u2014 Status Update",
"type": "n8n-nodes-base.stickyNote",
"position": [
3184,
-1328
],
"parameters": {
"color": 7,
"width": 464,
"height": 832,
"content": "## Post Status Update\n\nUpdates the status of the post in Google Sheets as published or failed."
},
"typeVersion": 1
},
{
"id": "060cbe53-e385-4f34-8e08-8f7773d41545",
"name": "Sticky \u2014 Branching and Endings",
"type": "n8n-nodes-base.stickyNote",
"position": [
-640,
-832
],
"parameters": {
"color": 7,
"width": 1296,
"height": 688,
"content": "## Branching and Endings\n\nHandles different scenarios such as no posts found, all posts processed, or errors."
},
"typeVersion": 1
},
{
"id": "e06d22e2-fdee-4078-bd64-0f89b8fbbb03",
"name": "Daily Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Initiates the workflow automatically on a daily schedule.",
"position": [
-1920,
-928
],
"parameters": {
"rule": {
"interval": [
{
"daysInterval": 1,
"triggerAtMinute": 10
}
]
}
},
"typeVersion": 1.3
},
{
"id": "d37a3ac8-9397-4729-aa76-3874f245bfda",
"name": "Fetch Brand Context",
"type": "n8n-nodes-base.httpRequest",
"notes": "Downloads the master brand context file to feed instructions and restrictions to the AI prompts.",
"position": [
-1168,
-928
],
"parameters": {
"url": "={{ $('Set Workflow Config').item.json.masterContextUrl }}",
"options": {}
},
"typeVersion": 4.4
},
{
"id": "57ca72df-d996-458a-8700-f4465cb074a8",
"name": "If Context Exists",
"type": "n8n-nodes-base.if",
"notes": "Stops execution if the brand context URL returned no content.",
"position": [
-992,
-928
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "4aa62f0e-4920-4dec-b0a9-f998785b25c3",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.data }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "7f6df112-012a-400c-987b-a7914f923b6d",
"name": "Fetch Logo Image",
"type": "n8n-nodes-base.httpRequest",
"notes": "Fetches the raw company logo image file.",
"position": [
-1552,
-928
],
"parameters": {
"url": "={{ $json.logoUrl }}",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "logo"
}
}
}
},
"typeVersion": 4.4
},
{
"id": "06216e07-57d5-418c-b006-779bad28b307",
"name": "Resize Logo Image",
"type": "n8n-nodes-base.editImage",
"notes": "Resizes the company logo to fit within 170x130 while preserving its aspect ratio.",
"position": [
-1344,
-928
],
"parameters": {
"width": 170,
"height": 130,
"options": {},
"operation": "resize",
"dataPropertyName": "logo"
},
"typeVersion": 1
},
{
"id": "f932ee3f-aa4f-43b3-9561-88a88cc17906",
"name": "Read Instagram Posts",
"type": "n8n-nodes-base.googleSheets",
"notes": "Queries the content calendar to find any posts scheduled for today that are still marked as 'Pending'.",
"position": [
-592,
-1040
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "={{ $today.format('yyyy-MM-dd') }}",
"lookupColumn": "Date"
},
{
"lookupValue": "Pending",
"lookupColumn": "Status"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "={{ $('Set Workflow Config').item.json.googleSheetUrl }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "87eb9e8b-1daf-44b8-b039-cb6760e73897",
"name": "If Posts Pending",
"type": "n8n-nodes-base.if",
"notes": "Routes the workflow forward if there are pending posts, or halts if the list is empty.",
"position": [
-336,
-1040
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b10726a7-d181-4b91-83e0-6daa2f05ff1b",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1d6d9a66-232c-4da0-a183-a351ef1dba4e",
"name": "Sort Posts by Time",
"type": "n8n-nodes-base.sort",
"notes": "Orders the pending posts chronologically based on their scheduled execution time.",
"position": [
-80,
-1056
],
"parameters": {
"options": {},
"sortFieldsUi": {
"sortField": [
{
"fieldName": "Time"
}
]
}
},
"typeVersion": 1
},
{
"id": "b79c12ba-87e1-40cb-a9d9-378958522166",
"name": "Loop Each Post",
"type": "n8n-nodes-base.splitInBatches",
"notes": "Iterates through the sorted list of pending posts one by one.",
"position": [
176,
-1056
],
"parameters": {
"options": {}
},
"typeVersion": 3,
"alwaysOutputData": false
},
{
"id": "38341ce2-d876-46b1-a4e3-92caf9decd82",
"name": "Wait for Post Time",
"type": "n8n-nodes-base.wait",
"notes": "Pauses the loop execution until the exact scheduled date and time for the current post.",
"position": [
464,
-1040
],
"parameters": {
"resume": "specificTime",
"dateTime": "={{ $now.set({\n year: Number($json.Date.split('-')[0]),\n month: Number($json.Date.split('-')[1]),\n day: Number($json.Date.split('-')[2]),\n hour: Number($json.Time.split(':')[0]),\n minute: Number($json.Time.split(':')[1]),\n second: 0\n}) }}"
},
"typeVersion": 1.1
},
{
"id": "ae3a9759-622b-410a-a689-c37d7cf8334e",
"name": "Format Caption Data",
"type": "n8n-nodes-base.code",
"notes": "Parses the raw JSON string output from the LLM into structured data for downstream nodes.",
"position": [
1040,
-1104
],
"parameters": {
"jsCode": "const raw = $input.first().json.message.content;\n\nconst cleaned = raw\n .replace(/^\\uFEFF/, '')\n .replace(/```json\\s*/gi, '')\n .replace(/```\\s*/g, '')\n .trim();\n\n// Fix unescaped control characters inside JSON string values\nconst sanitized = cleaned.replace(\n /\"caption\"\\s*:\\s*\"([\\s\\S]*?)(?<!\\\\)\"/,\n (_, captionValue) => {\n const fixed = captionValue\n .replace(/\\r\\n/g, '\\\\n') // Windows line breaks\n .replace(/\\r/g, '\\\\n') // old Mac line breaks\n .replace(/\\n/g, '\\\\n') // Unix line breaks\n .replace(/\\t/g, '\\\\t'); // tabs\n return `\"caption\":\"${fixed}\"`;\n }\n);\n\nlet data;\ntry {\n data = JSON.parse(sanitized);\n} catch (e) {\n const match = sanitized.match(/\\{[\\s\\S]*\\}/);\n if (match) {\n data = JSON.parse(match[0].replace(/[\\n\\r\\t]/g, ' '));\n } else {\n throw new Error(`Failed to parse JSON: ${e.message} | Raw: ${cleaned}`);\n }\n}\n\nreturn [{ json: data }];"
},
"typeVersion": 2
},
{
"id": "442d157b-b177-4025-9fc0-2f40b2a0c7db",
"name": "Calculate Logo Position",
"type": "n8n-nodes-base.code",
"notes": "Calculates the dynamic X/Y placement coordinates for the logo based on the AI's layout decision.",
"position": [
1248,
-1120
],
"parameters": {
"jsCode": "const data = $input.first().json;\nconst canvasWidth = 1024;\nconst margin = 20;\n\n// Max area resize guarantees logo fits within 170x130\n// Use 170 as safe max width for position calculation\nconst logoWidth = 170;\nconst yPos = margin;\n\nlet xPos;\nconst position = data.logo_position?.trim() ?? \"Top Left\";\n\nif (position === \"Top Left\") {\n xPos = margin;\n} else if (position === \"Top Right\") {\n xPos = canvasWidth - logoWidth - margin;\n} else {\n xPos = Math.round((canvasWidth - logoWidth) / 2);\n}\n\nreturn { json: {\n x_position: xPos,\n y_position: yPos\n}};"
},
"typeVersion": 2
},
{
"id": "bf873312-4f8e-4b85-9b10-87d6995e2075",
"name": "Add Logo to Post",
"type": "n8n-nodes-base.editImage",
"notes": "Composites the resized brand logo onto the AI-generated image at the calculated coordinates.",
"position": [
2256,
-1104
],
"parameters": {
"options": {
"format": "png",
"destinationKey": "data"
},
"operation": "composite",
"positionX": "={{ $('Calculate Logo Position').item.json.x_position }}",
"positionY": "={{ $('Calculate Logo Position').item.json.y_position }}",
"dataPropertyNameComposite": "logo"
},
"typeVersion": 1
},
{
"id": "4daa47e6-181f-4f6d-97fb-37a4efb1192e",
"name": "Create Instagram Container",
"type": "n8n-nodes-base.facebookGraphApi",
"notes": "Drafts the Instagram post via the Graph API using body parameters \u2014 required by Meta to correctly handle captions with emojis, hashtags, and line breaks.",
"position": [
2800,
-1184
],
"parameters": {
"edge": "media",
"node": "={{ $('Set Workflow Config').item.json.instagramBusinessId }}",
"options": {
"queryParameters": {
"parameter": [
{
"name": "caption",
"value": "={{ $('Format Caption Data').item.json.caption }}"
},
{
"name": "image_url",
"value": "={{ $('Upload Image to S3').item.json.Location }}"
},
{
"name": "media_type",
"value": "IMAGE"
}
]
}
},
"graphApiVersion": "v23.0",
"httpRequestMethod": "POST"
},
"credentials": {
"facebookGraphApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "1d8ecd2d-b1ae-44f6-bf72-c6ae3017b015",
"name": "Upload Image to S3",
"type": "n8n-nodes-base.awsS3",
"notes": "Uploads the finalized composited image to AWS S3 to generate a public URL required by the Instagram API.",
"onError": "continueErrorOutput",
"position": [
2512,
-1088
],
"parameters": {
"fileName": "={{ $('Loop Each Post').item.json.Topic.replaceAll(\" \", \"-\")}}{{ $now.format('yyyy-MM-dd-HH-mm-ss') }}.png",
"operation": "upload",
"bucketName": "={{ $('Set Workflow Config').item.json.awsS3BucketName }}",
"additionalFields": {
"acl": "publicRead",
"grantRead": false
}
},
"credentials": {
"aws": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "056024af-6a37-44e2-a82c-de40877eafe5",
"name": "Publish Post to Instagram",
"type": "n8n-nodes-base.facebookGraphApi",
"notes": "Triggers the final publish action for the created media container, pushing it live to the Instagram feed.",
"onError": "continueErrorOutput",
"position": [
2992,
-1184
],
"parameters": {
"edge": "media_publish",
"node": "={{ $('Set Workflow Config').item.json.instagramBusinessId }}",
"options": {
"queryParameters": {
"parameter": [
{
"name": "creation_id",
"value": "={{ $json.id }}"
}
]
}
},
"graphApiVersion": "v23.0",
"httpRequestMethod": "POST"
},
"credentials": {
"facebookGraphApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "2146a908-7379-48c2-9506-63cc2f432984",
"name": "Update Post Status in Sheets",
"type": "n8n-nodes-base.googleSheets",
"notes": "Logs the post execution results back to the Google Sheet, including status, timestamp, final caption, and image URL.",
"position": [
3504,
-672
],
"parameters": {
"columns": {
"value": {
"Status": "={{ $json.postStatus ?? 'Failed' }}",
"Post Image": "={{ $json.postImage ?? '' }}",
"row_number": "={{ $('Loop Each Post').item.json.row_number }}",
"Post Caption": "={{ $json.postCaption ?? '' }}",
"Publish Time": "={{ $now.format('yyyy-MM-dd HH:mm:ss') }}",
"Error Message": "={{ $json.publishError || $('Upload Image to S3').first()?.error?.message || $('Generate AI Image').first()?.error?.message || $('Generate Post Caption').first()?.error?.message || '' }}"
},
"schema": [
{
"id": "Topic",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Topic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Context",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Context",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Date",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Time",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Publish Time",
"type": "string",
"display": true,
"required": false,
"displayName": "Publish Time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Post Image",
"type": "string",
"display": true,
"required": false,
"displayName": "Post Image",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Post Caption",
"type": "string",
"display": true,
"required": false,
"displayName": "Post Caption",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": false,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Error Message",
"type": "string",
"display": true,
"required": false,
"displayName": "Error Message",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"row_number"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "={{ $('Set Workflow Config').item.json.googleSheetUrl }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "eddd9c2a-ec18-44bb-b5b7-cbc904379bb0",
"name": "Map Post and Logo Data",
"type": "n8n-nodes-base.set",
"notes": "Combines the downloaded AI image and the resized company logo into a single item for the composite node.",
"position": [
2048,
-1136
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "028204b1-ae77-455d-85c1-e83d76e1cd25",
"name": "data",
"type": "binary",
"value": "={{ $binary.data }}"
},
{
"id": "b43761d5-69f5-4225-809e-2a0c993b7213",
"name": "logo",
"type": "binary",
"value": "={{ $('Resize Logo Image').item.binary.logo }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "af6a3ed5-a4a8-4526-aab8-9ad5fcb434d8",
"name": "Handle No Posts Found",
"type": "n8n-nodes-base.set",
"notes": "Fallback path that gracefully stops execution and logs a message if there's no pending content.",
"position": [
48,
-736
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "b5c02c58-5572-4616-97a7-484dddd9bffb",
"name": "noPosts",
"type": "string",
"value": "No posts scheduled today"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9d9a5e47-d1c7-4023-9e28-2b6c12f7d2f4",
"name": "Error Trigger",
"type": "n8n-nodes-base.errorTrigger",
"position": [
-544,
-336
],
"parameters": {},
"typeVersion": 1
},
{
"id": "0ff769ae-ef3c-4e9c-b4ec-867f81a2864f",
"name": "Format Error Alert",
"type": "n8n-nodes-base.set",
"notes": "Connect a Slack, Gmail, or Teams node here to receive error notifications.",
"position": [
-336,
-336
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "error-msg-format",
"name": "alertMessage",
"type": "string",
"value": "=\ud83d\udea8 Workflow Failed | Node: {{ $json.execution.lastNodeExecuted }} | Error: {{ $json.error.message }}\n\n(Route this to Slack, Teams, or Email)_"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "1e3763ac-e489-4568-93d9-1351a077a33a",
"name": "Handle Missing Context",
"type": "n8n-nodes-base.set",
"notes": "Fires when masterContextUrl returns no content. Halts execution with a clear diagnostic message.",
"position": [
-448,
-704
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cme-001",
"name": "error",
"type": "string",
"value": "Brand context URL returned empty or unreachable \u2014 check masterContextUrl in Set Config."
}
]
}
},
"typeVersion": 3.4
},
{
"id": "4889e5e9-0886-42e4-8e42-3d918fe5525d",
"name": "Handle All Posts Processed",
"type": "n8n-nodes-base.set",
"notes": "Terminal node for Loop out0. Provides a clean completion signal in the execution log.",
"position": [
336,
-704
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "apd-001",
"name": "batchComplete",
"type": "string",
"value": "All scheduled posts for today have been processed."
}
]
}
},
"typeVersion": 3.4
},
{
"id": "8aad9c20-df5d-49f0-b89f-9a7972b99c16",
"name": "Set Published Status",
"type": "n8n-nodes-base.set",
"notes": "Stamps postStatus: Published before updating the Google Sheet.",
"position": [
3232,
-1200
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "tap-001",
"name": "postStatus",
"type": "string",
"value": "Published"
},
{
"id": "tap-002",
"name": "postImage",
"type": "string",
"value": "={{ $('Upload Image to S3').item.json?.Location ?? '' }}"
},
{
"id": "tap-003",
"name": "postCaption",
"type": "string",
"value": "={{ $('Format Caption Data').item.json?.caption ?? '' }}"
},
{
"id": "tap-004",
"name": "publishError",
"type": "string",
"value": ""
}
]
}
},
"typeVersion": 3.4
},
{
"id": "249c8022-999e-4b05-9234-ff5f661eaa2b",
"name": "Set Failed Status",
"type": "n8n-nodes-base.set",
"notes": "Captures the Publish to Instagram error and stamps postStatus: Failed before updating the Google Sheet.",
"position": [
3248,
-1008
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "taf-001",
"name": "postStatus",
"type": "string",
"value": "Failed"
},
{
"id": "taf-002",
"name": "postImage",
"type": "string",
"value": "={{ $('Upload Image to S3').item.json?.Location ?? '' }}"
},
{
"id": "taf-003",
"name": "postCaption",
"type": "string",
"value": "={{ $('Format Caption Data').item.json?.caption ?? '' }}"
},
{
"id": "taf-004",
"name": "publishError",
"type": "string",
"value": "={{ $json.error?.message ?? 'Instagram publish call failed \u2014 check Graph API token and permissions.' }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "00ab385b-5d71-455e-8f5d-a7dab99e3ef3",
"name": "Set Workflow Config",
"type": "n8n-nodes-base.set",
"notes": "Initializes global configuration variables, including URLs for the logo, target Google Sheet, and brand master context.",
"position": [
-1728,
-928
],
"parameters": {
"fields": {
"values": [
{
"name": "logoUrl",
"stringValue": "PASTE_YOUR_LOGO_URL_HERE"
},
{
"name": "googleSheetUrl",
"stringValue": "PASTE_YOUR_GOOGLE_SHEET_URL_HERE"
},
{
"name": "masterContextUrl",
"stringValue": "PASTE_YOUR_MASTER_CONTEXT_URL_HERE"
},
{
"name": "awsS3BucketName",
"stringValue": "PASTE_YOUR_AWS_BUCKET_NAME_HERE"
},
{
"name": "instagramBusinessId",
"stringValue": "PASTE_YOUR_INSTAGRAM_ID_HERE"
}
]
},
"options": {}
},
"typeVersion": 3.4
},
{
"id": "b7667f24-209a-4ce3-8ca2-75785383a1ac",
"name": "Generate Post Caption",
"type": "@n8n/n8n-nodes-langchain.openAi",
"onError": "continueErrorOutput",
"position": [
672,
-1040
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {
"maxTokens": 4256,
"textFormat": {
"textOptions": {
"type": "json_schema",
"schema": "{\n \"type\": \"object\",\n \"properties\": {\n \"category_letter\": {\n \"type\": \"string\",\n \"description\": \"Matched category letter (A / B / C / D / E)\"\n },\n \"category_name\": {\n \"type\": \"string\"\n },\n \"logo_position\": {\n \"type\": \"string\",\n \"description\": \"Top Left / Top Right / Top Center\"\n },\n \"caption\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"category_letter\",\n \"category_name\",\n \"logo_position\",\n \"caption\"\n ],\n \"additionalProperties\": false\n}",
"strict": true,
"description": "An Instagram post configuration containing the matched brand category, layout instructions, and the final optimized caption."
}
},
"temperature": 0.8
},
"responses": {
"values": [
{
"content": "=BRAND CONTEXT:\n{{ $('Fetch Brand Context').first().json?.data }}\n\nTOPIC: {{ $('Loop Each Post').item.json.Topic }}\nCONTEXT: {{ $('Loop Each Post').item.json.Context }}"
},
{
"role": "system",
"content": "You are a professional Instagram content writer. Follow the brand voice, tone, and industry context defined in the BRAND CONTEXT below.\n\nCAPTION STRUCTURE (strictly follow this order, separate each section with \\\\n\\\\n): \n1. HOOK \u2014 one punchy opening line with 1 emoji \n2. BODY \u2014 1\u20132 lines of value or detail with 1\u20132 emojis \n3. CTA \u2014 one action-oriented closing line aligned with the brand\u2019s call-to-action style \n4. HASHTAGS \u2014 5\u20137 hashtags on the last line \n\nCAPTION RULES: \n- Write in the language defined in the brand context (default: English) \n- 3\u20135 emojis max across the entire caption, placed naturally \n- Tone: follow the brand voice rules defined in the brand context \n- Follow all brand voice rules and avoid all prohibited phrases from brand context \n- The caption value must use the escaped sequence \\\\n (backslash + n), never a real line break character"
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3,
"alwaysOutputData": false
},
{
"id": "a9181a70-b9dd-4d41-b017-b6b7cbc27cf0",
"name": "Generate AI Image",
"type": "@n8n/n8n-nodes-langchain.openAi",
"notes": "Prompts OpenAI's image generation model to create a 1024x1024 visual graphic matching the strict brand context and assigned layout.",
"onError": "continueErrorOutput",
"position": [
1440,
-1136
],
"parameters": {
"prompt": "=You are a professional Instagram graphic designer. Follow all brand rules below strictly and without exception. These rules override any creative judgment.\n\nDesign a premium Instagram post using the full brand context provided below.\n\nBRAND CONTEXT:\n{{ $('Fetch Brand Context').first().json.data }}\n\nCAMPAIGN DETAILS:\nTopic: {{ $('Loop Each Post').item.json.Topic }}\nContext / Objective: {{ $('Loop Each Post').item.json.Context }}\n\nCLASSIFIED CATEGORY (already determined \u2014 do not reclassify):\nCategory: {{ $('Format Caption Data').item.json.category_letter }} \u2014 {{ $('Format Caption Data').item.json.category_name }}\n\nINSTRUCTIONS:\nStep 1: Read the topic and context carefully.\nStep 2: The category has already been classified. Use ONLY the layout defined for Category {{ $('Format Caption Data').item.json.category_letter }} \u2014 {{ $('Format Caption Data').item.json.category_name }} from the brand context. Do not reclassify.\nStep 3: Apply that category's layout strictly. Do not use any other layout.\nStep 4: Follow all global brand rules strictly \u2014 colors, fonts, post style, tone, and language.\nStep 5: Place the logo placeholder exactly at {{ $('Format Caption Data').item.json.logo_position }} as instructed in the category layout.\n\nOUTPUT REQUIREMENTS:\n- Premium, realistic, high-quality visual\n- Soft lighting, clean spacing, polished composition\n- Strictly follow Category {{ $('Format Caption Data').item.json.category_letter }} layout only\n- Do not add any logo from your side\n- Do not add any placeholder box, dotted border, dashed border, rounded rectangle, or any container marking the logo area\n- Leave the logo safe zone as plain empty background only\n- Do not mix layouts from different categories\n- Do not use generic stock ad design\n- The main canvas background must always remain white",
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-image-2",
"cachedResultName": "GPT-IMAGE-2"
},
"options": {
"size": "1024x1024",
"quality": "high"
},
"resource": "image"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "4aeaf417-4332-4a81-8f08-8a6fdf7d0d57",
"name": "Download AI Image",
"type": "n8n-nodes-base.httpRequest",
"notes": "Downloads the finalized image asset returned by OpenAI so it can be manipulated locally.",
"position": [
1744,
-1184
],
"parameters": {
"url": "={{ $json.url }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"typeVersion": 4.4
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"connections": {
"Error Trigger": {
"main": [
[
{
"node": "Format Error Alert",
"type": "main",
"index": 0
}
]
]
},
"Loop Each Post": {
"main": [
[
{
"node": "Handle All Posts Processed",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait for Post Time",
"type": "main",
"index": 0
}
]
]
},
"Add Logo to Post": {
"main": [
[
{
"node": "Upload Image to S3",
"type": "main",
"index": 0
}
]
]
},
"Fetch Logo Image": {
"main": [
[
{
"node": "Resize Logo Image",
"type": "main",
"index": 0
}
]
]
},
"If Posts Pending": {
"main": [
[
{
"node": "Sort Posts by Time",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle No Posts Found",
"type": "main",
"index": 0
}
]
]
},
"Download AI Image": {
"main": [
[
{
"node": "Map Post and Logo Data",
"type": "main",
"index": 0
}
]
]
},
"Generate AI Image": {
"main": [
[
{
"node": "Download AI Image",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Post Status in Sheets",
"type": "main",
"index": 0
}
]
]
},
"If Context Exists": {
"main": [
[
{
"node": "Read Instagram Posts",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Missing Context",
"type": "main",
"index": 0
}
]
]
},
"Resize Logo Image": {
"main": [
[
{
"node": "Fetch Brand Context",
"type": "main",
"index": 0
}
]
]
},
"Set Failed Status": {
"main": [
[
{
"node": "Update Post Status in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Sort Posts by Time": {
"main": [
[
{
"node": "Loop Each Post",
"type": "main",
"index": 0
}
]
]
},
"Upload Image to S3": {
"main": [
[
{
"node": "Create Instagram Container",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Post Status in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Wait for Post Time": {
"main": [
[
{
"node": "Generate Post Caption",
"type": "main",
"index": 0
}
]
]
},
"Fetch Brand Context": {
"main": [
[
{
"node": "If Context Exists",
"type": "main",
"index": 0
}
]
]
},
"Format Caption Data": {
"main": [
[
{
"node": "Calculate Logo Position",
"type": "main",
"index": 0
}
]
]
},
"Set Workflow Config": {
"main": [
[
{
"node": "Fetch Logo Image",
"type": "main",
"index": 0
}
]
]
},
"Read Instagram Posts": {
"main": [
[
{
"node": "If Posts Pending",
"type": "main",
"index": 0
}
]
]
},
"Set Published Status": {
"main": [
[
{
"node": "Update Post Status in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Generate Post Caption": {
"main": [
[
{
"node": "Format Caption Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Post Status in Sheets",
"type": "main",
"index": 0
}
]
]
},
"Daily Schedule Trigger": {
"main": [
[
{
"node": "Set Workflow Config",
"type": "main",
"index": 0
}
]
]
},
"Map Post and Logo Data": {
"main": [
[
{
"node": "Add Logo to Post",
"type": "main",
"index": 0
}
]
]
},
"Calculate Logo Position": {
"main": [
[
{
"node": "Generate AI Image",
"type": "main",
"index": 0
}
]
]
},
"Publish Post to Instagram": {
"main": [
[
{
"node": "Set Published Status",
"type": "main",
"index": 0
}
],
[
{
"node": "Set Failed Status",
"type": "main",
"index": 0
}
]
]
},
"Create Instagram Container": {
"main": [
[
{
"node": "Publish Post to Instagram",
"type": "main",
"index": 0
}
]
]
},
"Update Post Status in Sheets": {
"main": [
[
{
"node": "Loop Each Post",
"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.
awsfacebookGraphApigoogleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily to read a Google Sheets content calendar, use OpenAI to generate brand-compliant Instagram captions and images from a master brand context file, watermark each image with your logo, publish posts via the Facebook Graph API, and write publishing results…
Source: https://n8n.io/workflows/15958/ — 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.
This workflow automates trend extraction and social media content creation for businesses and marketers. It eliminates manual trend research and content generation by fetching trends, scoring them wit
Managing content for multiple social media platforms manually is time-consuming and error-prone. This workflow automates content creation, image generation, approval flows, and publishing for LinkedIn
Instead of manually writing, designing, and posting content, this workflow turns a single Google Sheet row into multi-platform posts plus a custom AI image that matches your message.
This comprehensive n8n workflow automatically transforms trending Google search queries into engaging LinkedIn posts using AI. The system runs autonomously, discovering viral topics, researching conte
This workflow is ideal for individuals, marketers, agencies, and brands who want to effortlessly automate the entire blogging and social media process—from idea generation to promotion. Its primary go