{
  "name": "Turn new blog articles into social posts with AI, Antwork and Slack approval",
  "nodes": [
    {
      "parameters": {
        "content": "## Turn new blog articles into social posts, approved in Slack\n\nEvery new article in your RSS feed becomes one draft per social account, each written in that account's own voice from its recent posts. You approve the set in Slack, and Antwork schedules it. Nothing is posted without your click.\n\n### How it works\n1. **New article** checks your feed every hour; **Up to 3 per run** stops a first run from flooding you.\n2. **Fetch the article** and **Extract text and image** read the full article and its `og:image`, not just the feed summary.\n3. **Draft one post per account** is an AI Agent with the Antwork MCP tool. It reads each account's recent and best-performing posts (`get_post_context`), writes a different post for each, saves them as one campaign of drafts (`create_campaign`) and attaches the article image (`add_media`), which Instagram and Pinterest need to publish.\n4. **Ask for approval** sends the drafts, with their edit links, to Slack and waits.\n5. Approve: **Schedule the campaign** schedules every draft for your next posting slot through the Antwork API. Decline: the drafts stay in Antwork for you to edit or delete.\n\n### Requirements\n- An Antwork account with at least one social account connected (LinkedIn, X, Instagram, Threads, Bluesky, Facebook, Mastodon, Pinterest...).\n- An OpenAI API key (or any other chat model).\n- A Slack workspace.\n\n### Setup steps\n1. In Antwork, go to Settings, then Developer, and create an API key with the read, write, media and publish scopes. In n8n, create one **Bearer Auth** credential with it; **Antwork** and **Schedule the campaign** both use it.\n2. **New article**: set your feed URL.\n3. **Your settings**: the accounts to post to (`all`, or comma-separated account ids), the hour to post and your timezone.\n4. **OpenAI Chat Model**: add your OpenAI credential.\n5. **Ask for approval** and **Confirm in Slack**: connect Slack and pick a channel.\n\n### Customization\n- Replace Slack with Gmail, Telegram or Discord: each has a Send and Wait for Response operation.\n- Change the system message in **Draft one post per account** for a different tone, a call to action or hashtag rules.\n- The agent's tools are limited to reading and drafting. Scheduling only happens in **Schedule the campaign**, after your approval.\n\nSetup guide: https://antwork.io/automate/n8n",
        "height": 1100,
        "width": 640
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c01",
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -300
      ]
    },
    {
      "parameters": {
        "content": "## 1. New article\nRead the feed, then the full article and its image.",
        "height": 380,
        "width": 1120,
        "color": 7
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c02",
      "name": "Section: source",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -120
      ]
    },
    {
      "parameters": {
        "content": "## 2. Draft in each account's voice\nOne campaign of drafts in Antwork, one post per account.",
        "height": 640,
        "width": 620,
        "color": 7
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c03",
      "name": "Section: draft",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1160,
        -120
      ]
    },
    {
      "parameters": {
        "content": "## 3. Approve in Slack, then schedule",
        "height": 560,
        "width": 900,
        "color": 7
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c04",
      "name": "Section: approve",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1820,
        -120
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyHour"
            }
          ]
        },
        "feedUrl": "https://example.com/feed.xml"
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c10",
      "name": "New article",
      "type": "n8n-nodes-base.rssFeedReadTrigger",
      "typeVersion": 1,
      "position": [
        40,
        40
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c40",
              "name": "account_ids",
              "type": "string",
              "value": "all"
            },
            {
              "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c41",
              "name": "post_hour",
              "type": "number",
              "value": 9
            },
            {
              "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c42",
              "name": "timezone",
              "type": "string",
              "value": "UTC"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c11",
      "name": "Your settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        40
      ]
    },
    {
      "parameters": {
        "maxItems": 3
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c12",
      "name": "Up to 3 per run",
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        480,
        40
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.link }}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "responseFormat": "text"
            }
          },
          "timeout": 15000
        }
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c13",
      "name": "Fetch the article",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        700,
        40
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "extractHtmlContent",
        "dataPropertyName": "data",
        "extractionValues": {
          "values": [
            {
              "key": "image",
              "cssSelector": "meta[property=\"og:image\"]",
              "returnValue": "attribute",
              "attribute": "content"
            },
            {
              "key": "article_text",
              "cssSelector": "article, main",
              "returnValue": "text"
            }
          ]
        },
        "options": {
          "trimValues": true,
          "cleanUpText": true
        }
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c14",
      "name": "Extract text and image",
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [
        920,
        40
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=New article. Draft one post per account.\n\nTitle: {{ $('New article').item.json.title }}\nLink: {{ $('New article').item.json.link }}\nSummary: {{ $('New article').item.json.contentSnippet }}\nArticle text: {{ ($json.article_text || '').slice(0, 6000) }}\nImage: {{ $json.image || '' }}\nAccounts: {{ $('Your settings').item.json.account_ids }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You turn a new blog article into social post drafts in Antwork. You only draft: a person approves the drafts in Slack before anything is scheduled.\n\n1. Accounts. If Accounts is a list of account ids, use exactly those. If it is \"all\", call list_social_accounts and use every connected account except TikTok and YouTube, which need a video.\n2. Call get_post_context once with account_ids set to those ids. Each account's recentPosts are its voice: match its length, hooks, line breaks, emoji and hashtag habits. performance.topPosts shows what worked best there; lean on those hooks and topics. Voice is per account: never reuse one text for two accounts.\n3. If Image is empty, leave out Instagram and Pinterest accounts: they cannot publish without an image or video.\n4. Write one post per account about the article, in that account's voice and within its platform's character limit. Lead with the article's most useful idea, not its title. Include the article link, except on Instagram, where links are not clickable.\n5. Call create_campaign once, with one variant per account: {\"account_id\": \"...\", \"text\": \"...\"}. This saves drafts and publishes nothing.\n6. If Image is a URL, call add_media with url set to it and attach_to_post_ids set to every postId create_campaign returned.\n\nYou cannot schedule or publish, so never say that anything was posted. Answer with the campaign id and, for each draft created, its platform, account name, text and editUrl.",
          "maxIterations": 12
        }
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c15",
      "name": "Draft one post per account",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 2.2,
      "position": [
        1300,
        40
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "gpt-4.1-mini"
        },
        "options": {}
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c16",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "endpointUrl": "https://api.antwork.io/mcp",
        "serverTransport": "httpStreamable",
        "authentication": "bearerAuth",
        "include": "selected",
        "includeTools": [
          "list_social_accounts",
          "get_post_context",
          "create_campaign",
          "add_media"
        ],
        "options": {
          "timeout": 120000
        }
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c17",
      "name": "Antwork",
      "type": "@n8n/n8n-nodes-langchain.mcpClientTool",
      "typeVersion": 1.2,
      "position": [
        1400,
        300
      ]
    },
    {
      "parameters": {
        "jsonSchemaExample": "{\n  \"campaign_id\": \"cmp_123\",\n  \"posts\": [\n    {\n      \"platform\": \"linkedin\",\n      \"account\": \"Acme\",\n      \"text\": \"The post text\",\n      \"edit_url\": \"https://antwork.io/workspace/abc/compose?postId=xyz\"\n    }\n  ]\n}"
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c18",
      "name": "Drafts as JSON",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.2,
      "position": [
        1600,
        300
      ]
    },
    {
      "parameters": {
        "operation": "sendAndWait",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "message": "=*New drafts for* <{{ $('New article').item.json.link }}|{{ $('New article').item.json.title }}>\n\n{{ $('Draft one post per account').item.json.output.posts.map(p => '*' + p.platform + ' · ' + p.account + '*\\n' + p.text + '\\n<' + p.edit_url + '|Edit in Antwork>').join('\\n\\n') }}\n\nApprove to schedule them for tomorrow at {{ $('Your settings').item.json.post_hour }}:00 ({{ $('Your settings').item.json.timezone }}). Decline keeps them as drafts in Antwork.",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Schedule",
            "disapproveLabel": "Keep as drafts"
          }
        },
        "options": {}
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c19",
      "name": "Ask for approval",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1860,
        40
      ],
      "webhookId": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4d01"
    },
    {
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c43",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.data.approved }}",
              "rightValue": ""
            }
          ]
        }
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c20",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2080,
        40
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://antwork.io/api/v1/campaigns/{{ $('Draft one post per account').item.json.output.campaign_id }}/schedule",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $('Draft one post per account').item.json.output.campaign_id }}-schedule"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ scheduledFor: $now.setZone($('Your settings').item.json.timezone).plus({ days: 1 }).set({ hour: Number($('Your settings').item.json.post_hour), minute: 0, second: 0, millisecond: 0 }).toISO() }) }}",
        "options": {}
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c21",
      "name": "Schedule the campaign",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2300,
        -20
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "text": "=Scheduled: {{ $('Draft one post per account').item.json.output.posts.length }} posts about <{{ $('New article').item.json.link }}|{{ $('New article').item.json.title }}> go out tomorrow at {{ $('Your settings').item.json.post_hour }}:00 ({{ $('Your settings').item.json.timezone }}). See them in Antwork: https://antwork.io/home?to=posts",
        "otherOptions": {}
      },
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c22",
      "name": "Confirm in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2520,
        -20
      ],
      "webhookId": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4d02"
    },
    {
      "parameters": {},
      "id": "5b0c4a7e-1f2d-4c3b-9a8e-0d1e2f3a4c23",
      "name": "Declined: drafts stay in Antwork",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2300,
        200
      ]
    }
  ],
  "connections": {
    "New article": {
      "main": [
        [
          {
            "node": "Your settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Your settings": {
      "main": [
        [
          {
            "node": "Up to 3 per run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Up to 3 per run": {
      "main": [
        [
          {
            "node": "Fetch the article",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch the article": {
      "main": [
        [
          {
            "node": "Extract text and image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract text and image": {
      "main": [
        [
          {
            "node": "Draft one post per account",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft one post per account": {
      "main": [
        [
          {
            "node": "Ask for approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask for approval": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Schedule the campaign",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Declined: drafts stay in Antwork",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule the campaign": {
      "main": [
        [
          {
            "node": "Confirm in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Draft one post per account",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Antwork": {
      "ai_tool": [
        [
          {
            "node": "Draft one post per account",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Drafts as JSON": {
      "ai_outputParser": [
        [
          {
            "node": "Draft one post per account",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
