{
  "id": "FRfUJmvevN8j6X3k",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Schedule and optimize social media posts to Twitter and LinkedIn using AI",
  "tags": [],
  "nodes": [
    {
      "id": "40b15783-6b4b-4ba9-ae47-cf1ad563e85a",
      "name": "Main Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        880
      ],
      "parameters": {
        "width": 380,
        "height": 474,
        "content": "## How it works\nThis workflow automates your social media presence. It monitors a Google Sheet for scheduled posts, uses AI to optimize captions and hashtags for specific platforms (Twitter and LinkedIn), and publishes them automatically. Finally, it updates the post status and notifies you via Slack.\n\n## Setup steps\n1. **Spreadsheet**: Create a Google Sheet with columns: `status`, `content`, `platforms`, `scheduled_time`, and `hashtags`.\n2. **Credentials**: Connect your Google Sheets, OpenAI, Twitter, LinkedIn, and Slack accounts.\n3. **Node Config**: Select your specific spreadsheet in both the 'Fetch' and 'Update' Google Sheets nodes.\n4. **Test**: Use the 'Manual Post Trigger' to verify the flow before enabling the 'Hourly' schedule."
      },
      "typeVersion": 1
    },
    {
      "id": "7481371a-76bf-4f28-9660-f64fcb85b9d2",
      "name": "Section Sticky 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        1216
      ],
      "parameters": {
        "color": 7,
        "width": 712,
        "height": 528,
        "content": "## 1. Data Retrieval\nTriggers the workflow hourly or via webhook and pulls the latest content queue from Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "a3ad0557-0cd1-4e98-95c2-10be485522c4",
      "name": "Section Sticky 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        464,
        1200
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 528,
        "content": "## 2. AI Optimization\nFilters posts ready for publishing. The AI Agent then rewrites content to fit platform constraints (e.g., character limits) and generates hashtags."
      },
      "typeVersion": 1
    },
    {
      "id": "0bd3a028-f758-4654-9f6a-c6c4e13caf0c",
      "name": "Section Sticky 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1440,
        1088
      ],
      "parameters": {
        "color": 7,
        "width": 260,
        "height": 592,
        "content": "## 3. Social Publishing\nDistributes the optimized content to Twitter and LinkedIn simultaneously."
      },
      "typeVersion": 1
    },
    {
      "id": "5ba26121-7b57-4ecd-b073-ed05f98eda96",
      "name": "Section Sticky 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2080,
        1088
      ],
      "parameters": {
        "color": 7,
        "width": 268,
        "height": 544,
        "content": "## 4. Reporting & Response\nAggregates results, logs post URLs back to the spreadsheet, and sends a summary report to your Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "e6fa39a6-3eb5-4896-b0e8-f178efdae2ed",
      "name": "Hourly Content Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -192,
        1392
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f601cb08-1506-4e66-be27-0c9ac5db491c",
      "name": "Manual Post Trigger",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        -192,
        1600
      ],
      "parameters": {
        "path": "social-post",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "448d52f3-7481-4fef-a4cc-109fbbf18457",
      "name": "Merge Triggers",
      "type": "n8n-nodes-base.merge",
      "position": [
        32,
        1488
      ],
      "parameters": {
        "mode": "chooseBranch"
      },
      "typeVersion": 3
    },
    {
      "id": "42458dfc-9d2e-4fb6-b0ad-47b9f8a69c96",
      "name": "Fetch Content Queue",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        256,
        1488
      ],
      "parameters": {
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "f8b72bff-8883-4e4f-834c-56763a6fa9aa",
      "name": "Filter Ready Posts",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        1488
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst now = new Date();\nconst readyPosts = [];\n\nfor (const item of items) {\n  const row = item.json;\n  \n  // Check if post is scheduled and not yet published\n  const status = (row.status || '').toLowerCase();\n  if (status !== 'scheduled' && status !== 'ready') continue;\n  \n  // Check scheduled time\n  const scheduledTime = row.scheduled_time || row.scheduledTime;\n  if (scheduledTime) {\n    const schedDate = new Date(scheduledTime);\n    if (schedDate > now) continue; // Not yet time\n  }\n  \n  // Validate required fields\n  if (!row.content && !row.text && !row.message) continue;\n  \n  const platforms = (row.platforms || row.platform || 'twitter,linkedin')\n    .split(',')\n    .map(p => p.trim().toLowerCase());\n  \n  readyPosts.push({\n    json: {\n      id: row.id || row.row_number || Date.now(),\n      content: row.content || row.text || row.message,\n      platforms: platforms,\n      imageUrl: row.image_url || row.imageUrl || null,\n      scheduledTime: scheduledTime,\n      category: row.category || 'general',\n      campaign: row.campaign || '',\n      hashtags: row.hashtags || '',\n      linkUrl: row.link_url || row.linkUrl || '',\n      tone: row.tone || 'professional'\n    }\n  });\n}\n\nif (readyPosts.length === 0) {\n  return [{ json: { noContent: true, message: 'No posts ready to publish' } }];\n}\n\nreturn readyPosts;"
      },
      "typeVersion": 2
    },
    {
      "id": "fced1dc4-1620-413b-ab3f-4b3d3debdf3c",
      "name": "Has Content to Post?",
      "type": "n8n-nodes-base.if",
      "position": [
        704,
        1488
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "content-check",
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.noContent }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "69efadfa-627c-41f3-ba12-3c57df88b1a2",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1008,
        1584
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {
          "temperature": 0.7
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "38e02a47-f9b7-4b73-ac91-e3491affe50a",
      "name": "AI Content Optimizer",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        928,
        1360
      ],
      "parameters": {
        "text": "=Optimize this social media post for maximum engagement:\n\nOriginal Content: {{ $json.content }}\nPlatforms: {{ $json.platforms.join(', ') }}\nTone: {{ $json.tone }}\nCategory: {{ $json.category }}\nExisting Hashtags: {{ $json.hashtags }}\nLink: {{ $json.linkUrl }}\n\nProvide optimized versions in this exact JSON format:\n{\n  \"twitter\": {\n    \"text\": \"optimized text for Twitter (max 280 chars)\",\n    \"hashtags\": [\"relevant\", \"hashtags\", \"max5\"]\n  },\n  \"linkedin\": {\n    \"text\": \"optimized text for LinkedIn (professional tone, can be longer)\",\n    \"hashtags\": [\"professional\", \"hashtags\"]\n  },\n  \"engagementTips\": [\"tip1\", \"tip2\"],\n  \"bestPostTime\": \"suggested time\",\n  \"contentScore\": 0-100\n}",
        "options": {
          "systemMessage": "You are a social media marketing expert. Optimize content for engagement while maintaining brand voice. Keep Twitter posts concise and punchy, LinkedIn posts professional and insightful. Include relevant trending hashtags when appropriate."
        }
      },
      "typeVersion": 1.7
    },
    {
      "id": "eb6268f6-9603-43f5-9c03-e5ccd0ae98e4",
      "name": "Parse AI Content",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        1360
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst originalData = $('Filter Ready Posts').item.json;\n\nlet optimized;\ntry {\n  const responseText = input.output || input.text || '';\n  const jsonMatch = responseText.match(/\\{[\\s\\S]*\\}/);\n  optimized = jsonMatch ? JSON.parse(jsonMatch[0]) : null;\n} catch (e) {\n  optimized = null;\n}\n\nif (!optimized) {\n  // Fallback to original content\n  optimized = {\n    twitter: {\n      text: originalData.content.substring(0, 280),\n      hashtags: originalData.hashtags ? originalData.hashtags.split(' ') : []\n    },\n    linkedin: {\n      text: originalData.content,\n      hashtags: originalData.hashtags ? originalData.hashtags.split(' ') : []\n    },\n    engagementTips: [],\n    bestPostTime: 'Now',\n    contentScore: 70\n  };\n}\n\n// Build final posts\nconst twitterText = optimized.twitter.text + \n  (optimized.twitter.hashtags?.length > 0 ? ' ' + optimized.twitter.hashtags.map(h => h.startsWith('#') ? h : '#' + h).join(' ') : '');\n\nconst linkedinText = optimized.linkedin.text + \n  (optimized.linkedin.hashtags?.length > 0 ? '\\n\\n' + optimized.linkedin.hashtags.map(h => h.startsWith('#') ? h : '#' + h).join(' ') : '');\n\nreturn [{\n  json: {\n    ...originalData,\n    optimized: optimized,\n    posts: {\n      twitter: {\n        text: twitterText.substring(0, 280),\n        ready: originalData.platforms.includes('twitter')\n      },\n      linkedin: {\n        text: linkedinText,\n        ready: originalData.platforms.includes('linkedin')\n      }\n    },\n    contentScore: optimized.contentScore || 70,\n    processedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4d39d9f2-9aef-4bb1-8f2b-bb2c20895f34",
      "name": "Post to Twitter",
      "type": "n8n-nodes-base.twitter",
      "position": [
        1504,
        1264
      ],
      "parameters": {
        "text": "={{ $json.posts.twitter.text }}",
        "additionalFields": {}
      },
      "typeVersion": 2
    },
    {
      "id": "4c609576-3906-4822-9c82-ed064fe5b6c6",
      "name": "Post to LinkedIn",
      "type": "n8n-nodes-base.linkedIn",
      "position": [
        1504,
        1488
      ],
      "parameters": {
        "text": "={{ $json.posts.linkedin.text }}",
        "person": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.linkedinPersonId || '' }}"
        },
        "additionalFields": {}
      },
      "typeVersion": 1
    },
    {
      "id": "f0a19a81-e1db-40ee-92e4-0ecfee3d8474",
      "name": "Aggregate Post Results",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1728,
        1360
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "3533a23e-9b66-4b93-bb00-990c00f15dec",
      "name": "Format Results",
      "type": "n8n-nodes-base.code",
      "position": [
        1952,
        1360
      ],
      "parameters": {
        "jsCode": "const results = $input.first().json.data || [];\nconst originalData = $('Parse AI Content').first().json;\n\nconst postResults = {\n  twitter: null,\n  linkedin: null\n};\n\nfor (const result of results) {\n  if (result.id_str || result.data?.id) {\n    postResults.twitter = {\n      success: true,\n      postId: result.id_str || result.data?.id,\n      url: `https://twitter.com/i/status/${result.id_str || result.data?.id}`\n    };\n  }\n  if (result.id && String(result.id).includes('urn:li:share')) {\n    postResults.linkedin = {\n      success: true,\n      postId: result.id,\n      url: `https://linkedin.com/feed/update/${result.id}`\n    };\n  }\n}\n\nconst successCount = Object.values(postResults).filter(p => p?.success).length;\nconst totalPlatforms = originalData.platforms.length;\n\nreturn [{\n  json: {\n    contentId: originalData.id,\n    originalContent: originalData.content.substring(0, 100) + '...',\n    platforms: originalData.platforms,\n    postResults: postResults,\n    summary: {\n      successCount: successCount,\n      totalPlatforms: totalPlatforms,\n      allSuccessful: successCount === totalPlatforms\n    },\n    contentScore: originalData.contentScore,\n    publishedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b86abd85-723d-49bb-b82e-4500aeeed486",
      "name": "Update Content Status",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2176,
        1264
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "11bce6ef-c71e-4762-b573-853e8ecf138f",
      "name": "Post Summary to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        2176,
        1456
      ],
      "parameters": {
        "text": "=:mega: *Social Media Post Published*\n\n*Platforms:* {{ $json.platforms.join(', ') }}\n*Success:* {{ $json.summary.successCount }}/{{ $json.summary.totalPlatforms }}\n*Content Score:* {{ $json.contentScore }}/100\n\n*Preview:*\n{{ $json.originalContent }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#social-media"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "a43c1f57-6383-426c-92a3-07a3c7b61bad",
      "name": "No Content Response",
      "type": "n8n-nodes-base.set",
      "position": [
        2176,
        1680
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1",
              "name": "message",
              "type": "string",
              "value": "No content scheduled for posting"
            },
            {
              "id": "2",
              "name": "timestamp",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "896c50a0-3c34-466a-a5d3-5655a732033d",
      "name": "Merge Final Paths",
      "type": "n8n-nodes-base.merge",
      "position": [
        2400,
        1456
      ],
      "parameters": {
        "mode": "chooseBranch"
      },
      "typeVersion": 3
    },
    {
      "id": "975e2adf-b180-4333-a226-6f6d451999a5",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2624,
        1456
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {success: true, published: $json.summary ? $json.summary.successCount : 0, message: $json.message || 'Posts published successfully'} }}"
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "934cc699-ab8d-48c6-b96b-7785202073da",
  "connections": {
    "Format Results": {
      "main": [
        [
          {
            "node": "Update Content Status",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post Summary to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Triggers": {
      "main": [
        [
          {
            "node": "Fetch Content Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to Twitter": {
      "main": [
        [
          {
            "node": "Aggregate Post Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Content": {
      "main": [
        [
          {
            "node": "Post to Twitter",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post to LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to LinkedIn": {
      "main": [
        [
          {
            "node": "Aggregate Post Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Final Paths": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Content Optimizer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Filter Ready Posts": {
      "main": [
        [
          {
            "node": "Has Content to Post?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Content Queue": {
      "main": [
        [
          {
            "node": "Filter Ready Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Post Trigger": {
      "main": [
        [
          {
            "node": "Merge Triggers",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "No Content Response": {
      "main": [
        [
          {
            "node": "Merge Final Paths",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "AI Content Optimizer": {
      "main": [
        [
          {
            "node": "Parse AI Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Content to Post?": {
      "main": [
        [
          {
            "node": "AI Content Optimizer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Content Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hourly Content Check": {
      "main": [
        [
          {
            "node": "Merge Triggers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Summary to Slack": {
      "main": [
        [
          {
            "node": "Merge Final Paths",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Content Status": {
      "main": [
        [
          {
            "node": "Merge Final Paths",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Post Results": {
      "main": [
        [
          {
            "node": "Format Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}