{
  "name": "Google Sheets to PostSider queue",
  "nodes": [
    {
      "parameters": {},
      "id": "00000000-0000-4000-8000-000000000001",
      "name": "When clicking ‘Test workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "[YOUR GOOGLE SHEET ID]",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "[YOUR SHEET NAME]",
          "mode": "list",
          "cachedResultName": "",
          "cachedResultUrl": ""
        }
      },
      "id": "00000000-0000-4000-8000-000000000002",
      "name": "1. Read rows",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "3",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000000003",
              "leftValue": "={{ $json.created }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "isEmpty",
                "name": "filter.operator.isEmpty"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "00000000-0000-4000-8000-000000000004",
      "name": "2. New row?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "maxItems": 1
      },
      "id": "00000000-0000-4000-8000-000000000005",
      "name": "3. One row per run",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000000006",
              "leftValue": "={{ $json.imageUrl }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty",
                "name": "filter.operator.isNotEmpty"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "00000000-0000-4000-8000-000000000007",
      "name": "4. Image?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.postsider.com/public/v1/upload-from-url",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"url\": \"{{ $json.imageUrl }}\"}"
      },
      "id": "00000000-0000-4000-8000-000000000008",
      "name": "5a. Upload image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        -180
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "1",
          "name": "PostSider API Key"
        }
      }
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.postsider.com/public/v1/find-slot/{{ $('3. One row per run').first().json.channelId }}"
      },
      "id": "00000000-0000-4000-8000-000000000009",
      "name": "6. Find best slot",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        0
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "1",
          "name": "PostSider API Key"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const row = $('3. One row per run').first().json; // content, channelId (one row per run)\nconst slot = $input.first().json; // { date } UTC, append 'Z'\nlet image = [];\ntry {\n  const media = $('5a. Upload image').first().json; // { id, path } when an image was uploaded\n  image = [{ id: media.id, path: media.path }];\n} catch (e) { /* text-only post */ }\nreturn [{\n  json: {\n    body: {\n      type: 'draft',\n      date: slot.date + 'Z',\n      shortLink: false,\n      tags: [],\n      posts: [{\n        integration: { id: row.channelId },\n        value: [{ content: row.content, image }],\n        settings: {}\n      }]\n    }\n  }\n}];"
      },
      "id": "00000000-0000-4000-8000-000000000010",
      "name": "7. Build post",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.postsider.com/public/v1/posts",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "=ps-{{ $now.toISO() }}"
            }
          ]
        }
      },
      "id": "00000000-0000-4000-8000-000000000011",
      "name": "8. Create draft",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        0
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "1",
          "name": "PostSider API Key"
        }
      }
    },
    {
      "parameters": {
        "content": "WHAT THIS DOES\nTurns a Google Sheet into your posting queue: every run takes the next pending row (created cell empty), asks PostSider for the next free slot (the same day-aware queue logic as the dashboard) and creates a scheduled draft there. Fill the sheet with your content plan, run it on a schedule, and the calendar tops itself up.\n\nSHEET COLUMNS (header row, order does not matter)\n- content: the post text (required)\n- channelId: a PostSider channel id from Settings > Channels (required)\n- imageUrl: optional public image URL to attach\n- created: queue marker. Leave it empty for pending rows. After a row has been scheduled, set this cell to anything (e.g. \"done\") so the next run skips it.\n\nSETUP (once, about 4 minutes)\n1. PostSider API key: app.postsider.com > Settings > API > Create key. It starts with ps_ and is shown once, so save it.\n2. n8n credentials:\n   - \"PostSider API Key\" (Header Auth): Name = Authorization, Value = your ps_ key. Select it on the HTTP nodes.\n   - \"Google Sheets account\" (OAuth2): connect your Google account. Select it on \"1. Read rows\".\n3. Open \"1. Read rows\" and pick your spreadsheet and sheet (the sheet needs a header row with the columns above).\n4. Fill in the sheet: content, channelId, optional imageUrl.\n\nHOW TO RUN\n1. Click \"Test workflow\": it schedules the first row with an empty created cell.\n2. Mark that row as done (set created to anything or delete the row), then run again to schedule the next one.\n3. For a hands-off queue, swap the Manual trigger for a Schedule Trigger (e.g. every hour) and keep filling the sheet.\n\nNOTE\nThis workflow creates DRAFTS in the calendar (they publish after you approve them in the app). To auto-publish at the slot instead, change \"type: 'draft'\" to \"type: 'schedule'\" in \"6. Build post\".\n\nSELF-HOSTED POSTSIDER\nReplace api.postsider.com with your domain; add /api if your install serves the API under /api.",
        "height": 540,
        "width": 560,
        "color": 3
      },
      "id": "00000000-0000-4000-8000-000000000012",
      "name": "How to use",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -460
      ]
    },
    {
      "parameters": {
        "content": "SETUP STEP BY STEP\nFollow in order. Do this once per workflow.\n\nSTEP 1 - Create the n8n credentials:\n  a. \"PostSider API Key\" (Header Auth): Name = Authorization, Value = your ps_ key (app.postsider.com > Settings > API > Create key).\n  b. \"Google Sheets account\" (Google Sheets OAuth2): connect your Google account (accounts.google.com).\n\nSTEP 2 - Prepare the sheet:\n  Create a Google Sheet with a header row and these columns (order does not matter):\n    content    = the post text (required)\n    channelId  = a PostSider channel id (required, from app > Settings > Channels)\n    imageUrl   = optional public image URL\n    created    = leave empty. After a row is scheduled, set this cell to anything (e.g. \"done\") so the next run skips it.\n\nSTEP 3 - \"1. Read rows\" (Google Sheets node):\n  Select the \"Google Sheets account\" credential.\n  Document: pick your spreadsheet (or paste its id into the Document field).\n  Sheet: pick the sheet tab (the one with the header row above).\n\nSTEP 4 - Select the \"PostSider API Key\" credential on these nodes:\n  - \"5a. Upload image\" (only used when a row has an imageUrl)\n  - \"6. Find best slot\"\n  - \"8. Create draft\"\n\nSTEP 5 - Run:\n  Click \"Test workflow\" once: it schedules the first row with an empty created cell.\n  Then set created to anything (or delete the row) and run again for the next row.\n  For a hands-off queue, replace the Manual trigger with a Schedule Trigger (e.g. every hour).",
        "height": 760,
        "width": 680,
        "color": 3
      },
      "id": "00000000-0000-4000-8000-000000000013",
      "name": "Setup step by step",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -1020
      ]
    }
  ],
  "connections": {
    "When clicking ‘Test workflow’": {
      "main": [
        [
          {
            "node": "1. Read rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Read rows": {
      "main": [
        [
          {
            "node": "2. New row?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. New row?": {
      "main": [
        [
          {
            "node": "3. One row per run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. One row per run": {
      "main": [
        [
          {
            "node": "4. Image?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Image?": {
      "main": [
        [
          {
            "node": "5a. Upload image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "6. Find best slot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5a. Upload image": {
      "main": [
        [
          {
            "node": "6. Find best slot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Find best slot": {
      "main": [
        [
          {
            "node": "7. Build post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Build post": {
      "main": [
        [
          {
            "node": "8. Create draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "active": false,
  "tags": [
    "PostSider",
    "Google Sheets",
    "Queue",
    "Automation"
  ]
}