AutomationFlowsSocial Media › Google Sheets to Instagram Posts from Reviews

Google Sheets to Instagram Posts from Reviews

Original n8n title: Auto-generate Instagram Posts From Google Sheets Reviews Using Uploadtourl

ByJitesh Dugar @jiteshdugar on n8n.io

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…

Cron / scheduled trigger★★★★☆ complexity18 nodesGoogle SheetsHTTP RequestN8N Nodes UploadtourlSlack
Social Media Trigger: Cron / scheduled Nodes: 18 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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>' + '&#9733;'.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
          }
        ]
      ]
    }
  }
}
Pro

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 →

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

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

HTTP Request, Google Sheets, Slack
Social Media

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.

Google Sheets, Slack, HTTP Request
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

Youtube Videos Checker. Uses httpRequest, googleSheets, slack. Scheduled trigger; 18 nodes.

HTTP Request, Google Sheets, Slack
Social Media

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.

@Blotato/N8N Nodes Blotato, Execute Command, HTTP Request +2