{
  "name": "Content to Social Posts with Zabio and WhatsApp Approval",
  "connections": {
    "Every 2 Hours": {
      "main": [
        [
          {
            "node": "Get Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Sources": {
      "main": [
        [
          {
            "node": "Fetch Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Source": {
      "main": [
        [
          {
            "node": "Extract Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Items": {
      "main": [
        [
          {
            "node": "New Items Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New Items Only": {
      "main": [
        [
          {
            "node": "Pick New Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick New Items": {
      "main": [
        [
          {
            "node": "Claim Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claim Posts": {
      "main": [
        [
          {
            "node": "Only Drafts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Only Drafts": {
      "main": [
        [
          {
            "node": "Draft Each Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post From a URL": {
      "main": [
        [
          {
            "node": "Find Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Source": {
      "main": [
        [
          {
            "node": "Manual Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Item": {
      "main": [
        [
          {
            "node": "Claim Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft One Post": {
      "main": [
        [
          {
            "node": "Social Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Social Settings": {
      "main": [
        [
          {
            "node": "Get Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Post": {
      "main": [
        [
          {
            "node": "Get Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Source": {
      "main": [
        [
          {
            "node": "Fetch Article",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Article": {
      "main": [
        [
          {
            "node": "Build Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Brief": {
      "main": [
        [
          {
            "node": "Write Posts With Zabio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Posts With Zabio": {
      "main": [
        [
          {
            "node": "Parse Drafts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Drafts": {
      "main": [
        [
          {
            "node": "Needs an Image?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs an Image?": {
      "main": [
        [
          {
            "node": "Draw Image With Zabio",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draw Image With Zabio": {
      "main": [
        [
          {
            "node": "Prepare Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Review": {
      "main": [
        [
          {
            "node": "Save Draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Draft": {
      "main": [
        [
          {
            "node": "Send Review Link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Review Link": {
      "main": [
        [
          {
            "node": "Review Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Review Posts": {
      "main": [
        [
          {
            "node": "Read Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Decision": {
      "main": [
        [
          {
            "node": "Publish?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Publish?": {
      "main": [
        [
          {
            "node": "Image Route",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Route": {
      "main": [
        [
          {
            "node": "Image File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Host Image On Blotato",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image File": {
      "main": [
        [
          {
            "node": "Build Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Requests": {
      "main": [
        [
          {
            "node": "Route Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Host Image On Blotato": {
      "main": [
        [
          {
            "node": "Build Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Decision": {
      "main": [
        [
          {
            "node": "Not Published",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route Request": {
      "main": [
        [
          {
            "node": "Post Text (Upload-Post)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post Image (Upload-Post)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post (Blotato)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "All Results",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Post Text (Upload-Post)": {
      "main": [
        [
          {
            "node": "All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "All Results": {
      "main": [
        [
          {
            "node": "Collect Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Image (Upload-Post)": {
      "main": [
        [
          {
            "node": "All Results",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Post (Blotato)": {
      "main": [
        [
          {
            "node": "All Results",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Collect Results": {
      "main": [
        [
          {
            "node": "Save Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Result": {
      "main": [
        [
          {
            "node": "Show Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Link": {
      "main": [
        [
          {
            "node": "Find Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Image": {
      "main": [
        [
          {
            "node": "Image From Table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "nodes": [
    {
      "id": "511bf573-e3e0-4e48-bda9-7a46d0a624ff",
      "name": "Every 2 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 2
            }
          ]
        }
      }
    },
    {
      "id": "77c6400f-9e22-4c1b-a651-d5eebb5df86b",
      "name": "Get Sources",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        224,
        0
      ],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_sources"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "active",
              "condition": "isTrue"
            }
          ]
        },
        "returnAll": true
      }
    },
    {
      "id": "a7c692bb-89e5-4364-810d-2a7d60927900",
      "name": "Fetch Source",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        448,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n content-to-social)"
            }
          ]
        },
        "options": {
          "timeout": 20000,
          "response": {
            "response": {
              "responseFormat": "text",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "9240ee38-b09d-4fbd-87d7-3cc3db89a1be",
      "name": "Extract Items",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        672,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Reads RSS/Atom feeds, changelog pages (one entry per h2 heading) and single pages.\nconst NL = String.fromCharCode(10);\nconst WS = new RegExp('[' + String.fromCharCode(9, 10, 13, 32, 160) + ']+', 'g');\nfunction decode(s) {\n  s = String(s || '').split('<![CDATA[').join('').split(']]>').join('');\n  s = s.replace(/&#x([0-9a-fA-F]+);/g, (m, h) => String.fromCharCode(parseInt(h, 16)));\n  s = s.replace(/&#([0-9]+);/g, (m, d) => String.fromCharCode(Number(d)));\n  return s.replace(/&nbsp;/g, ' ').replace(/&quot;/g, '\"').replace(/&apos;/g, \"'\").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');\n}\nfunction strip(s) {\n  return String(s || '')\n    .replace(/<(script|style|noscript|svg)[^>]*>[^]*?<[/](script|style|noscript|svg)>/gi, ' ')\n    .replace(/<[^>]+>/g, ' ').replace(WS, ' ').trim();\n}\nfunction plain(s) { return strip(decode(s)); }\nfunction tag(block, name) {\n  const re = new RegExp('<' + name + '(?:[^a-zA-Z0-9:>][^>]*)?>([^]*?)<[/]' + name + '>', 'i');\n  const m = block.match(re);\n  return m ? m[1] : '';\n}\nfunction attrLink(block) {\n  const links = block.match(/<link[^>]*>/gi) || [];\n  let best = '';\n  for (const l of links) {\n    const h = l.match(/href=[\"']([^\"']+)[\"']/i);\n    if (!h) continue;\n    if (/rel=[\"']alternate[\"']/i.test(l) || !/rel=/i.test(l)) return h[1];\n    if (!best) best = h[1];\n  }\n  return best;\n}\nfunction slug(s) { return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60); }\n\nconst out = [];\nconst items = $input.all();\nfor (let i = 0; i < items.length; i++) {\n  const src = $('Get Sources').itemMatching(i).json;\n  const body = String(items[i].json.data || '');\n  if (!body || items[i].json.error) continue;\n  const type = String(src.type || 'rss').toLowerCase();\n  const base = String(src.url || '').split('#')[0];\n  let found = [];\n  if (type === 'rss') {\n    const blocks = body.match(/<(item|entry)[ >][^]*?<[/](item|entry)>/gi) || [];\n    for (const b of blocks) {\n      const link = decode(tag(b, 'link')).trim() || attrLink(b);\n      const guid = decode(tag(b, 'guid') || tag(b, 'id')).trim();\n      if (!link && !guid) continue;\n      const content = tag(b, 'content:encoded') || tag(b, 'content') || tag(b, 'description') || tag(b, 'summary');\n      const date = decode(tag(b, 'pubDate') || tag(b, 'published') || tag(b, 'updated') || tag(b, 'dc:date')).trim();\n      found.push({ key: 'rss:' + (guid || link), title: plain(tag(b, 'title')), url: link || guid, text: plain(content).slice(0, 6000), date: Date.parse(date) || 0 });\n    }\n    if (found.some(f => f.date)) found.sort((a, b) => b.date - a.date);\n  } else if (type === 'changelog') {\n    const re = /<h2([^>]*)>([^]*?)<[/]h2>/gi;\n    const heads = [];\n    let m;\n    while ((m = re.exec(body)) !== null) heads.push({ attrs: m[1], title: plain(m[2]), start: m.index, end: m.index + m[0].length });\n    if (heads.length) {\n      for (let h = 0; h < heads.length; h++) {\n        const next = h + 1 < heads.length ? heads[h + 1].start : body.length;\n        const idm = heads[h].attrs.match(/id=[\"']([^\"']+)[\"']/i);\n        const anchor = idm ? idm[1] : slug(heads[h].title);\n        found.push({ key: 'changelog:' + base + '#' + anchor, title: heads[h].title, url: base + '#' + anchor, text: plain(body.slice(heads[h].end, next)).slice(0, 6000), date: 0 });\n      }\n    } else {\n      // Plain Markdown changelogs: one entry per \"## \" heading.\n      body.split(NL + '## ').slice(1).forEach(p => {\n        const lines = p.split(NL);\n        const t = lines[0].trim();\n        found.push({ key: 'changelog:' + base + '#' + slug(t), title: t, url: base + '#' + slug(t), text: lines.slice(1).join(' ').replace(WS, ' ').trim().slice(0, 6000), date: 0 });\n      });\n    }\n  } else {\n    found.push({ key: 'page:' + base, title: plain(tag(body, 'title')), url: src.url, text: '', date: 0 });\n  }\n  found.slice(0, 10).forEach((f, pos) => out.push({ json: {\n    source_id: src.source_id, max_new: src.max_new,\n    item_key: f.key.slice(0, 500), item_title: f.title.slice(0, 300), item_url: f.url, item_text: f.text, position: pos\n  } }));\n}\nreturn out;"
      }
    },
    {
      "id": "bfb4b8a2-e17f-4616-b4c8-7662c4963cdb",
      "name": "New Items Only",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        896,
        0
      ],
      "parameters": {
        "resource": "row",
        "operation": "rowNotExists",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "condition": "eq",
              "keyName": "item_key",
              "keyValue": "={{ $json.item_key }}"
            },
            {
              "condition": "eq",
              "keyName": "source_id",
              "keyValue": "={{ $json.source_id }}"
            }
          ]
        }
      }
    },
    {
      "id": "3c2bd3d4-1c40-40c1-9ade-20f0b665f0d5",
      "name": "Pick New Items",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// The first time a feed is read, everything in it is new: draft only the newest item\n// and record the rest as skipped. After that, draft up to max_new items per run.\nconst all = $('Extract Items').all().map(i => i.json);\nconst fresh = $input.all().map(i => i.json);\nfunction rid(n) { const c = 'abcdefghijklmnopqrstuvwxyz0123456789'; let s = ''; for (let i = 0; i < n; i++) s += c[Math.floor(Math.random() * c.length)]; return s; }\nconst bySource = {};\nfor (const it of fresh) { if (!bySource[it.source_id]) bySource[it.source_id] = []; bySource[it.source_id].push(it); }\nconst now = new Date().toISOString();\nconst out = [];\nfor (const sid of Object.keys(bySource)) {\n  const list = bySource[sid].sort((a, b) => a.position - b.position);\n  const total = all.filter(x => x.source_id === sid).length;\n  const firstRun = list.length === total && total > 1;\n  const max = firstRun ? 1 : Math.max(1, Number(list[0].max_new) || 1);\n  list.forEach((it, idx) => {\n    const status = idx < max ? 'drafting' : (firstRun ? 'skipped' : 'later');\n    if (status === 'later') return;\n    out.push({ json: {\n      post_id: rid(12), token: rid(24), source_id: sid, item_key: it.item_key,\n      item_title: it.item_title, item_url: it.item_url, item_text: it.item_text, status, created_at: now\n    } });\n  });\n}\nreturn out;"
      }
    },
    {
      "id": "e4b9fa4e-dc73-4b1d-8236-ba94b33960de",
      "name": "Claim Posts",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1344,
        96
      ],
      "parameters": {
        "resource": "row",
        "operation": "insert",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        }
      }
    },
    {
      "id": "1122c335-7eac-439b-ba12-fa59c810716d",
      "name": "Only Drafts",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        1568,
        96
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "drafting"
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "a44280bc-d750-478f-ba20-3ff767e279c9",
      "name": "Draft Each Post",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        1792,
        96
      ],
      "parameters": {
        "mode": "each",
        "source": "database",
        "workflowId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $workflow.id }}"
        },
        "options": {
          "waitForSubWorkflow": false
        }
      }
    },
    {
      "id": "3b1050a5-d39c-417a-8f68-2763057984b6",
      "name": "Post From a URL",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        672,
        192
      ],
      "parameters": {
        "formTitle": "Turn a page into social posts",
        "formDescription": "Paste any public URL. Zabio drafts the posts and the review link arrives on WhatsApp.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Page URL",
              "fieldName": "url",
              "fieldType": "text",
              "placeholder": "https://example.com/blog/new-feature",
              "requiredField": true
            },
            {
              "fieldLabel": "Source ID",
              "fieldName": "source_id",
              "fieldType": "text",
              "placeholder": "The source_id of a row in social_sources, like blog",
              "requiredField": true
            }
          ]
        },
        "responseMode": "onReceived",
        "options": {
          "respondWithOptions": {
            "values": {
              "respondWith": "text",
              "formSubmittedText": "Drafting now. The review link arrives on WhatsApp in a minute or two."
            }
          }
        }
      },
      "webhookId": "c1acda85-ddd4-4d9d-9797-6b8bdd3be581"
    },
    {
      "id": "dd2804b3-8b34-4890-95ee-53aac9d7159c",
      "name": "Find Source",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        896,
        192
      ],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_sources"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "source_id",
              "condition": "eq",
              "keyValue": "={{ $json.source_id || $json[\"Source ID\"] }}"
            }
          ]
        },
        "limit": 1
      }
    },
    {
      "id": "02fc823c-7a89-4d32-a531-543f308b7c7e",
      "name": "Manual Item",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        192
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const f = $('Post From a URL').first().json;\nconst url = String(f.url || f['Page URL'] || '').trim();\nif (!/^https?:[/][/]/i.test(url)) return [];\nfunction rid(n) { const c = 'abcdefghijklmnopqrstuvwxyz0123456789'; let s = ''; for (let i = 0; i < n; i++) s += c[Math.floor(Math.random() * c.length)]; return s; }\nconst src = $input.first().json;\nreturn [{ json: {\n  post_id: rid(12), token: rid(24), source_id: src.source_id, item_key: 'manual:' + url + ':' + Date.now(),\n  item_title: '', item_url: url, item_text: '', status: 'drafting', created_at: new Date().toISOString()\n} }];"
      }
    },
    {
      "id": "90c034a4-7b4e-422f-95d0-5abed9f2bec4",
      "name": "Draft One Post",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        880
      ],
      "parameters": {
        "inputSource": "passthrough"
      }
    },
    {
      "id": "5164df69-eafc-43a1-9307-baa40a13877a",
      "name": "Social Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        224,
        880
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "n8n_url",
              "value": "https://n8n.yourcompany.com",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "zabio_model",
              "value": "auto/balanced",
              "type": "string"
            },
            {
              "id": "s3",
              "name": "image_model",
              "value": "auto",
              "type": "string"
            },
            {
              "id": "s4",
              "name": "image_size",
              "value": "1024x1024",
              "type": "string"
            },
            {
              "id": "s5",
              "name": "wa_phone_number_id",
              "value": "",
              "type": "string"
            },
            {
              "id": "s6",
              "name": "wa_template",
              "value": "content_review|en",
              "type": "string"
            },
            {
              "id": "s7",
              "name": "review_hours",
              "value": 48,
              "type": "number"
            },
            {
              "id": "s8",
              "name": "timezone",
              "value": "America/New_York",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "014e5f31-c013-4e59-8099-f9dfc33774c9",
      "name": "Get Post",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        448,
        880
      ],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "post_id",
              "condition": "eq",
              "keyValue": "={{ $json.post_id }}"
            }
          ]
        },
        "limit": 1
      }
    },
    {
      "id": "32d0481d-b080-4421-a29f-7acbfe52be37",
      "name": "Get Source",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        672,
        880
      ],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_sources"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "source_id",
              "condition": "eq",
              "keyValue": "={{ $json.source_id }}"
            }
          ]
        },
        "limit": 1
      }
    },
    {
      "id": "94a8b75c-0f4f-4750-a3ab-541b64a34105",
      "name": "Fetch Article",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        896,
        880
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $(\"Get Post\").first().json.item_url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n content-to-social)"
            }
          ]
        },
        "options": {
          "timeout": 20000,
          "response": {
            "response": {
              "responseFormat": "text",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "32fd26e6-cdaa-408b-8662-c53fd820040c",
      "name": "Build Brief",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        880
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const NL = String.fromCharCode(10);\nconst WS = new RegExp('[' + String.fromCharCode(9, 10, 13, 32, 160) + ']+', 'g');\nfunction decode(s) {\n  s = String(s || '').split('<![CDATA[').join('').split(']]>').join('');\n  s = s.replace(/&#x([0-9a-fA-F]+);/g, (m, h) => String.fromCharCode(parseInt(h, 16)));\n  s = s.replace(/&#([0-9]+);/g, (m, d) => String.fromCharCode(Number(d)));\n  return s.replace(/&nbsp;/g, ' ').replace(/&quot;/g, '\"').replace(/&apos;/g, \"'\").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');\n}\nfunction plain(s) { return decode(String(s || '').replace(/<[^>]+>/g, ' ')).replace(WS, ' ').trim(); }\nconst s = $('Social Settings').first().json;\nconst post = $('Get Post').first().json;\nconst src = $('Get Source').first().json;\nconst html = String($input.first().json.data || '');\n\nconst metas = {};\nfor (const t of (html.match(/<meta[^>]+>/gi) || [])) {\n  const n = t.match(/(?:property|name)=[\"']([^\"']+)[\"']/i);\n  const c = t.match(/content=[\"']([^\"']*)[\"']/i);\n  if (n && c && !metas[n[1].toLowerCase()]) metas[n[1].toLowerCase()] = decode(c[1]);\n}\nfunction section(name) { const m = html.match(new RegExp('<' + name + '(?:[^a-zA-Z0-9>][^>]*)?>([^]*?)<[/]' + name + '>', 'i')); return m ? m[1] : ''; }\nlet main = section('article') || section('main') || section('body') || html;\nmain = main.replace(/<(nav|header|footer|aside|form|script|style|noscript|svg)[^a-z][^]*?<[/](nav|header|footer|aside|form|script|style|noscript|svg)>/gi, ' ');\nconst pageText = plain(main).slice(0, 9000);\n\nconst type = String(src.type || 'rss').toLowerCase();\nlet text = type === 'changelog' ? post.item_text : (pageText.length > 400 ? pageText : (post.item_text || pageText));\nconst title = post.item_title || metas['og:title'] || plain(section('title'));\nif (!text && !title) throw new Error('Could not read anything from ' + post.item_url);\ntext = text || metas['og:description'] || metas['description'] || '';\n\nconst ALLOWED = ['x', 'linkedin', 'threads', 'bluesky', 'facebook', 'instagram', 'tiktok'];\nlet platforms = [];\ntry { platforms = JSON.parse(src.platforms || '[]'); } catch (e) { platforms = String(src.platforms || '').split(','); }\nplatforms = platforms.map(p => String(p).trim().toLowerCase()).map(p => (p === 'twitter' ? 'x' : p)).filter(p => ALLOWED.includes(p));\nif (!platforms.length) platforms = ['x', 'linkedin', 'threads', 'bluesky', 'facebook'];\nconst lang = String(src.language || 'en').toLowerCase().startsWith('es') ? 'es' : 'en';\nconst brand = src.brand || src.source_id;\n\nconst system = [\n  'You write social media posts for ' + brand + ', based only on the source below. Write in ' + (lang === 'es' ? 'Spanish' : 'English') + '.',\n  'Voice: ' + (src.voice || 'Direct, specific and human. Plain words, short sentences, no marketing fluff.'),\n  '',\n  'Rules for every post:',\n  '- Lead with what is new or useful for the reader, not with the company.',\n  '- Use only facts from the source. Never invent numbers, prices, dates, quotes, customers or features.',\n  '- Never use em dashes or en dashes. Use commas, periods, colons or parentheses.',\n  '- Avoid hype and filler: revolutionary, game-changer, unlock, unleash, supercharge, seamless, cutting-edge, elevate, delve, leverage, robust, empower, excited to announce, thrilled.',\n  '- Do not open with \"Exciting news\", \"Big news\", \"Did you know\" or \"In the fast-paced world of\".',\n  '- No URLs in any text. The link is added for you.',\n  '- No emojis unless the voice asks for them.',\n  '- Hashtags only where a format below asks for them.' + (src.hashtags ? ' Prefer these: ' + src.hashtags : ''),\n  '',\n  'Formats (write an empty string, or an empty list, for any network not in: ' + platforms.join(', ') + '):',\n  '- x_post: one post of at most 250 characters, no hashtags or one at most.',\n  '- x_thread: 3 to 5 posts of 180 to 260 characters each. The first one makes people want to read on; the last one says where to read more, without a URL.',\n  '- linkedin: 500 to 1200 characters, short paragraphs separated by a blank line, no markdown, up to 3 hashtags at the end.',\n  '- threads: at most 420 characters, conversational.',\n  '- bluesky: at most 230 characters.',\n  '- facebook: 200 to 500 characters.',\n  '- instagram: a caption of 400 to 1200 characters whose first line works on its own, 3 to 6 hashtags at the end.',\n  '- tiktok_title: at most 80 characters. tiktok_caption: at most 300 characters, 3 to 5 hashtags at the end.',\n  '- image_prompt (always): one picture for these posts, for an image generator. Describe the subject, composition, light and colors. ' + (src.image_style ? 'Style: ' + src.image_style + '.' : 'Style: clean, modern editorial illustration.') + ' No text, letters, numbers, logos or screenshots in the image.',\n  '- image_alt (always): one sentence that describes the image for screen readers.'\n].join(NL);\nconst user = 'Source type: ' + type + NL + 'Title: ' + title + NL + 'URL: ' + post.item_url + NL + NL + 'Source text:' + NL + text;\n\nconst str = { type: 'string' };\nconst keys = ['x_post', 'x_thread', 'linkedin', 'threads', 'bluesky', 'facebook', 'instagram', 'tiktok_title', 'tiktok_caption', 'image_prompt', 'image_alt'];\nconst props = {};\nkeys.forEach(k => { props[k] = k === 'x_thread' ? { type: 'array', items: str } : str; });\nreturn [{ json: {\n  title, url: post.item_url, lang, platforms, type, brand,\n  request: {\n    model: s.zabio_model,\n    messages: [{ role: 'system', content: system }, { role: 'user', content: user }],\n    response_format: { type: 'json_schema', json_schema: { name: 'social_posts', strict: true, schema: { type: 'object', additionalProperties: false, required: keys, properties: props } } }\n  }\n} }];"
      }
    },
    {
      "id": "c6da8f90-3d03-4073-9936-79f4d1117c14",
      "name": "Write Posts With Zabio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1344,
        880
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.zabio.ai/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.request) }}",
        "options": {
          "timeout": 180000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "2117e472-792d-4f3e-88d0-00b5dc62b14a",
      "name": "Parse Drafts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        880
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const NL = String.fromCharCode(10);\nconst ED = String.fromCharCode(8212);\nconst EN = String.fromCharCode(8211);\nconst s = $('Social Settings').first().json;\nconst src = $('Get Source').first().json;\nconst brief = $('Build Brief').first().json;\nconst res = $input.first().json;\nlet d;\ntry { d = JSON.parse(res.choices[0].message.content); } catch (e) { throw new Error('Zabio did not return the drafts: ' + JSON.stringify(res).slice(0, 300)); }\n\n// Clean-up the model cannot be trusted with: dashes, markdown bold, length limits.\nfunction clean(t) {\n  t = String(t || '');\n  t = t.split(' ' + ED + ' ').join(', ').split(ED).join(', ').split(' ' + EN + ' ').join(', ').split(EN).join('-');\n  return t.replace(/[*]{2}/g, '').trim();\n}\nfunction fit(t, max) {\n  if (t.length <= max) return t;\n  const cut = t.slice(0, max);\n  const p = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '), cut.lastIndexOf(NL));\n  if (p > max * 0.5) return cut.slice(0, p + 1).trim();\n  const sp = cut.lastIndexOf(' ');\n  return cut.slice(0, sp > 0 ? sp : max).replace(/[,;:]$/, '').trim();\n}\nconst url = brief.url;\nconst on = p => brief.platforms.includes(p);\nconst out = { x_post: '', x_thread: '', linkedin: '', threads: '', bluesky: '', facebook: '', instagram: '', tiktok_title: '', tiktok_caption: '' };\nif (on('x')) {\n  out.x_post = fit(clean(d.x_post), 255) + ' ' + url;\n  const th = (d.x_thread || []).map(clean).filter(Boolean).slice(0, 6).map(t => fit(t, 275));\n  if (th.length) th[th.length - 1] = fit(th[th.length - 1], 255) + ' ' + url;\n  out.x_thread = th.join(NL + NL);\n}\nif (on('linkedin')) out.linkedin = fit(clean(d.linkedin), 2900) + NL + NL + url;\nif (on('threads')) out.threads = fit(clean(d.threads), Math.max(200, 498 - url.length)) + NL + NL + url;\nif (on('bluesky')) out.bluesky = url.length <= 70 ? fit(clean(d.bluesky), 299 - url.length) + ' ' + url : fit(clean(d.bluesky), 300);\nif (on('facebook')) out.facebook = fit(clean(d.facebook), 1800) + NL + NL + url;\nif (on('instagram')) out.instagram = fit(clean(d.instagram), 2100);\nif (on('tiktok')) { out.tiktok_title = fit(clean(d.tiktok_title), 90); out.tiktok_caption = fit(clean(d.tiktok_caption), 2000); }\n\nconst mode = String(src.image_mode || 'visual').toLowerCase();\nconst need_image = mode === 'none' ? false : (mode === 'all' || on('instagram') || on('tiktok'));\nconst ip = clean(d.image_prompt);\nconst image_prompt = ip + (/[.!?]$/.test(ip) ? '' : '.') + (src.image_style ? ' Style: ' + src.image_style + '.' : '') + ' No text, letters, numbers or logos in the image.';\nreturn [{ json: {\n  drafts: out, need_image, image_prompt, image_alt: clean(d.image_alt),\n  text_cost: (res.zabio && res.zabio.cost_micros) || 0,\n  image_request: { model: s.image_model, prompt: image_prompt, size: s.image_size, output_format: 'jpeg', n: 1 }\n} }];"
      }
    },
    {
      "id": "ad5d9d3f-8ed6-4c2c-b562-49fbe005ddba",
      "name": "Needs an Image?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1792,
        880
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.need_image }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "63c0a31c-240d-40ce-935c-eb2f962696ef",
      "name": "Draw Image With Zabio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2016,
        800
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.zabio.ai/v1/images/generations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.image_request) }}",
        "options": {
          "timeout": 240000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "96672f82-d01f-486a-a26e-46cffc0cc540",
      "name": "Prepare Review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        880
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const NL = String.fromCharCode(10);\nconst s = $('Social Settings').first().json;\nconst post = $('Get Post').first().json;\nconst src = $('Get Source').first().json;\nconst brief = $('Build Brief').first().json;\nconst pd = $('Parse Drafts').first().json;\nconst lang = brief.lang;\nconst L = (en, es) => (lang === 'es' ? es : en);\nconst esc = t => String(t || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst oneLine = (t, n) => String(t || '').replace(new RegExp('[' + String.fromCharCode(9, 10, 13) + ']+', 'g'), ' ').replace(/[ ]{2,}/g, ' ').trim().slice(0, n);\n\nlet img = '';\nlet imgCost = 0;\nif ($('Draw Image With Zabio').isExecuted) {\n  const r = $('Draw Image With Zabio').first().json;\n  img = (r.data && r.data[0] && r.data[0].b64_json) || '';\n  imgCost = (r.zabio && r.zabio.cost_micros) || 0;\n}\nconst preview_url = String(s.n8n_url).replace(/[/]+$/, '') + '/webhook/social-image?post=' + post.post_id + '&t=' + post.token;\n\nconst names = { x: 'X', linkedin: 'LinkedIn', threads: 'Threads', bluesky: 'Bluesky', facebook: 'Facebook', instagram: 'Instagram', tiktok: 'TikTok' };\nconst labels = {\n  action: L('What to do', 'Qué hacer'),\n  publish_at: L('Publish at, only when scheduling (' + s.timezone + ')', 'Publicar el, solo al programar (' + s.timezone + ')'),\n  networks: L('Publish on', 'Publicar en'),\n  x_format: L('On X, publish', 'En X, publicar'),\n  image: L('Image', 'Imagen'),\n  x_post: L('X post', 'Publicación en X'),\n  x_thread: L('X thread, one post per paragraph', 'Hilo de X, una publicación por párrafo'),\n  linkedin: 'LinkedIn', threads: 'Threads', bluesky: 'Bluesky', facebook: 'Facebook',\n  instagram: L('Instagram caption', 'Texto de Instagram'),\n  tiktok_title: L('TikTok title', 'Título de TikTok'),\n  tiktok_caption: L('TikTok caption', 'Texto de TikTok')\n};\nconst opts = { now: L('Publish now', 'Publicar ahora'), schedule: L('Schedule', 'Programar'), reject: L(\"Don't publish\", 'No publicar') };\nconst xopts = { post: L('The single post', 'La publicación única'), thread: L('The thread', 'El hilo') };\nconst imgopt = L('Add the image to the text posts too', 'Añadir la imagen también a las publicaciones de texto');\n\nconst visual = ['instagram', 'tiktok'];\nconst avail = brief.platforms.filter(p => img || !visual.includes(p));\nconst missing = brief.platforms.filter(p => !avail.includes(p));\nconst mode = String(src.image_mode || 'visual').toLowerCase();\n\nconst fields = [];\nlet head = '<p><strong>' + esc(brief.title) + '</strong><br><a href=\"' + esc(brief.url) + '\">' + esc(brief.url) + '</a></p>';\nif (img) head += '<p><img src=\"' + esc(preview_url) + '\" alt=\"' + esc(pd.image_alt) + '\" width=\"360\"></p><p><a href=\"' + esc(preview_url) + '\">' + L('Open the image', 'Abrir la imagen') + '</a></p>';\nif (missing.length) head += '<p>' + L('No image was made, so these networks are left out: ', 'No se pudo crear la imagen, así que quedan fuera: ') + missing.map(p => names[p]).join(', ') + '.</p>';\nfields.push({ fieldType: 'html', elementName: 'preview', html: head });\nfields.push({ fieldLabel: labels.action, fieldName: 'action', fieldType: 'radio', fieldOptions: { values: [{ option: opts.now }, { option: opts.schedule }, { option: opts.reject }] }, defaultValue: opts.now, requiredField: true });\nfields.push({ fieldLabel: labels.publish_at, fieldName: 'publish_at', fieldType: 'text', placeholder: '2026-11-05 09:30' });\nfields.push({ fieldLabel: labels.networks, fieldName: 'networks', fieldType: 'checkbox', fieldOptions: { values: avail.map(p => ({ option: names[p] })) }, defaultValue: avail.map(p => names[p]).join(',') });\nif (img && mode !== 'all' && avail.some(p => !visual.includes(p))) {\n  fields.push({ fieldLabel: labels.image, fieldName: 'image', fieldType: 'checkbox', fieldOptions: { values: [{ option: imgopt }] } });\n}\nconst d = pd.drafts;\nconst area = (k, v) => fields.push({ fieldLabel: labels[k], fieldName: k, fieldType: k === 'tiktok_title' ? 'text' : 'textarea', defaultValue: v });\nif (avail.includes('x')) {\n  fields.push({ fieldLabel: labels.x_format, fieldName: 'x_format', fieldType: 'radio', fieldOptions: { values: [{ option: xopts.post }, { option: xopts.thread }] }, defaultValue: String(src.x_format || '').toLowerCase() === 'thread' ? xopts.thread : xopts.post });\n  area('x_post', d.x_post);\n  area('x_thread', d.x_thread);\n}\n['linkedin', 'threads', 'bluesky', 'facebook', 'instagram'].forEach(p => { if (avail.includes(p)) area(p, d[p]); });\nif (avail.includes('tiktok')) { area('tiktok_title', d.tiktok_title); area('tiktok_caption', d.tiktok_caption); }\n\nreturn [{ json: {\n  post_id: post.post_id, lang, item_url: brief.url, item_title: brief.title,\n  names, labels, opts, xopts, imgopt, platforms_avail: avail, image_mode: mode,\n  img_ok: !!img, image_b64: img, image_alt: pd.image_alt, image_prompt: pd.image_prompt, preview_url,\n  drafts: d, drafts_json: JSON.stringify(d),\n  cost_micros: (pd.text_cost || 0) + imgCost,\n  form_fields: JSON.stringify(fields),\n  form_title: L('Review posts: ', 'Revisar publicaciones: ') + brief.brand,\n  form_desc: L('Edit anything below, pick the networks and submit. Nothing is posted before you do.', 'Edita lo que quieras, elige las redes y envía. No se publica nada antes.'),\n  form_button: L('Submit', 'Enviar'),\n  approver: String(src.approver_whatsapp || '').replace(/[^0-9]/g, ''),\n  wa_brand: oneLine(brief.brand, 60),\n  wa_title: oneLine(brief.title, 80) || oneLine(brief.url, 80)\n} }];"
      }
    },
    {
      "id": "547c67f1-9261-4cbb-b146-caba62ee1079",
      "name": "Save Draft",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2464,
        880
      ],
      "parameters": {
        "resource": "row",
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "post_id",
              "condition": "eq",
              "keyValue": "={{ $json.post_id }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "status": "pending",
            "drafts": "={{ $json.drafts_json }}",
            "image_b64": "={{ $json.image_b64 }}",
            "image_prompt": "={{ $json.image_prompt }}",
            "cost_micros": "={{ $json.cost_micros }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "drafts",
              "displayName": "drafts",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "image_b64",
              "displayName": "image_b64",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "image_prompt",
              "displayName": "image_prompt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "cost_micros",
              "displayName": "cost_micros",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true
            }
          ]
        }
      }
    },
    {
      "id": "60aac7ed-27ca-48ab-bebd-645f2ad8f0a8",
      "name": "Send Review Link",
      "type": "n8n-nodes-base.whatsApp",
      "typeVersion": 1.1,
      "position": [
        2688,
        880
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendTemplate",
        "phoneNumberId": "={{ $(\"Social Settings\").first().json.wa_phone_number_id }}",
        "recipientPhoneNumber": "={{ $(\"Prepare Review\").first().json.approver }}",
        "template": "={{ $(\"Social Settings\").first().json.wa_template }}",
        "components": {
          "component": [
            {
              "type": "body",
              "bodyParameters": {
                "parameter": [
                  {
                    "type": "text",
                    "text": "={{ $(\"Prepare Review\").first().json.wa_brand }}"
                  },
                  {
                    "type": "text",
                    "text": "={{ $(\"Prepare Review\").first().json.wa_title }}"
                  },
                  {
                    "type": "text",
                    "text": "={{ $execution.resumeFormUrl }}"
                  }
                ]
              }
            }
          ]
        }
      },
      "webhookId": "8985fc34-c34f-43e0-aff1-31f256353aa4"
    },
    {
      "id": "9c309462-c833-4ac3-80e2-29d5ab221ab9",
      "name": "Review Posts",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        2912,
        880
      ],
      "parameters": {
        "operation": "page",
        "defineForm": "json",
        "jsonOutput": "={{ $(\"Prepare Review\").first().json.form_fields }}",
        "limitWaitTime": true,
        "limitType": "afterTimeInterval",
        "resumeAmount": "={{ $(\"Social Settings\").first().json.review_hours }}",
        "resumeUnit": "hours",
        "options": {
          "formTitle": "={{ $(\"Prepare Review\").first().json.form_title }}",
          "formDescription": "={{ $(\"Prepare Review\").first().json.form_desc }}",
          "buttonLabel": "={{ $(\"Prepare Review\").first().json.form_button }}"
        }
      },
      "webhookId": "e0d83484-f553-487d-9f4d-a34f66becea2"
    },
    {
      "id": "81fe3386-1654-450d-82ce-b30e8867ec41",
      "name": "Read Decision",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3136,
        880
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const f = $input.first().json;\nconst pr = $('Prepare Review').first().json;\nconst s = $('Social Settings').first().json;\nconst src = $('Get Source').first().json;\nconst CR = String.fromCharCode(13);\nconst v = k => (f[k] !== undefined ? f[k] : f[pr.labels[k]]);\nconst base = { post_id: pr.post_id, decided_at: new Date().toISOString() };\nconst action = v('action');\n// No answer before the time limit: the form timed out.\nif (action === undefined || action === null || action === '') return [{ json: Object.assign(base, { decision: 'expired' }) }];\nif (action === pr.opts.reject) return [{ json: Object.assign(base, { decision: 'rejected' }) }];\n\nlet scheduled = '';\nlet note = '';\nif (action === pr.opts.schedule) {\n  const raw = String(v('publish_at') || '').trim();\n  let dt = DateTime.fromFormat(raw, 'yyyy-MM-dd HH:mm', { zone: s.timezone });\n  if (!dt.isValid) dt = DateTime.fromISO(raw, { zone: s.timezone });\n  if (dt.isValid && dt > DateTime.now().plus({ minutes: 5 })) scheduled = dt.toUTC().toISO({ suppressMilliseconds: true });\n  else note = pr.lang === 'es' ? 'La fecha no era válida o ya había pasado, así que se publicó ahora.' : 'The date was not valid or had already passed, so the posts went out now.';\n}\nlet nets = v('networks');\nif (typeof nets === 'string') nets = nets.split(',');\nnets = (nets || []).map(n => String(n).trim());\nconst networks = pr.platforms_avail.filter(p => nets.includes(pr.names[p]));\nif (!networks.length) return [{ json: Object.assign(base, { decision: 'rejected' }) }];\n\nconst texts = {};\n['x_post', 'x_thread', 'linkedin', 'threads', 'bluesky', 'facebook', 'instagram', 'tiktok_title', 'tiktok_caption'].forEach(k => {\n  const t = v(k);\n  texts[k] = String(t === undefined || t === null ? (pr.drafts[k] || '') : t).split(CR).join('').trim();\n});\nconst xf = v('x_format');\nconst x_format = xf === undefined ? (String(src.x_format || '').toLowerCase() === 'thread' ? 'thread' : 'post') : (xf === pr.xopts.thread ? 'thread' : 'post');\nconst im = v('image');\nconst attach = pr.image_mode === 'all' || (Array.isArray(im) ? im.length > 0 : !!im);\nconst visual = ['instagram', 'tiktok'];\nconst needImg = pr.img_ok && networks.some(p => visual.includes(p) || attach);\nconst publisher = String(src.publisher || 'upload-post').toLowerCase().includes('blotato') ? 'blotato' : 'upload-post';\nreturn [{ json: Object.assign(base, {\n  decision: 'publish', scheduled, note, networks, texts, x_format, attach, publisher,\n  image_route: needImg ? publisher : 'none',\n  preview_url: pr.preview_url,\n  image_b64: needImg && publisher === 'upload-post' ? pr.image_b64 : ''\n}) }];"
      }
    },
    {
      "id": "4fc46bd1-8b31-47ca-8d71-0b3b2cf0f4a5",
      "name": "Publish?",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        3360,
        880
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "publish",
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.decision }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "publish"
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Rejected or expired"
        }
      }
    },
    {
      "id": "b2855fcb-cf46-43f9-acba-69ab3246158d",
      "name": "Image Route",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        3584,
        688
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Upload-Post",
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.image_route }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "upload-post"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "outputKey": "Blotato",
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.image_route }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "blotato"
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "No image"
        }
      }
    },
    {
      "id": "7f306fef-5f67-4d9e-9558-43617d348b36",
      "name": "Image File",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        3808,
        448
      ],
      "parameters": {
        "operation": "toBinary",
        "sourceProperty": "image_b64",
        "options": {
          "fileName": "post.jpg",
          "mimeType": "image/jpeg"
        }
      }
    },
    {
      "id": "5734df76-a5dd-4de7-b689-a800b82ef582",
      "name": "Build Requests",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4032,
        608
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// One item per network. Upload-Post gets multipart fields, Blotato a JSON body.\nconst NL = String.fromCharCode(10);\nconst d = $('Read Decision').first().json;\nconst pr = $('Prepare Review').first().json;\nconst src = $('Get Source').first().json;\nconst bin = $input.first().binary;\nlet media = '';\nif ($('Host Image On Blotato').isExecuted) { const r = $('Host Image On Blotato').first().json; media = r.url || r.publicUrl || ''; }\nconst hasImg = d.image_route !== 'none' && !!((bin && bin.data) || media);\nconst visualSet = ['instagram', 'tiktok'];\nconst thread = String(d.texts.x_thread || '').split(new RegExp(NL + '[ ]*' + NL)).map(t => t.trim()).filter(Boolean);\nfunction partsFor(p) {\n  if (p === 'x') return d.x_format === 'thread' && thread.length > 1 ? thread : [d.texts.x_post];\n  if (p === 'tiktok') return [d.texts.tiktok_caption];\n  return [d.texts[p]];\n}\nlet accounts = {};\ntry { accounts = JSON.parse(src.blotato_accounts || '{}'); } catch (e) { accounts = {}; }\nconst sched = d.scheduled;\nconst out = [];\nconst skip = (p, reason) => out.push({ json: { service: 'skip', platform: p, reason } });\nfor (const p of d.networks) {\n  const parts = partsFor(p).map(t => String(t || '').trim()).filter(Boolean);\n  const visual = visualSet.includes(p);\n  const useImg = hasImg && (visual || d.attach);\n  if (!parts.length) { skip(p, 'empty text'); continue; }\n  if (visual && !useImg) { skip(p, 'needs an image'); continue; }\n\n  if (d.publisher === 'blotato') {\n    const id = accounts[p] || (p === 'x' ? accounts.twitter : '');\n    if (!id) { skip(p, 'no account ID for ' + p + ' in blotato_accounts'); continue; }\n    const platform = p === 'x' ? 'twitter' : p;\n    const content = { text: parts[0], mediaUrls: useImg ? [media] : [], platform };\n    if (parts.length > 1) content.additionalPosts = parts.slice(1).map(t => ({ text: t, mediaUrls: [] }));\n    const target = { targetType: platform };\n    if (p === 'linkedin' && src.linkedin_page_id) target.pageId = String(src.linkedin_page_id);\n    if (p === 'facebook') {\n      if (!src.facebook_page_id) { skip(p, 'facebook_page_id is empty'); continue; }\n      target.pageId = String(src.facebook_page_id);\n      if (!useImg) target.link = pr.item_url;\n    }\n    if (p === 'instagram' && pr.image_alt) target.altText = pr.image_alt;\n    if (p === 'tiktok') Object.assign(target, { privacyLevel: 'PUBLIC_TO_EVERYONE', disabledComments: false, disabledDuet: false, disabledStitch: false, isBrandedContent: false, isYourBrand: false, isAiGenerated: true, title: String(d.texts.tiktok_title || '').slice(0, 90) });\n    const body = { post: { accountId: String(id), content, target } };\n    if (sched) body.scheduledTime = sched;\n    out.push({ json: { service: 'blotato', platform: p, body } });\n    continue;\n  }\n\n  // Upload-Post: up to five optional form fields, padded with harmless defaults.\n  const opts = [];\n  if (p === 'facebook' && src.facebook_page_id) opts.push(['facebook_page_id', String(src.facebook_page_id)]);\n  if (p === 'linkedin' && src.linkedin_page_id) opts.push(['target_linkedin_page_id', String(src.linkedin_page_id)]);\n  if (sched) { opts.push(['scheduled_date', sched]); opts.push(['timezone', 'UTC']); }\n  const text = parts.join(NL + NL);\n  let service = 'up_text';\n  let title = text;\n  let description = '';\n  if (useImg) {\n    service = 'up_photo';\n    if (p === 'linkedin') { title = pr.item_title || 'Image'; description = text; }\n    if (p === 'tiktok') { title = String(d.texts.tiktok_title || '').slice(0, 90) || pr.item_title; opts.push(['tiktok_description', text]); }\n    if (visual) opts.push(['is_ai_generated', 'true']);\n  } else if (['linkedin', 'facebook', 'bluesky'].includes(p)) {\n    opts.push(['link_url', pr.item_url]);\n  }\n  const fillers = service === 'up_text'\n    ? [['async_upload', 'false'], ['x_long_text_as_post', 'false'], ['threads_long_text_as_post', 'false'], ['add_to_queue', 'false'], ['nullcast', 'false']]\n    : [['async_upload', 'false'], ['x_long_text_as_post', 'false'], ['auto_add_music', 'false'], ['add_to_queue', 'false'], ['photo_cover_index', '0']];\n  for (const f of fillers) if (opts.length < 5 && !opts.some(o => o[0] === f[0])) opts.push(f);\n  const j = { service, platform: p, user: src.upload_post_user, title, description };\n  opts.slice(0, 5).forEach((o, i) => { j['o' + (i + 1) + 'n'] = o[0]; j['o' + (i + 1) + 'v'] = o[1]; });\n  const item = { json: j };\n  if (service === 'up_photo') item.binary = bin;\n  out.push(item);\n}\nreturn out;"
      }
    },
    {
      "id": "b771939a-c2d0-4206-8990-ab4c484caee4",
      "name": "Host Image On Blotato",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        3808,
        640
      ],
      "parameters": {
        "method": "POST",
        "url": "https://backend.blotato.com/v2/media",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ url: $json.preview_url }) }}",
        "options": {
          "timeout": 60000
        }
      }
    },
    {
      "id": "afcec873-225e-425a-b262-29201cc74ad4",
      "name": "Save Decision",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        3584,
        944
      ],
      "parameters": {
        "resource": "row",
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "post_id",
              "condition": "eq",
              "keyValue": "={{ $json.post_id }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "status": "={{ $json.decision }}",
            "decided_at": "={{ $json.decided_at }}",
            "image_b64": ""
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "decided_at",
              "displayName": "decided_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "image_b64",
              "displayName": "image_b64",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        }
      }
    },
    {
      "id": "a69cdb27-a9dc-4216-a679-d72aa3584b47",
      "name": "Not Published",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        3808,
        944
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "={{ $(\"Prepare Review\").first().json.lang === \"es\" ? \"No se publicó\" : \"Not published\" }}",
        "completionMessage": "={{ $(\"Prepare Review\").first().json.lang === \"es\" ? \"Las publicaciones quedan guardadas en social_posts.\" : \"The drafts stay in social_posts.\" }}"
      },
      "webhookId": "a1edad37-c226-44f8-8382-5181e196fef8"
    },
    {
      "id": "e1c3f336-a26f-4e8e-849e-87c98ae91227",
      "name": "Route Request",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        4256,
        576
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Upload-Post text",
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.service }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "up_text"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "outputKey": "Upload-Post image",
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.service }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "up_photo"
                  }
                ],
                "combinator": "and"
              }
            },
            {
              "outputKey": "Blotato",
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict"
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.service }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "blotato"
                  }
                ],
                "combinator": "and"
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Skipped"
        }
      }
    },
    {
      "id": "8a88ea13-89f8-4daf-a0b4-02fb066b98b0",
      "name": "Post Text (Upload-Post)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4480,
        416
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.upload-post.com/api/upload_text",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formData",
              "name": "user",
              "value": "={{ $json.user }}"
            },
            {
              "parameterType": "formData",
              "name": "platform[]",
              "value": "={{ $json.platform }}"
            },
            {
              "parameterType": "formData",
              "name": "title",
              "value": "={{ $json.title }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o1n }}",
              "value": "={{ $json.o1v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o2n }}",
              "value": "={{ $json.o2v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o3n }}",
              "value": "={{ $json.o3v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o4n }}",
              "value": "={{ $json.o4v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o5n }}",
              "value": "={{ $json.o5v }}"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "3b3b1eda-1e25-416d-9709-c4de2dbee8aa",
      "name": "All Results",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        4704,
        592
      ],
      "parameters": {
        "mode": "append",
        "numberInputs": 4
      }
    },
    {
      "id": "413f1e9a-9e01-4862-94be-adaf93beb9d3",
      "name": "Post Image (Upload-Post)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4480,
        608
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.upload-post.com/api/upload_photos",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formData",
              "name": "user",
              "value": "={{ $json.user }}"
            },
            {
              "parameterType": "formData",
              "name": "platform[]",
              "value": "={{ $json.platform }}"
            },
            {
              "parameterType": "formData",
              "name": "title",
              "value": "={{ $json.title }}"
            },
            {
              "parameterType": "formData",
              "name": "description",
              "value": "={{ $json.description }}"
            },
            {
              "parameterType": "formBinaryData",
              "name": "photos[]",
              "inputDataFieldName": "data"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o1n }}",
              "value": "={{ $json.o1v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o2n }}",
              "value": "={{ $json.o2v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o3n }}",
              "value": "={{ $json.o3v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o4n }}",
              "value": "={{ $json.o4v }}"
            },
            {
              "parameterType": "formData",
              "name": "={{ $json.o5n }}",
              "value": "={{ $json.o5v }}"
            }
          ]
        },
        "options": {
          "timeout": 180000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "fff3174d-2d9a-4c0d-bd9d-0fbd8e8e3239",
      "name": "Post (Blotato)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        4480,
        800
      ],
      "parameters": {
        "method": "POST",
        "url": "https://backend.blotato.com/v2/posts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {
          "timeout": 60000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "d21b6291-1084-4a2e-b720-90d108db127c",
      "name": "Collect Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4928,
        624
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const pr = $('Prepare Review').first().json;\nconst d = $('Read Decision').first().json;\nconst L = (en, es) => (pr.lang === 'es' ? es : en);\nconst esc = t => String(t || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst items = $input.all();\nconst rows = [];\nfor (let i = 0; i < items.length; i++) {\n  const req = $('Build Requests').itemMatching(i).json;\n  const res = items[i].json || {};\n  const p = req.platform;\n  let state = 'failed';\n  let url = '';\n  let info = '';\n  const errText = () => (res.error && (res.error.message || res.error.description || res.error)) || res.message || JSON.stringify(res).slice(0, 200);\n  if (req.service === 'skip') { state = 'skipped'; info = req.reason; }\n  else if (req.service === 'blotato') {\n    if (res.postSubmissionId) { state = d.scheduled ? 'scheduled' : 'sent'; info = 'Blotato ' + res.postSubmissionId; }\n    else info = errText();\n  } else {\n    const r = res.results && (res.results[p] || res.results[Object.keys(res.results)[0]]);\n    if (res.job_id) { state = 'scheduled'; info = 'Upload-Post ' + res.job_id; }\n    else if (r && r.success) { state = 'published'; url = r.url || r.post_url || ''; }\n    else if (res.success === true && !r) state = 'published';\n    else info = (r && (r.error || r.message)) || errText();\n  }\n  rows.push({ platform: p, state, url, info: String(info).slice(0, 300) });\n}\nconst ok = rows.filter(r => ['published', 'scheduled', 'sent'].includes(r.state)).length;\nconst tried = rows.filter(r => r.state !== 'skipped').length;\nconst status = ok === 0 ? 'failed' : (ok === tried ? (d.scheduled ? 'scheduled' : 'published') : 'partial');\nconst word = {\n  published: L('Published', 'Publicado'), scheduled: L('Scheduled', 'Programado'), sent: L('Sent to Blotato', 'Enviado a Blotato'),\n  failed: L('Failed', 'Falló'), skipped: L('Skipped', 'Omitido')\n};\nlet html = '<h2>' + (status === 'failed' ? L('Nothing was published', 'No se publicó nada') : L('Done', 'Listo')) + '</h2><ul>';\nfor (const r of rows) {\n  html += '<li><strong>' + esc(pr.names[r.platform] || r.platform) + '</strong>: ' + word[r.state];\n  if (r.url) html += ' (<a href=\"' + esc(r.url) + '\">' + L('view', 'ver') + '</a>)';\n  if (r.info && r.state !== 'published') html += '. ' + esc(r.info);\n  html += '</li>';\n}\nhtml += '</ul>';\nif (d.note) html += '<p>' + esc(d.note) + '</p>';\nhtml += '<p>' + L('AI cost of this post: ', 'Coste de IA de esta publicación: ') + '$' + (pr.cost_micros / 1000000).toFixed(4) + '</p>';\nreturn [{ json: { post_id: pr.post_id, status, results: JSON.stringify(rows), html, decided_at: d.decided_at } }];"
      }
    },
    {
      "id": "99aea736-b351-4ef5-9def-8bf6280331c5",
      "name": "Save Result",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        5152,
        624
      ],
      "parameters": {
        "resource": "row",
        "operation": "update",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "matchType": "anyCondition",
        "filters": {
          "conditions": [
            {
              "keyName": "post_id",
              "condition": "eq",
              "keyValue": "={{ $json.post_id }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "status": "={{ $json.status }}",
            "results": "={{ $json.results }}",
            "decided_at": "={{ $json.decided_at }}",
            "image_b64": ""
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "results",
              "displayName": "results",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "decided_at",
              "displayName": "decided_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "image_b64",
              "displayName": "image_b64",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        }
      }
    },
    {
      "id": "78f9407f-0607-4a3a-a77a-7e0228640e9e",
      "name": "Show Result",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        5376,
        624
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ $(\"Collect Results\").first().json.html }}"
      },
      "webhookId": "cd9d9869-cb21-44f4-8785-5c16458dc269"
    },
    {
      "id": "002396e3-5dd5-44d6-9e00-171374a7ebf7",
      "name": "Image Link",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        1168
      ],
      "parameters": {
        "httpMethod": "GET",
        "path": "social-image",
        "responseMode": "lastNode",
        "responseData": "firstEntryBinary",
        "responseBinaryPropertyName": "data"
      },
      "webhookId": "03210158-f304-460d-8211-13fffe389e69"
    },
    {
      "id": "b6f8aeea-d0fb-4f12-9d70-e6c7778a3424",
      "name": "Find Image",
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        224,
        1168
      ],
      "parameters": {
        "resource": "row",
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "social_posts"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "post_id",
              "condition": "eq",
              "keyValue": "={{ $json.query.post }}"
            },
            {
              "keyName": "token",
              "condition": "eq",
              "keyValue": "={{ $json.query.t }}"
            }
          ]
        },
        "limit": 1
      }
    },
    {
      "id": "44207414-ceb5-4e3c-af40-a516565b4ee1",
      "name": "Image From Table",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        448,
        1168
      ],
      "parameters": {
        "operation": "toBinary",
        "sourceProperty": "image_b64",
        "options": {
          "fileName": "post.jpg",
          "mimeType": "image/jpeg"
        }
      }
    },
    {
      "id": "00898b82-de8d-4df2-8fd4-5e07bae3cd31",
      "name": "Sticky Note cff2cee8",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2624,
        1168
      ],
      "parameters": {
        "content": "## Content to social posts, approved on WhatsApp\n\nWatches your RSS feeds and changelog pages, or takes any URL from a form. Zabio writes the posts for X (post or thread), LinkedIn, Threads, Bluesky, Facebook, Instagram and TikTok, and draws an image. You get a WhatsApp message with a review link: edit, pick the networks, publish now or schedule. Nothing goes out before you submit. Publishing runs through Upload-Post or Blotato.\n\n**Setup**\n1. Create the data tables **social_sources** and **social_posts**.\n2. Fill in **Social Settings**: your n8n address, WhatsApp phone number ID and template.\n3. Credentials: Zabio, WhatsApp, and Upload-Post or Blotato (all Header Auth except WhatsApp).\n4. Activate the workflow.\n\nGuide: remarkablecloud.com/docs/automation/content-to-social/",
        "color": 4
      }
    },
    {
      "id": "78f8df14-53da-41c4-852c-64abaf0611f1",
      "name": "Sticky Note e5ae7632",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2624,
        1424
      ],
      "parameters": {
        "content": "### 1. Find new content\nEvery 2 hours each active source is read. A source is an RSS or Atom feed, a changelog page (one entry per heading) or a single page. The first run of a feed drafts only the newest item and records the rest as seen.",
        "color": 7
      }
    },
    {
      "id": "5fea0fe7-2f61-45be-a3a2-ad5508431680",
      "name": "Sticky Note 6687859b",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2624,
        1680
      ],
      "parameters": {
        "content": "### 2. Draft one post per execution\nEach item runs on its own, so one slow review never holds up the others. Zabio writes every format in one structured call; code removes dashes, trims to each network's limit and adds the link. The image is made only when a network needs it, or when image_mode is all.",
        "color": 7
      }
    },
    {
      "id": "176d5e00-8ee4-4dc5-a4c0-5ab4af4bec2c",
      "name": "Sticky Note d984b1d8",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2624,
        1936
      ],
      "parameters": {
        "content": "### 3. Review on WhatsApp\nWhatsApp needs an approved template to message you first. Create a Utility template, for example content_review, with this body:\n\"New posts for {{1}} are ready: {{2}}. Review and publish here: {{3}}\"\nThe form waits review_hours, then the draft is marked expired.",
        "color": 7
      }
    },
    {
      "id": "760200b6-bd7e-4fb9-baac-9f28627dfdcc",
      "name": "Sticky Note 4e1c2a28",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2624,
        2192
      ],
      "parameters": {
        "content": "### 4. Publish\nUpload-Post gets one request per network, with the image as a file. Blotato gets one post per network, with the image hosted on Blotato first. Results, links and the AI cost show on the page you submitted, and in social_posts.",
        "color": 7
      }
    },
    {
      "id": "585afb68-0d3c-4a2b-a8f1-0eaf615fc846",
      "name": "Sticky Note ea6d813d",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2624,
        2448
      ],
      "parameters": {
        "content": "### Image link\nServes the draft image to the review form and to Blotato. It only answers while the workflow is active, and only with the right token. The image is removed from the table once you decide.",
        "color": 7
      }
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
