{
  "name": "PostSider - Verified Agent Draft (manual, no publishing)",
  "nodes": [
    {
      "id": "a1",
      "name": "Manual test only",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -680,
        260
      ],
      "parameters": {}
    },
    {
      "id": "a2",
      "name": "01 Safe demo settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -470,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Before the first run, set channel, caption, future UTC date and stable job ID.\n// For a retry, keep the same date, body and idempotency key.\n// Use a connected TEST channel ID. This workflow only creates a DRAFT.\nconst channelId = 'REPLACE_WITH_REAL_CHANNEL_ID';\nconst content = 'Draft demo: a human will decide if this should ever go live.';\nconst date = 'REPLACE_WITH_FUTURE_UTC_TIMESTAMP';\nconst jobId = 'n8n-postsider-demo-001';\nif (!/^[a-zA-Z0-9_-]{6,128}$/.test(channelId) || channelId.startsWith('REPLACE_')) throw Error('Select a real test channel ID');\nif (!/^[a-zA-Z0-9._:-]{8,100}$/.test(jobId)) throw Error('Set a stable idempotency key');\nif (!date.endsWith('Z') || !Number.isFinite(Date.parse(date)) || Date.parse(date) <= Date.now()) throw Error('Set a future UTC ISO timestamp in Safe demo settings');\nreturn [{json:{channelId, content, date, jobId}}];"
      }
    },
    {
      "id": "a3",
      "name": "02 List connected channels",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -260,
        260
      ],
      "parameters": {
        "method": "GET",
        "url": "https://api.postsider.com/public/v1/integrations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      }
    },
    {
      "id": "a4",
      "name": "03 Validate and build DRAFT",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -30,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const config = $('01 Safe demo settings').first().json;\nconst raw = $input.all().map(x => x.json);\nconst channels = raw.length === 1 && Array.isArray(raw[0]) ? raw[0] : raw;\nif (!channels.some(ch => ch?.id === config.channelId && !ch.disabled && !ch.refreshNeeded)) {\n  throw Error('Selected test channel is not connected and active');\n}\nconst body = {\n  type: 'draft',\n  date: config.date,\n  shortLink: false,\n  tags: [],\n  posts: [{\n    integration: { id: config.channelId },\n    value: [{ content: config.content, image: [] }],\n    settings: {}\n  }]\n};\nreturn [{json:{body, jobId:config.jobId}}];"
      }
    },
    {
      "id": "a5",
      "name": "04 Create ONE draft",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        190,
        260
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.postsider.com/public/v1/posts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.jobId }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      }
    },
    {
      "id": "a6",
      "name": "05 Check receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        400,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// PostSider returns an array of {postId, integration}.\n// n8n may represent the array as individual items.\nconst all = $input.all().map(x => x.json);\nconst receipts = all.length === 1 && Array.isArray(all[0]) ? all[0] : all;\nconst expected = $('01 Safe demo settings').first().json.channelId;\nif (receipts.length !== 1 || receipts[0].integration !== expected ||\n    !/^[a-zA-Z0-9_-]{6,128}$/.test(receipts[0].postId || '')) {\n  throw Error('Create response does not match the chosen channel');\n}\nreturn [{json:{postId:receipts[0].postId, channelId:expected}}];"
      }
    },
    {
      "id": "a7",
      "name": "06 Read saved post",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        620,
        260
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.postsider.com/public/v1/posts/' + $('05 Check receipt').first().json.postId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      }
    },
    {
      "id": "a8",
      "name": "07 Verify saved DRAFT",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        850,
        260
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const detail = $input.first().json;\nconst expected = $('01 Safe demo settings').first().json;\nconst receipt = $('05 Check receipt').first().json;\nconst posts = Array.isArray(detail.posts) ? detail.posts : [];\nconst saved = posts.find(p => p.id === receipt.postId);\nconst actualChannel = detail.integration?.id || saved?.integrationId || saved?.integration?.id;\nif (!saved || saved.state !== 'DRAFT' || saved.content !== expected.content ||\n    actualChannel !== expected.channelId) {\n  throw Error('Readback mismatch. No publish/schedule was requested.');\n}\nreturn [{json:{\n  verified:true,\n  postId:receipt.postId,\n  channelId:expected.channelId,\n  state:'DRAFT',\n  published:false,\n  next:'Review this saved draft in the PostSider calendar. Publication requires an explicit separate decision.'\n}}];"
      }
    },
    {
      "id": "note",
      "name": "START HERE - safe manual demo",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -670,
        -100
      ],
      "parameters": {
        "height": 320,
        "width": 650,
        "color": 4,
        "content": "MANUAL RUN ONLY — never schedules or publishes.\n1. In Settings > API create a NAMED ORGANIZATION API key with channels:read, posts:read and posts:write. The new-key default is read-only; explicitly add posts:write for this draft demo. IMPORTANT: posts:write ALSO permits scheduling and publishing, not just drafts. Old credentials may have broader grants. Keep the key secret.\n2. In n8n create HTTP Header Auth credential: Name Authorization; Value = raw key.\n3. Select the credential in all 3 HTTP Request nodes.\n4. Edit node 01: a real TEST channel ID, reviewed text, FUTURE UTC ISO date and stable idempotency job ID.\n5. Test workflow once. Node 07 must prove state DRAFT, correct channel/content.\n6. On retry keep exactly the same body + job ID.\nNever paste a secret in this JSON or share it after populating credentials."
      }
    }
  ],
  "connections": {
    "Manual test only": {
      "main": [
        [
          {
            "node": "01 Safe demo settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "01 Safe demo settings": {
      "main": [
        [
          {
            "node": "02 List connected channels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "02 List connected channels": {
      "main": [
        [
          {
            "node": "03 Validate and build DRAFT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "03 Validate and build DRAFT": {
      "main": [
        [
          {
            "node": "04 Create ONE draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "04 Create ONE draft": {
      "main": [
        [
          {
            "node": "05 Check receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "05 Check receipt": {
      "main": [
        [
          {
            "node": "06 Read saved post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "06 Read saved post": {
      "main": [
        [
          {
            "node": "07 Verify saved DRAFT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": [],
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
