This workflow corresponds to n8n.io template #13540 — we link there as the canonical source.
This workflow follows the HTTP Request → OpenAI recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "f133eb2f-a75f-4891-8adb-b8809a3703a0",
"name": "\ud83d\udccb Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2256,
-928
],
"parameters": {
"width": 560,
"height": 468,
"content": "AI Social Media Auto-Scheduler via UploadToURL\nAutomate the bridge between local media files and live social posts. This workflow hosts your assets via UploadToURL, generates platform-specific copy with AI, and schedules the results via Buffer.\n\n\u2699\ufe0f How it works\nWebhook receives a file (Binary or URL) and platform preferences.\n\nUploadToURL hosts the asset and returns a public CDN link.\n\nOpenAI generates a platform-optimized caption, hashtags, and alt-text.\n\nBuffer schedules the post to Twitter, Instagram, or LinkedIn.\n\n\ud83d\udd27 Setup\nInstall n8n-nodes-uploadtourl community node.\n\nAdd UploadToURL, OpenAI, and Buffer credentials.\n\nSet variables: BUFFER_PROFILE_TWITTER, BUFFER_PROFILE_INSTAGRAM, BUFFER_PROFILE_LINKEDIN."
},
"typeVersion": 1
},
{
"id": "efc15edf-aa70-4916-8862-a9ccf488650f",
"name": "Section 1 \u2014 Intake",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1328,
-368
],
"parameters": {
"color": 7,
"width": 420,
"height": 616,
"content": "## 1 \u2014 Intake & validation\n\n**Webhook \u2192 Validate & Enrich \u2192 Has Remote URL?**\n\nThe webhook accepts a POST with `fileUrl` or binary data plus `platform`, `tone`, `brand`, `campaignContext`, and optional `scheduleTime`.\n\nThe code node checks that at least a filename or URL is present, validates the platform against the allowed list, maps per-platform character limits (Twitter 280, Instagram 2200, LinkedIn 3000), detects content type from the file extension, and cleans the filename into readable words for the AI prompt.\n\nThe IF node then routes to the correct upload path based on whether `fileUrl` is set."
},
"typeVersion": 1
},
{
"id": "7019ee99-0415-4fe0-bde9-2dc1968482e3",
"name": "Section 2 \u2014 Upload",
"type": "n8n-nodes-base.stickyNote",
"position": [
-816,
-400
],
"parameters": {
"color": 7,
"width": 420,
"height": 656,
"content": "## 2 \u2014 File hosting via UploadToURL\n\n**Upload to URL (\u00d72) \u2192 Extract clean URL**\n\nTwo parallel branches handle remote URLs and binary uploads using the native **UploadToURL community node** \u2014 no custom HTTP request needed.\n\nThe code node after both branches normalises the response (different field names across API versions) and throws a descriptive error if no URL is returned, so failures are easy to debug in the execution log."
},
"typeVersion": 1
},
{
"id": "dac3623b-f83e-4c52-ba1e-07f1fb18e8b3",
"name": "Section 3 \u2014 Caption",
"type": "n8n-nodes-base.stickyNote",
"position": [
-288,
-352
],
"parameters": {
"color": 7,
"width": 420,
"height": 548,
"content": "## 3 \u2014 AI caption generation\n\n**OpenAI \u2192 Assemble post payload**\n\nOpenAI GPT-4.1 mini receives the platform, tone, brand, campaign context, and character limit. The response format is set to `json_object` so the output is always structured \u2014 no parsing of freetext.\n\nThe returned fields are: `caption`, `hashtags[]`, `altText`, `hook`, `bestPostingTimeUTC`, and `estimatedEngagement`.\n\nThe assemble node appends hashtags, checks the character limit, and calculates the schedule time \u2014 using the AI's UTC hour suggestion if none was provided in the original request."
},
"typeVersion": 1
},
{
"id": "b40c1ab1-1bbd-4d4a-8f3a-8a9f9a76b8d8",
"name": "Section 4 \u2014 Scheduling",
"type": "n8n-nodes-base.stickyNote",
"position": [
272,
-528
],
"parameters": {
"color": 7,
"width": 420,
"height": 772,
"content": "## 4 \u2014 Platform routing & scheduling\n\n**Switch \u2192 Buffer (Twitter / Instagram / LinkedIn) \u2192 Response**\n\nThe Switch node routes on the `platform` value. Each Buffer branch is slightly different: Instagram includes the `photo` field, LinkedIn includes a `title`, and Twitter enforces the 280-character limit set earlier.\n\nAll three branches merge into the same response node, which returns `scheduleId`, `assetUrl`, `caption`, `hashtags`, and `estimatedEngagement`.\n\nTo add more platforms (Facebook, TikTok), add a new output on the Switch node and duplicate one of the Buffer nodes."
},
"typeVersion": 1
},
{
"id": "864f54db-ad25-43da-88e2-f8d532ca009a",
"name": "Webhook - Receive Asset",
"type": "n8n-nodes-base.webhook",
"position": [
-1296,
0
],
"parameters": {
"path": "social-asset-upload",
"options": {
"allowedOrigins": "*"
},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "884d58fc-3087-4b7a-ae3f-6e726cb0731e",
"name": "Validate & Enrich Payload",
"type": "n8n-nodes-base.code",
"position": [
-1072,
0
],
"parameters": {
"jsCode": "const body = $input.first().json.body || $input.first().json;\nconst allowedPlatforms = ['instagram', 'twitter', 'linkedin', 'facebook'];\nconst platform = (body.platform || 'instagram').toLowerCase();\n\nif (!body.filename && !body.fileUrl) {\n throw new Error('Missing required fields: provide filename or fileUrl');\n}\nif (!allowedPlatforms.includes(platform)) {\n throw new Error(`Invalid platform. Must be one of: ${allowedPlatforms.join(', ')}`);\n}\n\nconst charLimits = { twitter: 280, instagram: 2200, linkedin: 3000, facebook: 63206 };\nconst filename = body.filename || body.fileUrl?.split('/').pop() || 'asset';\nconst ext = filename.split('.').pop()?.toLowerCase();\nconst isVideo = ['mp4', 'mov', 'avi', 'webm'].includes(ext);\nconst isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext);\nconst cleanName = filename\n .replace(/\\.[^.]+$/, '')\n .replace(/[-_]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .toLowerCase().trim();\n\nreturn [{\n json: {\n filename,\n cleanName,\n fileUrl: body.fileUrl || null,\n platform,\n tone: body.tone || 'professional',\n includeHashtags: body.hashtags !== false,\n charLimit: charLimits[platform],\n contentType: isVideo ? 'video' : isImage ? 'image' : 'file',\n brand: body.brand || '',\n campaignContext: body.campaignContext || '',\n scheduleTime: body.scheduleTime || null,\n timestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "db7db58d-7cf7-4e62-b803-f63391f1ad04",
"name": "Has Remote URL?",
"type": "n8n-nodes-base.if",
"position": [
-848,
0
],
"parameters": {
"options": {},
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-has-url",
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json.fileUrl }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "72d26911-946b-48e2-a036-5e17b33cb72b",
"name": "Upload to URL - From Remote URL",
"type": "n8n-nodes-uploadtourl.uploadToUrl",
"position": [
-624,
-128
],
"parameters": {},
"credentials": {
"uploadToUrlApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "802f6c34-2f99-456a-aa49-51e158d8a539",
"name": "Upload to URL - From Binary",
"type": "n8n-nodes-uploadtourl.uploadToUrl",
"position": [
-624,
112
],
"parameters": {
"operation": "uploadFile"
},
"credentials": {
"uploadToUrlApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "05fd0d9d-9d64-4b79-b01c-e9b320df2bac",
"name": "Extract Clean URL",
"type": "n8n-nodes-base.code",
"position": [
-416,
0
],
"parameters": {
"jsCode": "const uploadResponse = $input.first().json;\nconst meta = $('Validate & Enrich Payload').first().json;\n\nconst cleanUrl =\n uploadResponse.url ||\n uploadResponse.link ||\n uploadResponse.data?.url ||\n uploadResponse.file?.url ||\n uploadResponse.shortUrl;\n\nif (!cleanUrl) {\n throw new Error('Upload to URL did not return a valid public URL. Response: ' + JSON.stringify(uploadResponse));\n}\n\nreturn [{\n json: {\n ...meta,\n cleanUrl,\n uploadId: uploadResponse.id || uploadResponse.data?.id || null,\n fileSize: uploadResponse.size || uploadResponse.data?.size || null\n }\n}];"
},
"typeVersion": 2
},
{
"id": "110bd7b7-39bd-4253-9270-1e5b4709d6c1",
"name": "OpenAI - Generate Caption",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-192,
0
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini",
"cachedResultName": "GPT-4.1-MINI"
},
"options": {
"maxTokens": 800,
"temperature": 0.75
},
"messages": {
"values": [
{
"role": "system",
"content": "You are an expert social media copywriter. Always return a valid JSON object only \u2014 no markdown, no preamble."
},
{
"content": "=Generate a social media caption.\n\n**Platform:** {{ $json.platform }}\n**Asset Name:** {{ $json.cleanName }}\n**Content Type:** {{ $json.contentType }}\n**Tone:** {{ $json.tone }}\n**Brand:** {{ $json.brand || 'Not specified' }}\n**Campaign Context:** {{ $json.campaignContext || 'General post' }}\n**Character Limit:** {{ $json.charLimit }}\n**Include Hashtags:** {{ $json.includeHashtags }}\n\nReturn ONLY this JSON:\n{\n \"caption\": \"Main caption text under the char limit\",\n \"hashtags\": [\"relevant\", \"hashtags\", \"without\", \"hash\"],\n \"altText\": \"Accessibility description of the image or video\",\n \"hook\": \"First-line hook for A/B testing\",\n \"emojiUsage\": \"low|medium|high\",\n \"bestPostingTimeUTC\": \"e.g. 14:00-16:00\",\n \"estimatedEngagement\": \"low|medium|high\"\n}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.5
},
{
"id": "2c2f4205-31a3-44f9-8a99-b498bd1cd094",
"name": "Assemble Post Payload",
"type": "n8n-nodes-base.code",
"position": [
112,
0
],
"parameters": {
"jsCode": "const openAIRaw = $input.first().json;\nconst assetData = $('Extract Clean URL').first().json;\n\nlet captionData;\ntry {\n const raw =\n openAIRaw.message?.content ||\n openAIRaw.choices?.[0]?.message?.content ||\n openAIRaw.content ||\n openAIRaw.text;\n captionData = typeof raw === 'string' ? JSON.parse(raw) : raw;\n} catch (e) {\n throw new Error('Failed to parse OpenAI caption JSON: ' + e.message);\n}\n\nconst hashtagString =\n assetData.includeHashtags && captionData.hashtags?.length\n ? '\\n\\n' + captionData.hashtags.map(h => (h.startsWith('#') ? h : '#' + h)).join(' ')\n : '';\n\nconst fullCaption = captionData.caption + hashtagString;\n\nif (fullCaption.length > assetData.charLimit) {\n console.warn(\n `Caption ${fullCaption.length} chars exceeds ${assetData.platform} limit of ${assetData.charLimit}`\n );\n}\n\nlet scheduleAt = assetData.scheduleTime;\nif (!scheduleAt) {\n const suggestedHour = parseInt(\n (captionData.bestPostingTimeUTC || '14:00').split(':')[0],\n 10\n );\n const next = new Date();\n next.setUTCHours(suggestedHour, 0, 0, 0);\n if (next <= new Date()) next.setUTCDate(next.getUTCDate() + 1);\n scheduleAt = next.toISOString();\n}\n\nreturn [{\n json: {\n assetUrl: assetData.cleanUrl,\n filename: assetData.filename,\n contentType: assetData.contentType,\n platform: assetData.platform,\n caption: captionData.caption,\n hashtags: captionData.hashtags || [],\n fullCaption,\n altText: captionData.altText || '',\n hook: captionData.hook || '',\n emojiUsage: captionData.emojiUsage,\n scheduleAt,\n suggestedPostTime: captionData.bestPostingTimeUTC,\n estimatedEngagement: captionData.estimatedEngagement,\n uploadId: assetData.uploadId,\n generatedAt: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "5e6319f4-40e9-44e2-8ec3-b743a6d6fa4d",
"name": "Route by Platform",
"type": "n8n-nodes-base.switch",
"position": [
256,
0
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Twitter/X",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.platform }}",
"rightValue": "twitter"
}
]
},
"renameOutput": true
},
{
"outputKey": "Instagram",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.platform }}",
"rightValue": "instagram"
}
]
},
"renameOutput": true
},
{
"outputKey": "LinkedIn",
"conditions": {
"options": {
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.platform }}",
"rightValue": "linkedin"
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"typeVersion": 3
},
{
"id": "dcc70bd8-774f-4579-9cbc-81abd7fd848e",
"name": "Buffer - Schedule Twitter/X",
"type": "n8n-nodes-base.httpRequest",
"position": [
480,
-160
],
"parameters": {
"url": "https://api.bufferapp.com/1/updates/create.json",
"method": "POST",
"options": {},
"jsonBody": "={\"text\": {{ JSON.stringify($json.fullCaption) }}, \"media\": {\"link\": {{ JSON.stringify($json.assetUrl) }}, \"description\": {{ JSON.stringify($json.altText) }}}, \"scheduled_at\": {{ JSON.stringify($json.scheduleAt) }}, \"profile_ids\": [\"{{ $vars.BUFFER_PROFILE_TWITTER }}\"]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "0e273c5b-45f8-4daa-9ab9-e523bae33f05",
"name": "Buffer - Schedule Instagram",
"type": "n8n-nodes-base.httpRequest",
"position": [
480,
16
],
"parameters": {
"url": "https://api.bufferapp.com/1/updates/create.json",
"method": "POST",
"options": {},
"jsonBody": "={\"text\": {{ JSON.stringify($json.fullCaption) }}, \"media\": {\"link\": {{ JSON.stringify($json.assetUrl) }}, \"description\": {{ JSON.stringify($json.altText) }}, \"photo\": {{ JSON.stringify($json.contentType === 'image' ? $json.assetUrl : '') }}}, \"scheduled_at\": {{ JSON.stringify($json.scheduleAt) }}, \"profile_ids\": [\"{{ $vars.BUFFER_PROFILE_INSTAGRAM }}\"]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "7bd22a5a-b101-4811-b559-96d93269384d",
"name": "Buffer - Schedule LinkedIn",
"type": "n8n-nodes-base.httpRequest",
"position": [
480,
160
],
"parameters": {
"url": "https://api.bufferapp.com/1/updates/create.json",
"method": "POST",
"options": {},
"jsonBody": "={\"text\": {{ JSON.stringify($json.fullCaption) }}, \"media\": {\"link\": {{ JSON.stringify($json.assetUrl) }}, \"title\": {{ JSON.stringify($json.filename) }}, \"description\": {{ JSON.stringify($json.altText) }}}, \"scheduled_at\": {{ JSON.stringify($json.scheduleAt) }}, \"profile_ids\": [\"{{ $vars.BUFFER_PROFILE_LINKEDIN }}\"]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "2ad95a6f-8c6b-4649-b2c3-e0b8fdfbab58",
"name": "Build Final Response",
"type": "n8n-nodes-base.code",
"position": [
704,
0
],
"parameters": {
"jsCode": "const bufferResponse = $input.first().json;\nconst postData = $('Assemble Post Payload').first().json;\nconst success = !bufferResponse.error && bufferResponse.success !== false;\n\nreturn [{\n json: {\n success,\n message: success\n ? `Post scheduled on ${postData.platform} for ${postData.scheduleAt}`\n : `Buffer scheduling failed: ${bufferResponse.error || 'Unknown error'}`,\n scheduleId: bufferResponse.update?.id || bufferResponse.data?.id || null,\n assetUrl: postData.assetUrl,\n platform: postData.platform,\n scheduledAt: postData.scheduleAt,\n caption: postData.caption,\n hook: postData.hook,\n hashtags: postData.hashtags,\n altText: postData.altText,\n estimatedEngagement: postData.estimatedEngagement,\n bufferError: bufferResponse.error || null\n }\n}];"
},
"typeVersion": 2
},
{
"id": "6b130b72-a224-4a00-920c-be81955b6abe",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
912,
0
],
"parameters": {
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"respondWith": "json",
"responseBody": "={{ $json }}"
},
"typeVersion": 1.1
}
],
"connections": {
"Has Remote URL?": {
"main": [
[
{
"node": "Upload to URL - From Remote URL",
"type": "main",
"index": 0
}
],
[
{
"node": "Upload to URL - From Binary",
"type": "main",
"index": 0
}
]
]
},
"Extract Clean URL": {
"main": [
[
{
"node": "OpenAI - Generate Caption",
"type": "main",
"index": 0
}
]
]
},
"Route by Platform": {
"main": [
[
{
"node": "Buffer - Schedule Twitter/X",
"type": "main",
"index": 0
}
],
[
{
"node": "Buffer - Schedule Instagram",
"type": "main",
"index": 0
}
],
[
{
"node": "Buffer - Schedule LinkedIn",
"type": "main",
"index": 0
}
]
]
},
"Build Final Response": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Assemble Post Payload": {
"main": [
[
{
"node": "Route by Platform",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Receive Asset": {
"main": [
[
{
"node": "Validate & Enrich Payload",
"type": "main",
"index": 0
}
]
]
},
"OpenAI - Generate Caption": {
"main": [
[
{
"node": "Assemble Post Payload",
"type": "main",
"index": 0
}
]
]
},
"Validate & Enrich Payload": {
"main": [
[
{
"node": "Has Remote URL?",
"type": "main",
"index": 0
}
]
]
},
"Buffer - Schedule LinkedIn": {
"main": [
[
{
"node": "Build Final Response",
"type": "main",
"index": 0
}
]
]
},
"Buffer - Schedule Instagram": {
"main": [
[
{
"node": "Build Final Response",
"type": "main",
"index": 0
}
]
]
},
"Buffer - Schedule Twitter/X": {
"main": [
[
{
"node": "Build Final Response",
"type": "main",
"index": 0
}
]
]
},
"Upload to URL - From Binary": {
"main": [
[
{
"node": "Extract Clean URL",
"type": "main",
"index": 0
}
]
]
},
"Upload to URL - From Remote URL": {
"main": [
[
{
"node": "Extract Clean URL",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpHeaderAuthopenAiApiuploadToUrlApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Schedule social media posts from local files using UploadToURL, OpenAI, and Buffer
Source: https://n8n.io/workflows/13540/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Transform raw product images into fully-optimized e-commerce listings in seconds. This workflow automates the bridge between a photo upload and a live product page by combining UploadToURL for hosting
Accelerate your real estate marketing by moving from "photo capture" to "published listing" in seconds. This workflow automates the entire listing process by hosting property photos via UploadToURL, u
This powerful n8n automation workflow is designed to execute advanced B2B lead enrichment and hyper-personalization for cold email outreach. By orchestrating a complex chain of data scraping, AI analy
This workflow bridges the gap between raw product data and revenue sales tools. It automates the entire Product Qualified Lead (PQL) lifecycle—from real-time intent routing to churn prevention—reducin
User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow