This workflow corresponds to n8n.io template #17467 — 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": false
},
"name": "Google Sheets Content Calendar \u2192 Branded Visuals (Canvora)",
"nodes": [
{
"id": "st000000-0000-4000-8000-000000000000",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1100,
-300
],
"parameters": {
"width": 560,
"height": 1168,
"content": "## Google Sheets Content Calendar \u2192 Branded Visuals (Canvora)\n\n### How it works\n\nThis workflow turns a Google Sheet into a content calendar with a built-in design team. Every day it reads the sheet, picks the next row marked `todo`, sends the idea to Canvora to generate on-brand visuals in the row's format and language, polls until generation completes, and posts the finished images to Slack for review (or a failure alert if something goes wrong).\n\n### Sheet columns (row 1 headers, lowercase)\n\n`idea` | `format` | `language` | `brandId` | `status`\n\n- **idea** \u2014 what the post is about (\"5 mistakes new landlords make\").\n- **format** \u2014 Canvora format IDs, comma-separated; empty = instagram_post.\n- **language** \u2014 empty = auto-detect; or force `es`, `de`, \u2026 (150+ languages, generated natively).\n- **brandId** \u2014 optional brand kit ID (GET /api/brands) for your colors, fonts, and logo.\n- **status** \u2014 rows marked `todo` are eligible; set to `done` after reviewing.\n\n### Setup steps\n\n- Connect Google Sheets and select your calendar spreadsheet in the Get Content Calendar Rows node.\n- Create a free Canvora account at [canvora.ai](https://canvora.ai) \u2014 free credits included, no card required \u2014 and get an API key from canvora.ai \u2192 Integrations \u2192 API Keys (available on all plans).\n- In every Canvora HTTP Request node, use one shared Header Auth credential: name `X-API-Key`, value your `vd_...` key.\n- Connect Slack credentials and set the target channel for review and failure messages.\n- Adjust the daily schedule time if needed.\n\n### Customization\n\n- Formats: 100+ available \u2014 list them with GET https://api.canvora.ai/api/formats. Cost: 10 credits per visual, 15 per carousel slide.\n- Add a Google Sheets update node after the Slack step to mark rows `done` automatically.\n- Raise the limit node above 1 to design several rows per day."
},
"typeVersion": 1
},
{
"id": "st010000-0000-4000-8000-000000000000",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
-80
],
"parameters": {
"color": 7,
"width": 420,
"height": 320,
"content": "## Read the calendar\n\nRuns daily, fetches the calendar rows from Google Sheets, and keeps only rows marked todo."
},
"typeVersion": 1
},
{
"id": "st020000-0000-4000-8000-000000000000",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
140,
-80
],
"parameters": {
"color": 7,
"width": 220,
"height": 320,
"content": "## Pick today's post\n\nLimits the run to a single row so exactly one post is designed per day."
},
"typeVersion": 1
},
{
"id": "st030000-0000-4000-8000-000000000000",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
384,
-160
],
"parameters": {
"color": 7,
"width": 240,
"height": 400,
"content": "## Start Canvora generation\n\nSubmits the row's idea and settings to the Canvora generation API."
},
"typeVersion": 1
},
{
"id": "st040000-0000-4000-8000-000000000000",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
656,
-80
],
"parameters": {
"color": 7,
"width": 640,
"height": 544,
"content": "## Poll generation status\n\nWaits between status checks, fetches the generation state, and loops until completion or timeout."
},
"typeVersion": 1
},
{
"id": "st050000-0000-4000-8000-000000000000",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1328,
-160
],
"parameters": {
"color": 7,
"width": 656,
"height": 320,
"content": "## Prepare success review\n\nSorts the finished outputs and posts a Slack review message with the image links."
},
"typeVersion": 1
},
{
"id": "st060000-0000-4000-8000-000000000000",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1488,
208
],
"parameters": {
"color": 7,
"width": 240,
"height": 336,
"content": "## Send failure alert\n\nPosts a Slack notification when generation fails or times out."
},
"typeVersion": 1
},
{
"id": "n00200000-0000-4000-8000-00000000000",
"name": "When Daily at 8am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-480,
80
],
"parameters": {
"rule": {
"interval": [
{
"field": "days",
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.2
},
{
"id": "n00300000-0000-4000-8000-00000000000",
"name": "Get Content Calendar Rows",
"type": "n8n-nodes-base.googleSheets",
"position": [
-260,
80
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.5
},
{
"id": "n00400000-0000-4000-8000-00000000000",
"name": "Filter Rows Marked Todo",
"type": "n8n-nodes-base.filter",
"position": [
-40,
80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "f1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "todo"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "n00500000-0000-4000-8000-00000000000",
"name": "Limit to One Post Per Run",
"type": "n8n-nodes-base.limit",
"position": [
180,
80
],
"parameters": {},
"typeVersion": 1
},
{
"id": "n00600000-0000-4000-8000-00000000000",
"name": "Post Idea to Canvora API",
"type": "n8n-nodes-base.httpRequest",
"position": [
400,
80
],
"parameters": {
"url": "https://api.canvora.ai/api/generations",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify(Object.assign({\n inputType: 'idea',\n inputContent: $('Limit to One Post Per Run').item.json.idea,\n title: 'Calendar: ' + String($('Limit to One Post Per Run').item.json.idea).slice(0, 60),\n outputFormats: String($('Limit to One Post Per Run').item.json.format || 'instagram_post').split(',').map(s => s.trim()).filter(Boolean)\n},\n$('Limit to One Post Per Run').item.json.language ? { language: String($('Limit to One Post Per Run').item.json.language).trim() } : {},\n$('Limit to One Post Per Run').item.json.brandId ? { brandId: String($('Limit to One Post Per Run').item.json.brandId).trim() } : {}\n)) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.2
},
{
"id": "n00700000-0000-4000-8000-00000000000",
"name": "Wait 30 Seconds for Canvora",
"type": "n8n-nodes-base.wait",
"position": [
640,
80
],
"parameters": {
"amount": 30
},
"typeVersion": 1.1
},
{
"id": "n00800000-0000-4000-8000-00000000000",
"name": "Fetch Canvora Generation Status",
"type": "n8n-nodes-base.httpRequest",
"position": [
860,
80
],
"parameters": {
"url": "=https://api.canvora.ai/api/generations/{{ $('Post Idea to Canvora API').item.json.generation.id }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.2
},
{
"id": "n00900000-0000-4000-8000-00000000000",
"name": "If Generation Completed",
"type": "n8n-nodes-base.if",
"position": [
1080,
80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e-done",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ ['completed', 'failed', 'partial', 'cancelled'].includes($json.generation.status) }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "n01000000-0000-4000-8000-00000000000",
"name": "If Generation Timeout",
"type": "n8n-nodes-base.if",
"position": [
1080,
300
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e-timeout",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $runIndex >= 20 }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "n01100000-0000-4000-8000-00000000000",
"name": "If Generation Successful",
"type": "n8n-nodes-base.if",
"position": [
1320,
0
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e-success",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ ['completed', 'partial'].includes($json.generation.status) }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "n01200000-0000-4000-8000-00000000000",
"name": "Send Slack Failure Notification",
"type": "n8n-nodes-base.slack",
"position": [
1540,
380
],
"parameters": {
"text": "=\u26a0\ufe0f Canvora generation didn't finish for today's calendar row (status: {{ $json.generation ? $json.generation.status : 'timeout' }}). Failed generations refund credits automatically. Check https://canvora.ai/dashboard",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "#content"
},
"otherOptions": {}
},
"typeVersion": 2.2
},
{
"id": "n01300000-0000-4000-8000-00000000000",
"name": "Sort Outputs by Order",
"type": "n8n-nodes-base.code",
"position": [
1560,
0
],
"parameters": {
"jsCode": "// Sort outputs by sortOrder (carousel slides can complete out of order)\nconst g = $input.first().json.generation;\nconst outputs = (g.outputs || [])\n .slice()\n .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));\n\nconst lines = outputs.map((o, i) => `${i + 1}. ${o.format || 'visual'} \u2014 ${o.fileUrl}`);\n\nconst text = [\n `\ud83c\udfa8 *New visuals ready* for \"${g.title || 'your content'}\"`,\n `${outputs.length} image(s)${g.status === 'partial' ? ' (some formats failed and were refunded)' : ''}:`,\n '',\n ...lines,\n '',\n `Review or edit: https://canvora.ai/generations/${g.id}`\n].join('\\n');\n\nreturn [{ json: { text, outputs, generationId: g.id } }];"
},
"typeVersion": 2
},
{
"id": "n01400000-0000-4000-8000-00000000000",
"name": "Post Review to Slack Channel",
"type": "n8n-nodes-base.slack",
"position": [
1780,
0
],
"parameters": {
"text": "={{ $json.text }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "name",
"value": "#content"
},
"otherOptions": {}
},
"typeVersion": 2.2
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"connections": {
"When Daily at 8am": {
"main": [
[
{
"node": "Get Content Calendar Rows",
"type": "main",
"index": 0
}
]
]
},
"If Generation Timeout": {
"main": [
[
{
"node": "Send Slack Failure Notification",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait 30 Seconds for Canvora",
"type": "main",
"index": 0
}
]
]
},
"Sort Outputs by Order": {
"main": [
[
{
"node": "Post Review to Slack Channel",
"type": "main",
"index": 0
}
]
]
},
"Filter Rows Marked Todo": {
"main": [
[
{
"node": "Limit to One Post Per Run",
"type": "main",
"index": 0
}
]
]
},
"If Generation Completed": {
"main": [
[
{
"node": "If Generation Successful",
"type": "main",
"index": 0
}
],
[
{
"node": "If Generation Timeout",
"type": "main",
"index": 0
}
]
]
},
"If Generation Successful": {
"main": [
[
{
"node": "Sort Outputs by Order",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Slack Failure Notification",
"type": "main",
"index": 0
}
]
]
},
"Post Idea to Canvora API": {
"main": [
[
{
"node": "Wait 30 Seconds for Canvora",
"type": "main",
"index": 0
}
]
]
},
"Get Content Calendar Rows": {
"main": [
[
{
"node": "Filter Rows Marked Todo",
"type": "main",
"index": 0
}
]
]
},
"Limit to One Post Per Run": {
"main": [
[
{
"node": "Post Idea to Canvora API",
"type": "main",
"index": 0
}
]
]
},
"Wait 30 Seconds for Canvora": {
"main": [
[
{
"node": "Fetch Canvora Generation Status",
"type": "main",
"index": 0
}
]
]
},
"Fetch Canvora Generation Status": {
"main": [
[
{
"node": "If Generation Completed",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily, reads a Google Sheets content calendar for the next row marked , sends the idea to Canvora to generate branded visuals, then posts the finished image links (or a failure/timeout alert) to a Slack channel. Runs every day at 8am on a schedule. Reads rows…
Source: https://n8n.io/workflows/17467/ — 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 contains community nodes that are only compatible with the self-hosted version of n8n.
Simplify financial oversight with this automated n8n workflow. Triggered daily, it fetches cash flow and expense data from a Google Sheet, analyzes inflows and outflows, validates records, and generat
Use cases are many: send recurring market updates to investors, distribute new listings context to buyers, or push periodic area snapshots to your client base — all without touching it manually after
Automated garden and farm irrigation system that uses weather forecasts and evapotranspiration calculations to determine optimal watering schedules, preventing water waste while maintaining healthy pl
This workflow automatically monitors competitor affiliate programs twice daily using Bright Data's web scraping API to extract commission rates, cookie durations, average order values, and payout term