{
  "name": "CSV to PostSider queue",
  "nodes": [
    {
      "parameters": {},
      "id": "00000000-0000-4000-8000-000000000097",
      "name": "When clicking ‘Test workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "csv",
        "options": {}
      },
      "id": "00000000-0000-4000-8000-000000000098",
      "name": "1. Read CSV",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000000099",
              "leftValue": "={{ $json.created }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "isEmpty",
                "name": "filter.operator.isEmpty"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "00000000-0000-4000-8000-000000000100",
      "name": "2. New row?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "maxItems": 1
      },
      "id": "00000000-0000-4000-8000-000000000101",
      "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-000000000102",
              "leftValue": "={{ $json.imageUrl }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty",
                "name": "filter.operator.isNotEmpty"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "00000000-0000-4000-8000-000000000103",
      "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-000000000104",
      "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-000000000105",
      "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-000000000106",
      "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-000000000107",
      "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 CSV file into your posting queue: upload a CSV (content, channelId, optional imageUrl, optional created), and every run schedules ONE pending row into the PostSider calendar at the next free slot. The same queue logic as the Google Sheets workflow, but from a plain file.\n\nCSV COLUMNS (header row)\n- content: the post text (required)\n- channelId: a PostSider channel id (required)\n- imageUrl: optional public image URL\n- created: queue marker, leave empty. Set it to anything after a row is scheduled to skip it.\n\nSETUP (once, about 3 minutes)\n1. PostSider API key: app.postsider.com > Settings > API > Create key (ps_...).\n2. n8n credential (Header Auth): \"PostSider API Key\" (Authorization = ps_...).\n3. Prepare the CSV with the columns above.\n\nHOW TO RUN\n1. Click \"Test workflow\" and in the \"1. Read CSV\" node choose your CSV file.\n2. It schedules the first row with an empty created cell. Set created to anything (or delete the row) and run again for the next row.\n\nSELF-HOSTED POSTSIDER\nReplace api.postsider.com with your domain; add /api if your install serves the API under /api.",
        "height": 640,
        "width": 620,
        "color": 3
      },
      "id": "00000000-0000-4000-8000-000000000108",
      "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 credential (Header Auth):\n  \"PostSider API Key\": Name = Authorization, Value = your ps_ key (app.postsider.com > Settings > API > Create key).\n\nSTEP 2 - Prepare the CSV:\n  Header row with these columns (order does not matter): content, channelId, imageUrl (optional), created (leave empty).\n\nSTEP 3 - Select the \"PostSider API Key\" credential on:\n  - \"5a. Upload image\" (only used when a row has an imageUrl)\n  - \"6. Find best slot\"\n  - \"8. Create draft\"\n\nSTEP 4 - Run:\n  Click \"Test workflow\" and in \"1. Read CSV\" choose your CSV file: it schedules the first row with an empty created cell.\n\nSTEP 5 - Next rows:\n  Set created to anything (or delete the row) and run again. For a hands-off queue, add a Schedule Trigger and keep the file updated.\n\nNOTE\nSelf-hosted PostSider? Replace api.postsider.com with your domain (add /api if your install serves the API under /api).",
        "height": 700,
        "width": 640,
        "color": 3
      },
      "id": "00000000-0000-4000-8000-000000000109",
      "name": "Setup step by step",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -1180
      ]
    }
  ],
  "connections": {
    "When clicking ‘Test workflow’": {
      "main": [
        [
          {
            "node": "1. Read CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Read CSV": {
      "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",
    "CSV",
    "Queue",
    "Batch"
  ]
}