This workflow corresponds to n8n.io template #14648 — we link there as the canonical source.
This workflow follows the Google Sheets → 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": "99c76596-d6c3-4cad-b3f3-c13e983ca9b2",
"name": "Template Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
4528,
3456
],
"parameters": {
"width": 440,
"height": 700,
"content": "Customer Review to Social Proof \u2013 Instagram Post\n\nAutomatically fetches the oldest unposted 5-star review from Google Sheets, generates a branded quote card using hcti.io, uploads it via UploadToURL to get a public CDN link, and publishes it to Instagram with a caption. Marks the review as posted and notifies the team on Slack.\n\nFlow:\nSchedule \u2192 Fetch review \u2192 Validate \u2192 Prepare caption \u2192 Generate image \u2192 Upload \u2192 Create IG post \u2192 Publish \u2192 Update sheet \u2192 Notify Slack\n\nRequired:\nIG_USER_ID, IG_ACCESS_TOKEN, GSHEET_DOCUMENT_ID, SLACK_CHANNEL_ID,\nBRAND_NAME, CARD_BG_COLOR, CARD_TEXT_COLOR, CARD_ACCENT_COLOR\n\nSheet Columns:\nReviewer Name, Review Text, Rating, Source,\nPosted, Submitted At, Instagram Post ID, Posted At, Card Image URL"
},
"typeVersion": 1
},
{
"id": "5c76022c-ee0c-40ce-bc6f-935f430a8a79",
"name": "Sticky Nodes 1-3",
"type": "n8n-nodes-base.stickyNote",
"position": [
5088,
4176
],
"parameters": {
"color": 7,
"width": 560,
"height": 784,
"content": "Nodes 1-3: Schedule, Fetch and Gate\n\nSchedule Trigger fires daily at 9 AM.\nAdjust the cron expression for different cadences.\n\nGoogle Sheets - Fetch Review reads the Reviews sheet,\nfiltering for Rating = 5 and Posted = FALSE, sorted by\nSubmitted At ascending (FIFO queue). Fetches 1 row per run.\n\nIF - Has Valid Review exits cleanly on the false branch\nwhen no unposted reviews remain. No errors, no noise."
},
"typeVersion": 1
},
{
"id": "03fc6fa2-1e09-43bb-9c4a-51804ac1b0d4",
"name": "Sticky Nodes 4-6",
"type": "n8n-nodes-base.stickyNote",
"position": [
5712,
4240
],
"parameters": {
"color": 7,
"width": 628,
"height": 796,
"content": "Field Preparation & Card Rendering\nCode \u2013 Prepare Fields: Cleans raw row data by truncating review text, formatting star ratings, and generating a structured Instagram caption.\n\nSet \u2013 Assemble HTML Card: Constructs a 1080x1080px HTML/CSS template referencing brand colors and text fields; using a Set node helps bypass Web Application Firewall (WAF) issues.\n\nHTTP \u2013 hcti.io: Sends the HTML string to hcti.io to render it into a PNG image, returning a URL for the final graphic."
},
"typeVersion": 1
},
{
"id": "206b3749-d3c5-49bf-a858-a09c1d554cfe",
"name": "Sticky Nodes 7-9",
"type": "n8n-nodes-base.stickyNote",
"position": [
6384,
4208
],
"parameters": {
"color": 7,
"width": 580,
"height": 776,
"content": "Upload to URL: Downloads the PNG from hcti.io and uploads it to a CDN to generate a mandatory public HTTPS URL, as Instagram's API rejects binary or base64 data.\n\nCode \u2013 Merge Fields: Unifies the new CDN image URL with the pre-built caption and associated metadata.\n\nHTTP \u2013 IG Create Media Container: Submits the CDN URL and caption to the Instagram Graph API, returning a container_id required for final publishing."
},
"typeVersion": 1
},
{
"id": "e17e5b09-e749-4f9b-bab6-e51bf3db64f8",
"name": "Sticky Nodes 10-13",
"type": "n8n-nodes-base.stickyNote",
"position": [
7008,
4224
],
"parameters": {
"color": 7,
"width": 848,
"height": 756,
"content": "\ud83d\ude80 Finalization & Team Notification\nWait 6s: Provides a processing buffer to ensure Instagram finishes validating the media container before the final call.\n\nHTTP \u2013 IG Publish Container: Executes the /media_publish call using the container_id and retrieves the official live Post ID.\n\nGoogle Sheets \u2013 Mark as Posted: Updates the specific row by setting Posted to TRUE and logging the Post ID and timestamp to prevent duplicate entries.\n\nSlack \u2013 Notify Team: Dispatches an alert containing the reviewer's name, a snippet of the text, and the final card URL for internal verification."
},
"typeVersion": 1
},
{
"id": "e996412b-6ce3-4b41-8c1a-f9ad7866e47e",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
5104,
4640
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.1
},
{
"id": "fdaf7f91-d267-4085-a7bb-1ed407e635ec",
"name": "Google Sheets Fetch Review",
"type": "n8n-nodes-base.googleSheets",
"position": [
5328,
4640
],
"parameters": {
"operation": "readRows",
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GSHEET_DOCUMENT_ID }}"
}
},
"typeVersion": 4.4
},
{
"id": "23df6a06-2606-48f0-a92f-78280936dd94",
"name": "IF Has Valid Review",
"type": "n8n-nodes-base.if",
"position": [
5552,
4640
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-row",
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json['Reviewer Name'] }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "d718ec60-c0f1-47d5-a0b5-82f7b40a6c83",
"name": "Code Prepare Fields",
"type": "n8n-nodes-base.code",
"position": [
5760,
4640
],
"parameters": {
"jsCode": "const item = $input.first().json;\nconst fields = item.fields || item;\n\nconst reviewerName = (fields['Reviewer Name'] || fields.reviewer_name || 'Happy Customer').trim();\nconst reviewText = (fields['Review Text'] || fields.review_text || '').trim();\nconst rating = Number(fields['Rating'] || fields.rating || 5);\nconst source = (fields['Source'] || fields.source || 'Verified Review').trim();\nconst recordId = item.id || item.record_id || '';\n\nif (!reviewText) throw new Error('Review text is empty. Check your Google Sheet column names.');\n\nconst cardText = reviewText.length > 200 ? reviewText.substring(0, 197) + '...' : reviewText;\nconst captionText = reviewText.length > 280 ? reviewText.substring(0, 277) + '...' : reviewText;\nconst starCount = Math.min(Math.max(rating, 1), 5);\nconst starsText = Array(starCount + 1).join('5star ');\nconst hashtags = '#customerreview #5stars #testimonial #socialproof #happycustomer #review';\n\nconst finalCaption = [\n '(' + starCount + '/5) Real words from a real customer.',\n '',\n '\"' + captionText + '\"',\n '-- ' + reviewerName + ' via ' + source,\n '',\n 'See why our customers keep coming back.',\n 'Link in bio.',\n '',\n hashtags,\n].join('\\n').trim().substring(0, 2200);\n\nconsole.log('[Prep] Reviewer:', reviewerName, 'Rating:', rating);\n\nreturn [{\n json: {\n record_id: recordId,\n reviewer_name: reviewerName,\n review_text: reviewText,\n card_text: cardText,\n rating: rating,\n star_count: starCount,\n source: source,\n final_caption: finalCaption,\n ig_user_id: $env.IG_USER_ID,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c06c73f3-5549-4c6b-ba56-e2127c551510",
"name": "Set Assemble HTML Card",
"type": "n8n-nodes-base.set",
"position": [
5984,
4640
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "html-field",
"name": "html",
"type": "string",
"value": "={{ '<html><head><meta charset=UTF-8><style>body{margin:0;width:1080px;height:1080px;background:' + ($env.CARD_BG_COLOR || '#1a1a2e') + ';display:flex;flex-direction:column;justify-content:center;align-items:center;padding:80px;font-family:Georgia,serif;box-sizing:border-box;}.stars{font-size:44px;color:' + ($env.CARD_ACCENT_COLOR || '#e94560') + ';margin-bottom:32px;}.quote{font-size:34px;line-height:1.55;color:' + ($env.CARD_TEXT_COLOR || '#ffffff') + ';text-align:center;margin-bottom:32px;font-style:italic;max-width:880px;}.reviewer{font-size:24px;color:' + ($env.CARD_ACCENT_COLOR || '#e94560') + ';font-family:Arial,sans-serif;font-weight:bold;margin-bottom:12px;}.source{font-size:20px;color:#aaaaaa;font-family:Arial,sans-serif;}.brand{position:absolute;bottom:40px;right:56px;font-size:18px;color:#666666;font-family:Arial,sans-serif;text-transform:uppercase;letter-spacing:2px;}.bar{width:56px;height:3px;background:' + ($env.CARD_ACCENT_COLOR || '#e94560') + ';margin:0 auto 32px;}</style></head><body><div class=stars>' + '★'.repeat($json.star_count) + '</div><div class=bar></div><div class=quote>' + $json.card_text + '</div><div class=reviewer>-- ' + $json.reviewer_name + '</div><div class=source>' + $json.source + '</div><div class=brand>' + ($env.BRAND_NAME || 'Our Brand') + '</div></body></html>' }}"
},
{
"id": "pass-fields",
"name": "passFields",
"type": "object",
"value": "={{ $json }}"
}
]
}
},
"typeVersion": 3.3
},
{
"id": "16d65e2b-bdc5-400b-91ae-7c06b9fd62d3",
"name": "HTTP Render HTML to Image",
"type": "n8n-nodes-base.httpRequest",
"position": [
6208,
4640
],
"parameters": {
"url": "https://hcti.io/v1/image",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "html",
"value": "={{ $json.html }}"
}
]
},
"genericAuthType": "httpBasicAuth"
},
"typeVersion": 4.1
},
{
"id": "90a70a3a-1cb8-4702-93f5-3d8a2de2156b",
"name": "Upload to URL",
"type": "n8n-nodes-uploadtourl.uploadToUrl",
"position": [
6432,
4640
],
"parameters": {},
"typeVersion": 1
},
{
"id": "16b86a27-7d21-4d54-a52d-1acd6c47f4db",
"name": "Code Merge Fields",
"type": "n8n-nodes-base.code",
"position": [
6640,
4640
],
"parameters": {
"jsCode": "const uploadResult = $input.first().json;\nconst prepData = $('Code Prepare Fields').first().json;\n\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('Upload to URL returned no public URL. Keys: ' + JSON.stringify(Object.keys(uploadResult)));\n}\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 source: prepData.source,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "07187c79-3949-4e67-8f2b-8bda3e3d7c1d",
"name": "IG Create Media Container",
"type": "n8n-nodes-base.httpRequest",
"position": [
6864,
4640
],
"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": "6538979f-e905-49bd-af1e-165ebb147bb8",
"name": "Wait IG Buffer",
"type": "n8n-nodes-base.wait",
"position": [
7088,
4640
],
"parameters": {
"unit": "seconds",
"amount": 6
},
"typeVersion": 1
},
{
"id": "e1a2792b-605c-405e-aafb-c71dfc52899f",
"name": "IG Publish Container",
"type": "n8n-nodes-base.httpRequest",
"position": [
7312,
4640
],
"parameters": {
"url": "=https://graph.facebook.com/v19.0/{{ $(\"Code Merge Fields\").first().json.ig_user_id }}/media_publish",
"method": "POST",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "creation_id",
"value": "={{ $(\"IG Create Media Container\").first().json.id }}"
},
{
"name": "access_token",
"value": "={{ $env.IG_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "83ca2b33-1514-4ba4-8ecc-f3b21a7099a0",
"name": "Google Sheets Mark as Posted",
"type": "n8n-nodes-base.googleSheets",
"position": [
7520,
4640
],
"parameters": {
"operation": "updateRow",
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GSHEET_DOCUMENT_ID }}"
}
},
"typeVersion": 4.4
},
{
"id": "7c705944-8857-4575-be1c-c46ab918a420",
"name": "Slack Notify Team",
"type": "n8n-nodes-base.slack",
"position": [
7744,
4640
],
"parameters": {
"text": "Social Proof Post Published!\n\nReviewer: {{ $(\"Code Merge Fields\").first().json.reviewer_name }}\nSource: {{ $(\"Code Merge Fields\").first().json.source }}\nPost ID: {{ $(\"IG Publish Container\").first().json.id }}\n\nReview: {{ $(\"Code Merge Fields\").first().json.review_text.substring(0, 100) }}\n\nCard URL: {{ $(\"Code Merge Fields\").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 Fields",
"type": "main",
"index": 0
}
]
]
},
"Wait IG Buffer": {
"main": [
[
{
"node": "IG Publish Container",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Google Sheets Fetch Review",
"type": "main",
"index": 0
}
]
]
},
"Code Merge Fields": {
"main": [
[
{
"node": "IG Create Media Container",
"type": "main",
"index": 0
}
]
]
},
"Code Prepare Fields": {
"main": [
[
{
"node": "Set Assemble HTML Card",
"type": "main",
"index": 0
}
]
]
},
"IF Has Valid Review": {
"main": [
[
{
"node": "Code Prepare Fields",
"type": "main",
"index": 0
}
]
]
},
"IG Publish Container": {
"main": [
[
{
"node": "Google Sheets Mark as Posted",
"type": "main",
"index": 0
}
]
]
},
"Set Assemble HTML Card": {
"main": [
[
{
"node": "HTTP Render HTML to Image",
"type": "main",
"index": 0
}
]
]
},
"HTTP Render HTML to Image": {
"main": [
[
{
"node": "Upload to URL",
"type": "main",
"index": 0
}
]
]
},
"IG Create Media Container": {
"main": [
[
{
"node": "Wait IG Buffer",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets Fetch Review": {
"main": [
[
{
"node": "IF Has Valid Review",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets Mark as Posted": {
"main": [
[
{
"node": "Slack Notify Team",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
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 card using the hcti.io HTML-to-image API, uploads it via Upload to URL to get…
Source: https://n8n.io/workflows/14648/ — 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 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
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.
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
Youtube Videos Checker. Uses httpRequest, googleSheets, slack. Scheduled trigger; 18 nodes.
This workflow automatically mirrors your YouTube to TikTok and Instagram, so you don’t have to manually download and re-upload your content across platforms.