This workflow corresponds to n8n.io template #14629 — 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "ddbe3e51-4161-460d-99a1-a6e483e0d881",
"name": "Upload to URL",
"type": "n8n-nodes-uploadtourl.uploadToUrl",
"position": [
8288,
3424
],
"parameters": {
"binaryPropertyName": "cardImage"
},
"typeVersion": 1
},
{
"id": "c9d602f5-fa62-4ceb-867c-c43e44731ae3",
"name": "Template Overview1",
"type": "n8n-nodes-base.stickyNote",
"position": [
5728,
2384
],
"parameters": {
"width": 480,
"height": 800,
"content": "## Branded Social Proof Automation\n### Bannerbear + Upload to URL Instagram\n\n**What this does:**\nDaily schedule fetches the oldest unposted 5-star review from Airtable, generates a branded quote card via Bannerbear, uploads it via Upload to URL to get a public CDN link, then publishes to Instagram. Marks the review as posted and notifies Slack.\n\n**Flow:**\n1. Schedule -- daily at 10 AM\n2. Airtable -- fetch oldest unposted 5 review\n3. IF -- skip gracefully if none found\n4. Code -- build Bannerbear payload + IG caption\n5. HTTP -- Bannerbear: submit image generation job\n6. Wait 5s -- initial render buffer\n7. HTTP -- Bannerbear: poll for completed status\n8. IF -- completed vs still pending\n9. HTTP -- fetch rendered image as binary\n10. Upload to URL -- upload binary, get public CDN URL\n11. Code -- merge CDN URL + caption fields\n12. HTTP -- IG: create media container\n13. Wait 6s -- IG processing buffer\n14. HTTP -- IG: publish container\n15. Airtable -- mark review as posted\n16. Slack -- notify team\n\n**Required env vars:**\n'IG_USER_ID' 'IG_ACCESS_TOKEN'\n'BANNERBEAR_API_KEY' 'BANNERBEAR_TEMPLATE_ID'\n'AIRTABLE_BASE_ID' 'SLACK_CHANNEL_ID'\n\n**Airtable Reviews table fields:**\n'Reviewer Name' . 'Review Text' . 'Rating'\n'Posted' (checkbox) . 'Submitted At' (date)\n'Instagram Post ID' . 'Posted At' . 'Card Image URL'\n\n**Bannerbear template layer names:**\n'reviewer_name' . 'review_text' . 'star_label'"
},
"typeVersion": 1
},
{
"id": "cdf0a485-7827-4883-a7ba-0941a8694c60",
"name": "Sticky Nodes 1-",
"type": "n8n-nodes-base.stickyNote",
"position": [
6320,
3200
],
"parameters": {
"color": 7,
"width": 560,
"height": 380,
"content": "### Nodes 1-3: Schedule, Fetch & Gate\n\n**Schedule Trigger** fires daily at 10 AM.\n\n**Airtable -- Fetch Review** uses 'filterByFormula' to retrieve only the oldest unposted 5-star record. 'AND({Rating}=5, {Posted}=FALSE())' + sort by 'Submitted At asc' + 'maxRecords: 1' ensures FIFO queue dispatch.\n\n**IF -- Has Valid Review?** exits cleanly on false branch if no records are found."
},
"typeVersion": 1
},
{
"id": "d26ae5a7-904b-4468-bf14-e4d9df09508f",
"name": "Sticky Nodes 4-",
"type": "n8n-nodes-base.stickyNote",
"position": [
6960,
3232
],
"parameters": {
"color": 7,
"width": 408,
"height": 340,
"content": "### Nodes 4-5: Build Payload & Generate Image\n\n**Code -- Prepare Payload** formats review data into Bannerbear modifications array, truncates card text to 180 chars, and builds the full Instagram caption (max 2200 chars).\n\n**HTTP -- Bannerbear Create Job** POSTs to '/v2/images'. Returns a 'uid' for polling. Generation is async -- initial response is 'pending'."
},
"typeVersion": 1
},
{
"id": "78dc78cc-4973-435c-92b9-9fe61abb279a",
"name": "Sticky Nodes 6-",
"type": "n8n-nodes-base.stickyNote",
"position": [
7408,
3200
],
"parameters": {
"color": 7,
"width": 500,
"height": 380,
"content": "### Nodes 6-8: Poll Loop\n\n**Wait 5s** -- initial render buffer.\n\n**HTTP -- Poll Status** calls 'GET /v2/images/{uid}'. Returns 'status: pending' or 'status: completed'.\n\n**IF -- Image Ready?** branches on 'completed'. False branch exits -- extend with a loop back to Wait for full retry logic.\n\nWhen completed, 'image_url' is the rendered PNG CDN link."
},
"typeVersion": 1
},
{
"id": "05441a5f-7a54-4e80-81d0-5b63feb34c98",
"name": "Sticky Nodes 9-",
"type": "n8n-nodes-base.stickyNote",
"position": [
8032,
3184
],
"parameters": {
"color": 7,
"width": 540,
"height": 420,
"content": "### Nodes 9-11: Fetch Binary Upload to URL Merge\n\n**HTTP Fetch Rendered Card** downloads the Bannerbear image as a binary ('responseFormat: file', stored in 'cardImage').\n\n**Upload to URL** -- the mandatory CDN bridge. Instagram's API requires a direct public HTTPS image URL; it rejects base64 and binary. This node uploads the binary and returns a 'public_url'.\n\n**Code -- Merge** combines the CDN URL with caption and metadata from the earlier Code node."
},
"typeVersion": 1
},
{
"id": "b1a9d493-79ac-46d2-8c66-1ab34a00d87c",
"name": "Sticky Nodes 12-",
"type": "n8n-nodes-base.stickyNote",
"position": [
8720,
3152
],
"parameters": {
"color": 7,
"width": 1068,
"height": 440,
"content": "### Nodes 12-16: Publish, Log & Notify\n\n**IG Create Media Container** POSTs to '/media' with CDN URL + caption. Returns 'container_id'.\n\n**Wait 6s** -- IG processing buffer.\n\n**IG Publish Container** calls '/media_publish' with 'creation_id'. Returns live Post ID.\n\n**Airtable -- Mark as Posted** updates record: 'Posted=true', Post ID, timestamp, card URL. Prevents re-posting.\n\n**Slack -- Notify Team** sends reviewer name, review snippet, Post ID, and card URL."
},
"typeVersion": 1
},
{
"id": "682ccf50-9530-4130-9c32-4265737b0f1b",
"name": "Schedule Trigger1",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
6352,
3424
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 10
}
]
}
},
"typeVersion": 1.1
},
{
"id": "67376d29-bd02-478a-94e8-8e524264d847",
"name": "Airtable Fetch Review1",
"type": "n8n-nodes-base.airtable",
"position": [
6528,
3424
],
"parameters": {
"base": {
"__rl": true,
"mode": "id",
"value": "={{ $env.AIRTABLE_BASE_ID }}"
},
"table": {
"__rl": true,
"mode": "name",
"value": "Reviews"
},
"operation": "list"
},
"typeVersion": 2
},
{
"id": "74751a43-093e-4ec5-bad5-e51d981bcfd7",
"name": "IF Has Valid Review1",
"type": "n8n-nodes-base.if",
"position": [
6752,
3424
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-review",
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json.id }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "c6ac9392-9df0-41c3-babf-2abb3004fbe7",
"name": "Code Prepare Bannerbear Payload1",
"type": "n8n-nodes-base.code",
"position": [
7008,
3424
],
"parameters": {
"jsCode": "// Prepare Bannerbear payload + IG caption \nconst review = $input.first().json;\nconst fields = review.fields || review;\n\nconst reviewerName = fields['Reviewer Name'] || 'A Happy Customer';\nconst reviewText = fields['Review Text'] || '';\nconst rating = Number(fields['Rating'] || 5);\nconst recordId = review.id;\n\n// Truncate review text for card layer (180 char max)\nconst cardText = reviewText.length > 180\n ? reviewText.substring(0, 177) + '...'\n : reviewText;\n\n// Stars string for card layer\nconst stars = ''.repeat(Math.min(rating, 5));\n\n// Bannerbear modifications layer names must match your template\nconst modifications = [\n { name: 'reviewer_name', text: ' ' + reviewerName },\n { name: 'review_text', text: '\"' + cardText + '\"' },\n { name: 'star_label', text: stars },\n];\n\n// Full Instagram caption (2200 char limit)\nconst captionReview = reviewText.length > 300\n ? reviewText.substring(0, 297) + '...'\n : reviewText;\n\nconst hashtagBlock = '#customerreview #5stars #testimonial #socialproof #happycustomer #review #customerexperience';\n\nlet finalCaption = [\n stars + ' Real words from a real customer.',\n '',\n '\"' + captionReview + '\"',\n ' ' + reviewerName,\n '',\n ' See why hundreds of customers trust us.',\n ' Link in bio.',\n '',\n hashtagBlock,\n].join('\\n').trim();\n\nif (finalCaption.length > 2200) {\n finalCaption = finalCaption.substring(0, 2196) + '...';\n}\n\nconsole.log('[Payload] Reviewer:', reviewerName, '| Record:', recordId);\n\nreturn [{\n json: {\n record_id: recordId,\n reviewer_name: reviewerName,\n review_text: reviewText,\n card_text: cardText,\n stars: stars,\n modifications: modifications,\n final_caption: finalCaption,\n ig_user_id: $env.IG_USER_ID,\n bannerbear_template_id: $env.BANNERBEAR_TEMPLATE_ID,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4770b12d-463e-49b1-b90f-495d66f4e105",
"name": "HTTP Bannerbear Create Job1",
"type": "n8n-nodes-base.httpRequest",
"position": [
7232,
3424
],
"parameters": {
"url": "https://api.bannerbear.com/v2/images",
"body": "={{ JSON.stringify({ template: $json.bannerbear_template_id, modifications: $json.modifications }) }}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"rawContentType": "application/json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.BANNERBEAR_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "861f9015-8ce5-45aa-a5cb-5c00b8502552",
"name": "Wait Before Poll1",
"type": "n8n-nodes-base.wait",
"position": [
7440,
3424
],
"parameters": {
"unit": "seconds",
"amount": 5
},
"typeVersion": 1
},
{
"id": "1cb0e82b-e0cb-4d7a-8ca2-14023323a997",
"name": "HTTP Bannerbear Poll Status1",
"type": "n8n-nodes-base.httpRequest",
"position": [
7600,
3424
],
"parameters": {
"url": "=https://api.bannerbear.com/v2/images/{{ $('HTTP Bannerbear Create Job1').first().json.uid }}",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.BANNERBEAR_API_KEY }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "d0edef0c-82cb-4abc-97e5-399940d387b1",
"name": "IF Image Ready1",
"type": "n8n-nodes-base.if",
"position": [
7792,
3424
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-image-ready",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "completed"
}
]
}
},
"typeVersion": 2
},
{
"id": "2b1a94a8-57d9-47a9-8c30-65cc33059a66",
"name": "HTTP Fetch Rendered Card1",
"type": "n8n-nodes-base.httpRequest",
"position": [
8096,
3424
],
"parameters": {
"url": "={{ $json.image_url }}",
"options": {
"response": {
"response": {
"responseFormat": "file",
"outputPropertyName": "cardImage"
}
}
}
},
"typeVersion": 4.1
},
{
"id": "67a627e7-1f27-4c92-82e6-d101da62b3a6",
"name": "Code Merge Upload and Caption1",
"type": "n8n-nodes-base.code",
"position": [
8448,
3424
],
"parameters": {
"jsCode": "// Merge CDN URL from Upload to URL + caption from Code node \nconst uploadResult = $input.first().json;\nconst prepData = $('Code Prepare Bannerbear Payload1').first().json;\n\n// Upload to URL returns the public URL under one of these field names\nconst publicImageUrl =\n uploadResult.public_url ||\n uploadResult.url ||\n uploadResult.file_url ||\n uploadResult.cdn_url ||\n uploadResult.imageUrl ||\n '';\n\nif (!publicImageUrl) {\n throw new Error(\n 'Upload to URL returned no public URL. Available fields: ' +\n JSON.stringify(Object.keys(uploadResult))\n );\n}\n\nconsole.log('[Merge] CDN URL:', publicImageUrl);\n\nreturn [{\n json: {\n public_image_url: publicImageUrl,\n final_caption: prepData.final_caption,\n ig_user_id: prepData.ig_user_id,\n record_id: prepData.record_id,\n reviewer_name: prepData.reviewer_name,\n review_text: prepData.review_text,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4a29c68e-c112-4e18-b6dc-ef91dd6442b8",
"name": "IG Create Media Container1",
"type": "n8n-nodes-base.httpRequest",
"position": [
8768,
3424
],
"parameters": {
"url": "=https://graph.facebook.com/v19.0/{{ $json.ig_user_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": "={{ $env.IG_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "82c6802c-24f9-4527-b60c-c6a9527bd24b",
"name": "Wait IG Buffer1",
"type": "n8n-nodes-base.wait",
"position": [
8992,
3424
],
"parameters": {
"unit": "seconds",
"amount": 6
},
"typeVersion": 1
},
{
"id": "a6ab9a7b-8e90-4ab9-be81-4d030915d784",
"name": "IG Publish Container1",
"type": "n8n-nodes-base.httpRequest",
"position": [
9200,
3424
],
"parameters": {
"url": "=https://graph.facebook.com/v19.0/{{ $('Code Merge Upload and Caption1').first().json.ig_user_id }}/media_publish",
"method": "POST",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "creation_id",
"value": "={{ $('IG Create Media Container1').first().json.id }}"
},
{
"name": "access_token",
"value": "={{ $env.IG_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "7b4c4b8b-84af-4ddc-9631-e8ae20a401ac",
"name": "Airtable Mark as Posted1",
"type": "n8n-nodes-base.airtable",
"position": [
9424,
3424
],
"parameters": {
"base": {
"__rl": true,
"mode": "id",
"value": "={{ $env.AIRTABLE_BASE_ID }}"
},
"table": {
"__rl": true,
"mode": "name",
"value": "Reviews"
},
"columns": {
"value": {
"Posted": true,
"Posted At": "={{ new Date().toISOString() }}",
"Card Image URL": "={{ $('Code Merge Upload and Caption1').first().json.public_image_url }}",
"Instagram Post ID": "={{ $('IG Publish Container1').first().json.id }}"
},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": [
"id"
]
},
"options": {},
"operation": "update"
},
"typeVersion": 2
},
{
"id": "8b7e37af-9d23-4128-a245-b02757edde95",
"name": "Slack Notify Team1",
"type": "n8n-nodes-base.slack",
"position": [
9648,
3424
],
"parameters": {
"text": "=[5-star] *Social Proof Post Published!*\n\n*Reviewer:* {{ $('Code Merge Upload and Caption1').first().json.reviewer_name }}\n*Post ID:* {{ $('IG Publish Container1').first().json.id }}\n*Posted At:* {{ new Date().toLocaleString() }}\n\n*Review:* {{ $('Code Merge Upload and Caption1').first().json.review_text.substring(0, 120) }}...\n\n*Card URL:* {{ $('Code Merge Upload and Caption1').first().json.public_image_url }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.SLACK_CHANNEL_ID }}"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"typeVersion": 2.2
}
],
"connections": {
"Upload to URL": {
"main": [
[
{
"node": "Code Merge Upload and Caption1",
"type": "main",
"index": 0
}
]
]
},
"IF Image Ready1": {
"main": [
[
{
"node": "HTTP Fetch Rendered Card1",
"type": "main",
"index": 0
}
]
]
},
"Wait IG Buffer1": {
"main": [
[
{
"node": "IG Publish Container1",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger1": {
"main": [
[
{
"node": "Airtable Fetch Review1",
"type": "main",
"index": 0
}
]
]
},
"Wait Before Poll1": {
"main": [
[
{
"node": "HTTP Bannerbear Poll Status1",
"type": "main",
"index": 0
}
]
]
},
"IF Has Valid Review1": {
"main": [
[
{
"node": "Code Prepare Bannerbear Payload1",
"type": "main",
"index": 0
}
]
]
},
"IG Publish Container1": {
"main": [
[
{
"node": "Airtable Mark as Posted1",
"type": "main",
"index": 0
}
]
]
},
"Airtable Fetch Review1": {
"main": [
[
{
"node": "IF Has Valid Review1",
"type": "main",
"index": 0
}
]
]
},
"Airtable Mark as Posted1": {
"main": [
[
{
"node": "Slack Notify Team1",
"type": "main",
"index": 0
}
]
]
},
"HTTP Fetch Rendered Card1": {
"main": [
[
{
"node": "Upload to URL",
"type": "main",
"index": 0
}
]
]
},
"IG Create Media Container1": {
"main": [
[
{
"node": "Wait IG Buffer1",
"type": "main",
"index": 0
}
]
]
},
"HTTP Bannerbear Create Job1": {
"main": [
[
{
"node": "Wait Before Poll1",
"type": "main",
"index": 0
}
]
]
},
"HTTP Bannerbear Poll Status1": {
"main": [
[
{
"node": "IF Image Ready1",
"type": "main",
"index": 0
}
]
]
},
"Code Merge Upload and Caption1": {
"main": [
[
{
"node": "IG Create Media Container1",
"type": "main",
"index": 0
}
]
]
},
"Code Prepare Bannerbear Payload1": {
"main": [
[
{
"node": "HTTP Bannerbear Create Job1",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
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 branded quote card, and publishes it directly to Instagram, ensuring a consistent…
Source: https://n8n.io/workflows/14629/ — 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.
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
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
This enterprise-grade n8n workflow automates the Instagram complaint handling process — from detection to resolution — using Claude AI, dynamic ticket assignment, and SLA enforcement. It converts cust
Gemini - Video Analysis (NEW). Uses httpRequest, stickyNote, sort, limit. Scheduled trigger; 29 nodes.
This enterprise-grade n8n workflow automates influencer contract compliance for Instagram campaigns — from deadline tracking to breach detection — using Claude AI, Instagram API, and smart reminders.