{
  "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
          }
        ]
      ]
    }
  }
}