{
  "name": "MADIAD Hub - Publish at a chosen time",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -160,
        0
      ],
      "id": "Every 5 minutes",
      "name": "Every 5 minutes"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Publishing Schedule"
        },
        "sheetName": {
          "__rl": true,
          "value": "Config",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        60,
        0
      ],
      "id": "Read Config",
      "name": "Read Config"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Publishing Schedule"
        },
        "sheetName": {
          "__rl": true,
          "value": "Schedule",
          "mode": "name"
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        280,
        0
      ],
      "id": "Read Schedule",
      "name": "Read Schedule"
    },
    {
      "parameters": {
        "jsCode": "\nconst cfg = {};\nfor (const it of $('Read Config').all()) {\n  const k = String(it.json['Key'] ?? '').trim();\n  if (k) cfg[k] = String(it.json['Value'] ?? '').trim();\n}\nif (!cfg.profile_id) throw new Error('No profile_id in the Config tab. Copy it from your MADIAD Hub dashboard.');\nconst KENH = { 'Facebook': 'facebook', 'Instagram': 'instagram', 'TikTok': 'tiktok', 'YouTube': 'youtube', 'LinkedIn': 'linkedin', 'X': 'x', 'Threads': 'threads', 'Pinterest': 'pinterest', 'Reddit': 'reddit', 'Bluesky': 'bluesky', 'Google Business': 'google_business', 'Telegram': 'telegram', 'Discord': 'discord', 'Slack': 'slack', 'Mastodon': 'mastodon', 'Nostr': 'nostr', 'Lemmy': 'lemmy', 'dev.to': 'devto', 'Hashnode': 'hashnode', 'WordPress': 'wordpress', 'Whop': 'whop', 'listmonk': 'listmonk' };\nconst IMG = ['.jpg', '.jpeg', '.png', '.webp', '.gif'];\nconst VID = ['.mp4', '.mov', '.m4v', '.webm'];\nconst NEEDS_TITLE = ['youtube', 'reddit', 'pinterest'];\nconst OPEN = ['', 'Draft', 'Queued'];\n\nfunction buildPost(r, cfg, KENH, IMG, VID, NEEDS_TITLE, execId) {\n  const caption = String(r['Caption'] ?? '').trim();\n  const raw = String(r['Channels'] ?? '').trim();\n  // A row that is not finished yet is skipped quietly rather than reported as an error.\n  if (!caption || !raw) return null;\n\n  const platforms = raw.split(',').map(s => KENH[s.trim()]).filter(Boolean);\n  if (!platforms.length) return { _row: r.row_number, _err: 'The Channels cell matches no platform. Pick one from the dropdown.' };\n\n  const links = String(r['Media'] ?? '').split(/[\\n,]/).map(s => s.trim()).filter(Boolean);\n  const bad = links.filter(l => !l.startsWith('https://'));\n  if (bad.length) return { _row: r.row_number, _err: 'Media links must start with https:// , this one is: ' + bad[0] };\n\n  const low = links.map(l => l.toLowerCase());\n  let endpoint = 'text';\n  if (links.length) endpoint = low.some(l => VID.some(e => l.includes(e))) ? 'video' : 'photos';\n\n  const body = { profile_id: cfg.profile_id, platforms, caption };\n  const title = String(r['Title'] ?? '').trim();\n  if (title) for (const p of platforms) if (NEEDS_TITLE.includes(p)) body[p + '_title'] = title;\n  const fc = String(r['First Comment'] ?? '').trim();\n  if (fc) body.first_comment = fc;\n  if (platforms.includes('pinterest')) {\n    if (!cfg.pinterest_board_id) return { _row: r.row_number, _err: 'Posting to Pinterest needs pinterest_board_id in the Config tab.' };\n    body.pinterest_board_id = cfg.pinterest_board_id;\n  }\n  if (endpoint === 'video') body.video_url = links[0];\n  if (endpoint === 'photos') body.photo_urls = links;\n\n  // The post ID is what stops a post going out twice. Generated BEFORE the API call and written straight to the sheet, so a re-run can never mint a second key for the same post.\n  const id = String(r['Post ID'] ?? '').trim() || ('P-' + execId + '-' + r.row_number);\n  return { _row: r.row_number, _id: id, _endpoint: endpoint, body };\n}\n\n// Your own n8n holds the schedule; it is not handed to the server. Reason: a server-side scheduled job drops the chosen Facebook Page, so an account with several Pages posts to the wrong one and nothing says so.\nconst tz = cfg.timezone || 'UTC';\nconst p2 = (x) => String(x).padStart(2, '0');\nfunction key(s) {\n  s = String(s ?? '').trim(); if (!s) return null;\n  let m = s.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})[ T](\\d{1,2}):(\\d{2})/);\n  if (m) return m[1] + p2(m[2]) + p2(m[3]) + p2(m[4]) + m[5];\n  m = s.match(/^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})[ ,]+(\\d{1,2}):(\\d{2})/);\n  if (m) return m[3] + p2(m[2]) + p2(m[1]) + p2(m[4]) + m[5];\n  return null;\n}\nlet now;\ntry { now = DateTime.now().setZone(tz).toFormat('yyyyMMddHHmm'); }\ncatch (e) {\n  const d = new Date(Date.now() + 7 * 3600 * 1000);\n  now = d.getUTCFullYear() + p2(d.getUTCMonth() + 1) + p2(d.getUTCDate()) + p2(d.getUTCHours()) + p2(d.getUTCMinutes());\n}\nconst perRun = parseInt(cfg.rows_per_run || '10', 10) || 10;\nconst out = [];\nfor (const it of $input.all()) {\n  if (out.length >= perRun) break;\n  const r = it.json;\n  if (String(r['Action'] ?? '').trim() !== 'Schedule') continue;\n  if (!OPEN.includes(String(r['Status'] ?? '').trim())) continue;\n  const k = key(r['Publish At']);\n  if (!k) { out.push({ json: { _row: r.row_number, _err: 'Could not read the Publish At cell. Use 2026-09-10 08:30 and format the cell as plain text.' } }); continue; }\n  if (k > now) continue;\n  const b = buildPost(r, cfg, KENH, IMG, VID, NEEDS_TITLE, $execution.id);\n  if (b) out.push({ json: b });\n}\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        0
      ],
      "id": "Build the post",
      "name": "Build the post"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "version": 2
          },
          "conditions": [
            {
              "id": "c",
              "operator": {
                "type": "string",
                "operation": "notExists"
              },
              "leftValue": "={{ $json._err }}",
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        720,
        0
      ],
      "id": "Row usable?",
      "name": "Row usable?"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Publishing Schedule"
        },
        "sheetName": {
          "__rl": true,
          "value": "Schedule",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $json._row }}",
            "Post ID": "={{ $json._id }}",
            "Status": "Publishing",
            "Error": ""
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        940,
        -120
      ],
      "id": "Mark as publishing",
      "name": "Mark as publishing",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.madiad.com/v1/posts/{{ $('Build the post').item.json._endpoint }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Madiad-Source",
              "value": "tpl-schedule-at-v1"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ $('Build the post').item.json._id }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($('Build the post').item.json.body) }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1160,
        -120
      ],
      "id": "MADIAD Hub",
      "name": "MADIAD Hub",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "version": 2
          },
          "conditions": [
            {
              "id": "c",
              "operator": {
                "type": "number",
                "operation": "lt"
              },
              "leftValue": "={{ $json.statusCode }}",
              "rightValue": 300
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1380,
        -120
      ],
      "id": "Published?",
      "name": "Published?"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Publishing Schedule"
        },
        "sheetName": {
          "__rl": true,
          "value": "Schedule",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $('Build the post').item.json._row }}",
            "Status": "={{ $json.body?.request_id ? 'Publishing' : 'Published' }}",
            "Request ID": "={{ $json.body?.request_id ?? '' }}",
            "Post Link": "={{ Object.entries($json.body?.results ?? {}).map(([k,v]) => k + ': ' + (v?.url ?? (v?.success === false ? ('failed, ' + (v?.error ?? '')) : 'accepted'))).join('\\n') }}",
            "Platform Post IDs": "={{ Object.entries($json.body?.results ?? {}).filter(([,v]) => v?.publish_id).map(([k,v]) => k + '=' + v.publish_id).join(';') }}",
            "Published At": "={{ $json.body?.request_id ? '' : $now.setZone('UTC').toFormat('yyyy-MM-dd HH:mm') }}",
            "Error": ""
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1600,
        -220
      ],
      "id": "Write result",
      "name": "Write result",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Publishing Schedule"
        },
        "sheetName": {
          "__rl": true,
          "value": "Schedule",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $('Build the post').item.json._row }}",
            "Status": "Error",
            "Error": "={{ $json.body?.error?.message ?? ('HTTP ' + ($json.statusCode ?? '?')) }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1600,
        -20
      ],
      "id": "Write error",
      "name": "Write error",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": "[MADIAD Template] Publishing Schedule"
        },
        "sheetName": {
          "__rl": true,
          "value": "Schedule",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "row_number": "={{ $json._row }}",
            "Status": "Error",
            "Error": "={{ $json._err }}"
          },
          "matchingColumns": [
            "row_number"
          ],
          "schema": []
        },
        "options": {
          "dataLocationOnSheet": {
            "values": {
              "rangeDefinition": "specifyRange",
              "headerRow": 1,
              "firstDataRow": 3
            }
          }
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        940,
        140
      ],
      "id": "Write input error",
      "name": "Write input error",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "content": "## MADIAD Hub - Publish at a chosen time\n\n**Three things before you switch this on:**\n1. Pick your own copy of the sheet in EVERY Google Sheets node.\n2. Create a **Header Auth** credential named `MADIAD Hub`:\n   Name `Authorization` , Value `Bearer <your API key>`\n   then select it on the node that calls the API.\n3. Put your `profile_id` in the sheet's Config tab.\n\nYour own n8n holds the schedule; it is not handed to the server. Reason: a server-side scheduled job drops the chosen Facebook Page, so an account with several Pages posts to the wrong one and nothing says so.",
        "height": 470,
        "width": 440,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -200
      ],
      "id": "Setup",
      "name": "Setup"
    }
  ],
  "connections": {
    "Every 5 minutes": {
      "main": [
        [
          {
            "node": "Read Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Config": {
      "main": [
        [
          {
            "node": "Read Schedule",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Schedule": {
      "main": [
        [
          {
            "node": "Build the post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the post": {
      "main": [
        [
          {
            "node": "Row usable?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Row usable?": {
      "main": [
        [
          {
            "node": "Mark as publishing",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write input error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark as publishing": {
      "main": [
        [
          {
            "node": "MADIAD Hub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MADIAD Hub": {
      "main": [
        [
          {
            "node": "Published?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Published?": {
      "main": [
        [
          {
            "node": "Write result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}