{
  "name": "Post Daily to LinkedIn and Instagram with n8n (free workflow JSON)",
  "nodes": [
    {
      "id": "bba4f2bc-e012-4ed1-bb7e-dcc3d265bcd8",
      "name": "Schedule Trigger Posts",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Runs every day at 08:00 (workflow timezone). Change the hour/interval here to adjust your posting rhythm.",
      "position": [
        608,
        128
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "notesInFlow": false,
      "typeVersion": 1.3
    },
    {
      "id": "dd4c240e-212b-46ac-b6ce-c7aa1a91f416",
      "name": "Get Post Ideas",
      "type": "n8n-nodes-base.airtable",
      "notes": "Reads every row from the 'Topics' table where Active = TRUE. This is your idea bank: one topic per row in a field called 'Topic'.",
      "position": [
        832,
        128
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {},
        "operation": "search",
        "filterByFormula": "{Active} = TRUE()"
      },
      "notesInFlow": false,
      "typeVersion": 2.2
    },
    {
      "id": "9cda55c9-b207-43d1-9bc0-190bcd013d6c",
      "name": "Check History Posts",
      "type": "n8n-nodes-base.airtable",
      "notes": "Fetches the last 14 published posts (sorted by Date, newest first). Used to avoid repeating a topic too soon. 'Always output data' is ON so an empty table does not stop the run.",
      "position": [
        1056,
        128
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sort": {
          "property": [
            {
              "field": "Date",
              "direction": "desc"
            }
          ]
        },
        "limit": 14,
        "table": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {},
        "operation": "search",
        "returnAll": false
      },
      "notesInFlow": false,
      "typeVersion": 2.2,
      "alwaysOutputData": true
    },
    {
      "id": "3523c85a-2943-4614-9d0f-3ad174742cc8",
      "name": "Filter and Choose Post Topic",
      "type": "n8n-nodes-base.code",
      "notes": "Compares the idea bank against the last 14 published posts, keeps only the topics that were NOT used recently and picks one at random. Outputs { topic, isValid }.",
      "position": [
        1280,
        128
      ],
      "parameters": {
        "jsCode": "// 1. Get the available topics from the 'Get Post Ideas' node\nconst availableRecords = $('Get Post Ideas').all();\nconst availableTopics = availableRecords.map(item => {\n  return item.json.Topic || item.json.topic || (item.json.fields ? item.json.fields.Topic : undefined);\n}).filter(t => t !== undefined);\n\n// 2. Get the LAST 14 posts from the 'Check History Posts' node\nconst historyRecords = $('Check History Posts').all();\nconst recentTopics = historyRecords.map(item => {\n  return item.json.Topic || item.json.topic || (item.json.fields ? item.json.fields.Topic : undefined);\n}).filter(t => t !== undefined);\n\n// 3. Filter: keep only the topics that were NOT posted in the last 14 posts\nconst freshTopics = availableTopics.filter(topic => !recentTopics.includes(topic));\n\n// 4. If ALL available topics were already used in the last 14 posts\nif (freshTopics.length === 0) {\n  return [{\n    json: {\n      topic: \"All available topics were already posted in the last 14 posts!\",\n      isValid: false\n    }\n  }];\n}\n\n// 5. Pick a random topic from the allowed list\nconst chosenTopic = freshTopics[Math.floor(Math.random() * freshTopics.length)];\n\nreturn [{\n  json: {\n    topic: chosenTopic,\n    isValid: true\n  }\n}];"
      },
      "notesInFlow": false,
      "typeVersion": 2
    },
    {
      "id": "32734e90-9d26-465b-9748-ac796c873128",
      "name": "Is Valid Post?",
      "type": "n8n-nodes-base.if",
      "notes": "TRUE branch = a fresh topic was found, continue. FALSE branch = every topic was already used in the last 14 runs, stop safely.",
      "position": [
        1504,
        128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c47f4a42-4f78-4660-a026-936af9adea14",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isValid }}",
              "rightValue": true
            }
          ]
        }
      },
      "notesInFlow": false,
      "typeVersion": 2.3
    },
    {
      "id": "b268a578-c2b1-4593-b8b6-5cde2aee07d5",
      "name": "No Operation, do nothing",
      "type": "n8n-nodes-base.noOp",
      "notes": "Dead end on purpose: nothing is published when no fresh topic is available. Add more rows to the Topics table (or a Slack/Email node here) if you want an alert.",
      "position": [
        1504,
        384
      ],
      "parameters": {},
      "notesInFlow": false,
      "typeVersion": 1
    },
    {
      "id": "50a851cd-bf34-49d8-8321-3591ad0ed280",
      "name": "Claude - Write Post Text",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Calls the Anthropic Messages API and writes the caption for the chosen topic. EDIT THE PROMPT INSIDE THE JSON BODY: replace the brand context, tone of voice and rules with your own.",
      "position": [
        1728,
        128
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"model\": \"claude-sonnet-5\",\n  \"max_tokens\": 500,\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": \"You are an experienced content writer for [YOUR BRAND NAME].\\n\\nBUSINESS CONTEXT:\\n[DESCRIBE YOUR BUSINESS: who you help, what you do, and what makes you different.]\\n\\nYOUR OFFER (mention it subtly when it fits the topic):\\n[LIST YOUR OFFERS OR SERVICES HERE, one per line.]\\n\\nTASK:\\nWrite a strong LinkedIn post in [LANGUAGE].\\n\\nTOPIC: {{ $json.topic }}\\n\\nSTRICT RULES:\\n- Length: between 120 and 180 words.\\n- Emojis: at most 2 or 3 subtle, professional emojis (for example a rocket or a briefcase). No excess.\\n- Hashtags: absolutely NO hashtags.\\n- Tone: professional, to the point, simple and easy to read (no complicated jargon).\\n- Structure: start with a strong opening sentence, focus on a practical solution, and ALWAYS end with a thought-provoking question.\\n\\nReturn ONLY the text of the post, without an introduction or extra comments.\"\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "nodeCredentialType": "anthropicApi"
      },
      "notesInFlow": false,
      "typeVersion": 4.4
    },
    {
      "id": "35f87aa4-6f68-4a1a-a798-f8a411f97643",
      "name": "Put Text in Variable",
      "type": "n8n-nodes-base.set",
      "notes": "Extracts the plain text out of the Anthropic API response and stores it as 'postText', so later nodes can reference it cleanly.",
      "position": [
        1952,
        128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ae899b15-0887-42ee-ac55-9cfec431c8a3",
              "name": "postText",
              "type": "string",
              "value": "={{ $json.content[1]?.text || $json.content[0]?.text }}"
            }
          ]
        }
      },
      "notesInFlow": false,
      "typeVersion": 3.4
    },
    {
      "id": "12d87a72-17fa-4ae6-afce-8380897d7cda",
      "name": "ChatGPT - Generate Image",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Generates a 1024x1024 branded illustration with the OpenAI image API. The prompt reuses the chosen topic and the generated caption, so image and text always match. Edit the style/colours in the prompt.",
      "position": [
        2176,
        128
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/images/generations",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "model",
              "value": "gpt-image-2"
            },
            {
              "name": "size",
              "value": "1024x1024"
            },
            {
              "name": "prompt",
              "value": "=A professional, modern vector illustration for [YOUR BRAND NAME], [ONE SENTENCE ABOUT WHAT YOUR BUSINESS DOES].\n\nCORE CONCEPT:\n[THE IDEA YOUR BRAND STANDS FOR].\n\nTHEME & TEXT INCLUSION:\nThe main subject of this post is: \"{{ $node[\"Is Valid Post?\"].json.topic }}\".\nInside the image, clearly and cleanly render one short, highly relevant word in [LANGUAGE] integrated into the design. Ensure perfect typography and correct spelling.\n\nINSPIRATION FROM CONTENT:\nUse the specific theme of this text to shape the main object of the illustration:\n\"{{ $json.postText }}\"\n\nCOLOR PALETTE (Mandatory):\n- Main Color: [YOUR MAIN COLOR, for example a hex code]\n- Secondary Color: [YOUR SECONDARY COLOR]\n- Accents: Deep Charcoal/Black and Pure White\n\nSTYLE DETAILS:\n- Clean 2D vector art with smooth, modern gradients.\n- Visually dynamic: create a central, recognizable metaphor for [THE MAIN IDEA OF YOUR BRAND].\n- Constraints: White background only. No photorealism. No corporate logos. No random gibberish text."
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "notesInFlow": false,
      "typeVersion": 4.4
    },
    {
      "id": "fdda7c03-2010-4f0a-a127-7d984e70fc00",
      "name": "Base64 to PNG",
      "type": "n8n-nodes-base.convertToFile",
      "notes": "The image API returns base64. This node converts data[0].b64_json into a real binary PNG file so it can be uploaded.",
      "position": [
        2400,
        128
      ],
      "parameters": {
        "options": {
          "fileName": "social-image.png"
        },
        "operation": "toBinary",
        "sourceProperty": "data[0].b64_json"
      },
      "notesInFlow": false,
      "typeVersion": 1.1
    },
    {
      "id": "32feec4b-44a7-4b33-a944-fc9bfde7589e",
      "name": "Upload Post to ImgBB",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Social APIs need a public image URL, not a file. ImgBB hosts the PNG and returns that URL. Paste your own free ImgBB API key in the 'key' query parameter (or swap this node for S3 / Cloudinary / Drive).",
      "position": [
        2688,
        128
      ],
      "parameters": {
        "url": "https://api.imgbb.com/1/upload",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendQuery": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "image",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            }
          ]
        },
        "queryParameters": {
          "parameters": [
            {
              "name": "key",
              "value": "YOUR_IMGBB_API_KEY"
            }
          ]
        }
      },
      "notesInFlow": false,
      "typeVersion": 4.4
    },
    {
      "id": "dca0a5e7-14de-4a51-86f3-fa394f6a9377",
      "name": "Create LinkedIn Post",
      "type": "@bufferapp/n8n-nodes-buffer.buffer",
      "notes": "Sends caption + image URL to Buffer for the LinkedIn channel. Fill in your own Buffer organization ID and channel ID.",
      "position": [
        2912,
        128
      ],
      "parameters": {
        "imageUrl": "={{ $json.data.image.url }}",
        "postText": "={{ $('Put Text in Variable').item.json.postText }}",
        "resource": "post",
        "channelId": "",
        "attachmentType": "image",
        "organizationId": ""
      },
      "notesInFlow": false,
      "typeVersion": 1
    },
    {
      "id": "c3e6edc3-f458-4b77-a6b3-c2eddaa2d6a6",
      "name": "Create Instagram Post",
      "type": "@bufferapp/n8n-nodes-buffer.buffer",
      "notes": "Sends the same caption + image URL to Buffer for the Instagram channel. Fill in your own Buffer organization ID and channel ID.",
      "position": [
        3136,
        128
      ],
      "parameters": {
        "imageUrl": "={{ $('Upload Post to ImgBB').item.json.data.image.url }}",
        "postText": "={{ $('Put Text in Variable').item.json.postText }}",
        "resource": "post",
        "channelId": "",
        "attachmentType": "image",
        "organizationId": ""
      },
      "notesInFlow": false,
      "typeVersion": 1
    },
    {
      "id": "4d942c34-a15c-45a4-a5f8-f8309c4c31d9",
      "name": "Log Posted Content",
      "type": "n8n-nodes-base.airtable",
      "notes": "Writes topic, caption and image URL to the 'Posts' table. This is what makes the 14-day anti-repeat memory work, so do not remove it.",
      "position": [
        3360,
        128
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "value": {
            "Post": "={{ $('Put Text in Variable').item.json.postText }}",
            "Topic": "={{ $('Is Valid Post?').item.json.topic }}",
            "Image URL": "={{ $('Upload Post to ImgBB').item.json.data.image.url }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "dateTime",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Topic",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Post",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Post",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Image URL",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Image URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Platform",
              "type": "array",
              "display": true,
              "options": [
                {
                  "name": "LinkedIn",
                  "value": "LinkedIn"
                },
                {
                  "name": "Instagram",
                  "value": "Instagram"
                }
              ],
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "Platform",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "create"
      },
      "notesInFlow": false,
      "typeVersion": 2.2
    },
    {
      "id": "7f9ce8de-30bd-4e5c-8fa3-8ffbe9e34967",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 1120,
        "height": 520,
        "content": "## 1. Pick a fresh topic\nLoads your active topics and the last 14 published posts, then randomly picks one that was not used recently. If every topic has been used, the run stops instead of repeating itself."
      },
      "typeVersion": 1
    },
    {
      "id": "6c206ae9-6f9a-406c-888d-9070d002eddd",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 930,
        "height": 520,
        "content": "## 2. Generate the content\nClaude writes the caption, OpenAI illustrates it, and the base64 response is converted into a real PNG file."
      },
      "typeVersion": 1
    },
    {
      "id": "cb613965-ae55-427e-9075-35d1da535f6e",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2640,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 970,
        "height": 520,
        "content": "## 3. Publish and remember\nImgBB turns the PNG into a public URL, Buffer schedules it on LinkedIn and Instagram, and Airtable logs it as tomorrow's memory."
      },
      "typeVersion": 1
    },
    {
      "id": "d842558e-3730-454e-a37f-63e33a30c81a",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        1144
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 960,
        "content": "## 🗂️ Airtable setup\n\nCreate one base with these tables.\n\n**Topics** — your idea bank\n| Field | Type |\n|---|---|\n| `Topic` | Single line text |\n| `Active` | Checkbox |\n\nOnly checked rows are used. Fill it with 20-40 ideas; the workflow picks one at random per run.\n\n**Posts** — the history it remembers\n| Field | Type |\n|---|---|\n| `Date` | Created time |\n| `Topic` | Single line text |\n| `Post` | Long text |\n| `Image URL` | URL |\n\n⚠️ `Date` must be a **Created time** field, not a normal date. The workflow never writes it, but it sorts on it to find the last 14 items. Without it the anti-repeat check silently breaks.\n\n**How the anti-repeat works:** active topics minus the last 14 logged rows = the pool it picks from. Empty pool means the run stops instead of repeating content, so keep 15+ topics active."
      },
      "typeVersion": 1
    },
    {
      "id": "edc088ec-3634-4c9e-b83d-13cd7bd2c7ea",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 500,
        "height": 1100,
        "content": "## 📅 Daily AI Social Post Engine\n\nEvery morning this workflow picks a topic it has not used recently, writes the caption with Claude, generates a matching AI image, and schedules it to LinkedIn and Instagram through Buffer.\n\n### How it works\n1. The schedule trigger fires daily at 08:00.\n2. Airtable returns your active topics plus the last 14 published posts.\n3. A Code node randomly picks a topic that is not in that history, so content never repeats.\n4. Claude writes the caption, then OpenAI generates an image from that same caption so text and visual always match.\n5. The base64 image becomes a real PNG, ImgBB hosts it and returns the public URL the social APIs require.\n6. Buffer schedules the post and Airtable logs it as tomorrow's memory.\n\n### Setup\n1. Create credentials for Airtable, Anthropic, OpenAI and Buffer.\n2. Build the Airtable base described in the grey note below this one.\n3. Add a free ImgBB API key, then pick your organization and channel in both Buffer nodes.\n4. Keep at least 15 active topics, otherwise the workflow stops instead of repeating itself.\n\n### Customization\nEdit the Claude prompt for your tone of voice and the image prompt for your brand style. Swap ImgBB for S3, Drive or Cloudinary if you prefer.\n\n### Before you turn it on\n1. Replace every [BRACKETED] placeholder in both prompts (Claude and image). If you forget, the placeholder text can end up in a real post.\n2. Run it by hand first, with a Buffer channel you can safely post to. Each run writes a row to your Posts table and can create real posts.\n3. The two Buffer nodes come from a Buffer community node. If n8n shows them as unknown, check Buffer's current n8n documentation for how to connect.\n4. The ImgBB key sits in the upload node's key field. Do not share or export this workflow after you paste your key.\n5. Review what gets published. The text and image are AI-generated, and platform rules and local laws still apply to you.\n\nSource: based on a template from the n8n Library at n8n.io, documented by LeenLabs.\nNot tested on live APIs."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Base64 to PNG": {
      "main": [
        [
          {
            "node": "Upload Post to ImgBB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Post Ideas": {
      "main": [
        [
          {
            "node": "Check History Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Valid Post?": {
      "main": [
        [
          {
            "node": "Claude - Write Post Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Operation, do nothing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check History Posts": {
      "main": [
        [
          {
            "node": "Filter and Choose Post Topic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create LinkedIn Post": {
      "main": [
        [
          {
            "node": "Create Instagram Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Put Text in Variable": {
      "main": [
        [
          {
            "node": "ChatGPT - Generate Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Post to ImgBB": {
      "main": [
        [
          {
            "node": "Create LinkedIn Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Instagram Post": {
      "main": [
        [
          {
            "node": "Log Posted Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger Posts": {
      "main": [
        [
          {
            "node": "Get Post Ideas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ChatGPT - Generate Image": {
      "main": [
        [
          {
            "node": "Base64 to PNG",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude - Write Post Text": {
      "main": [
        [
          {
            "node": "Put Text in Variable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter and Choose Post Topic": {
      "main": [
        [
          {
            "node": "Is Valid Post?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}