{
  "name": "FilePost — Schedule social assets with OpenAI and Buffer",
  "nodes": [
    {
      "parameters": {
        "content": "## One webhook to a scheduled social post\n\nThis workflow receives an image URL, hosts the asset with FilePost, asks OpenAI for platform-safe copy, and schedules it through Buffer's current GraphQL API. It supports `instagram`, `linkedin`, and `twitter`.\n\n### Required webhook fields\n- `file_url`\n- `platform`\n- `brand`\n- `campaign_context`\n\nOptional: `tone`, `schedule_at`, and `include_hashtags`.\n\n### Setup\n1. Install and connect `n8n-nodes-filepost`.\n2. Add OpenAI Header Auth to **Generate Social Copy** (`Authorization: Bearer ...`).\n3. Add Buffer Header Auth to **Schedule in Buffer** (`Authorization: Bearer ...`).\n4. Create variables `BUFFER_CHANNEL_INSTAGRAM`, `BUFFER_CHANNEL_LINKEDIN`, and `BUFFER_CHANNEL_TWITTER`.",
        "height": 570,
        "width": 620,
        "color": 7
      },
      "id": "f7d2a4ac-3e41-4a4a-8be1-27216fc45e5e",
      "name": "Setup guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-820, -410]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "filepost-social-scheduler",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "dd324e53-bc69-4820-8bc8-6e9577af9891",
      "name": "Receive Social Asset",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-780, 100],
      "webhookId": "filepost-social-scheduler-template"
    },
    {
      "parameters": {
        "jsCode": "const body = $input.first().json.body || $input.first().json;\nif (!body.file_url) throw new Error('file_url is required');\nconst platform = String(body.platform || '').trim().toLowerCase();\nconst limits = { twitter: 280, instagram: 2200, linkedin: 3000 };\nif (!limits[platform]) throw new Error('platform must be instagram, linkedin, or twitter');\nconst fileUrl = new URL(body.file_url);\nif (!['http:', 'https:'].includes(fileUrl.protocol)) throw new Error('file_url must use http or https');\nconst clean = (value, fallback = '') => String(value || fallback).trim().replace(/[\\r\\n]+/g, ' ').slice(0, 1000);\nconst channelIds = {\n  instagram: $vars.BUFFER_CHANNEL_INSTAGRAM,\n  linkedin: $vars.BUFFER_CHANNEL_LINKEDIN,\n  twitter: $vars.BUFFER_CHANNEL_TWITTER\n};\nif (!channelIds[platform]) throw new Error(`Missing Buffer channel variable for ${platform}`);\nlet scheduleAt = '';\nif (body.schedule_at) {\n  const requested = new Date(body.schedule_at);\n  if (Number.isNaN(requested.getTime())) throw new Error('schedule_at must be a valid ISO 8601 date');\n  scheduleAt = requested.toISOString();\n}\nreturn [{ json: {\n  fileUrl: fileUrl.toString(),\n  platform,\n  channelId: channelIds[platform],\n  charLimit: limits[platform],\n  brand: clean(body.brand, 'Our brand'),\n  campaignContext: clean(body.campaign_context, 'General brand update'),\n  tone: clean(body.tone, 'clear and helpful'),\n  includeHashtags: body.include_hashtags !== false,\n  scheduleAt,\n  receivedAt: new Date().toISOString()\n} }];"
      },
      "id": "6f7fe2e5-8825-471a-bcdb-fd6b6df89ba4",
      "name": "Validate Social Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-540, 100]
    },
    {
      "parameters": {
        "url": "={{ $json.fileUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "id": "35ce1609-50ca-4462-a08a-53c99f0f4448",
      "name": "Download Social Asset",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-300, 100]
    },
    {
      "parameters": {
        "operation": "upload",
        "binaryPropertyName": "data",
        "expiresIn": "",
        "filenameMode": "public"
      },
      "id": "9d614ccd-c0bb-4390-b8c2-2a70229d07da",
      "name": "Host Asset with FilePost",
      "type": "n8n-nodes-filepost.filePost",
      "typeVersion": 1,
      "position": [-60, 100]
    },
    {
      "parameters": {
        "jsCode": "const meta = $('Validate Social Request').first().json;\nconst hosted = $input.first().json;\nconst prompt = `Write one ${meta.platform} post for ${meta.brand}. Context: ${meta.campaignContext}. Tone: ${meta.tone}. Maximum total caption length: ${meta.charLimit} characters. Hashtags enabled: ${meta.includeHashtags}. Return JSON with caption, hashtags (array without #), alt_text, and suggested_utc_time (ISO 8601). Keep the combined caption and hashtags inside the platform limit.`;\nreturn [{ json: { requestBody: {\n  model: 'gpt-5.6-luna',\n  input: [{ role: 'user', content: [\n    { type: 'input_text', text: prompt },\n    { type: 'input_image', image_url: hosted.url, detail: 'low' }\n  ] }],\n  text: { format: { type: 'json_object' } }\n}, assetUrl: hosted.url, filepostFileId: hosted.file_id } }];"
      },
      "id": "fa239ea0-d4dd-46d7-aed3-a022a7498304",
      "name": "Build Caption Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [180, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.requestBody) }}",
        "options": {}
      },
      "id": "1d381742-c6e2-4016-81bb-83ac0a19ff96",
      "name": "Generate Social Copy",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [420, 100]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst outputText = response.output_text || (response.output || []).flatMap((item) => item.content || []).find((part) => part.type === 'output_text')?.text;\nif (!outputText) throw new Error('OpenAI returned no output_text');\nlet copy;\ntry { copy = JSON.parse(outputText); } catch (error) { throw new Error(`OpenAI returned invalid JSON: ${error.message}`); }\nconst meta = $('Validate Social Request').first().json;\nconst hosted = $('Build Caption Request').first().json;\nconst hashtags = meta.includeHashtags && Array.isArray(copy.hashtags)\n  ? copy.hashtags.map((tag) => `#${String(tag).replace(/^#/, '').replace(/\\s+/g, '')}`).filter((tag) => tag.length > 1)\n  : [];\nconst suffix = hashtags.length ? `\\n\\n${hashtags.join(' ')}` : '';\nlet text = `${String(copy.caption || '').trim()}${suffix}`;\nif (text.length > meta.charLimit) text = text.slice(0, meta.charLimit - 1).trimEnd() + '…';\nlet scheduleAt = meta.scheduleAt || String(copy.suggested_utc_time || '');\nif (!scheduleAt || Number.isNaN(Date.parse(scheduleAt)) || new Date(scheduleAt) <= new Date()) {\n  const fallback = new Date(Date.now() + 60 * 60 * 1000);\n  fallback.setUTCMinutes(0, 0, 0);\n  scheduleAt = fallback.toISOString();\n}\nconst altText = String(copy.alt_text || '').slice(0, 1000);\nconst image = { url: hosted.assetUrl, ...(altText ? { metadata: { altText } } : {}) };\nconst query = `mutation CreateFilePostScheduledPost($input: CreatePostInput!) {\n  createPost(input: $input) {\n    ... on PostActionSuccess { post { id text dueAt status assets { id mimeType } } }\n    ... on MutationError { message }\n  }\n}`;\nconst input = {\n  text,\n  channelId: meta.channelId,\n  schedulingType: 'automatic',\n  mode: 'customScheduled',\n  dueAt: scheduleAt,\n  aiAssisted: true,\n  assets: [{ image }]\n};\nreturn [{ json: {\n  platform: meta.platform,\n  channelId: meta.channelId,\n  text,\n  altText,\n  assetUrl: hosted.assetUrl,\n  filepostFileId: hosted.filepostFileId,\n  scheduleAt,\n  bufferBody: { query, variables: { input } },\n  generatedAt: new Date().toISOString()\n} }];"
      },
      "id": "46d8d96d-8300-48a5-8347-9ca41c4cc935",
      "name": "Enforce Platform Limits",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [660, 100]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.buffer.com",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.bufferBody) }}",
        "options": {}
      },
      "id": "991558ad-35ac-45c7-87c1-62df33963b8d",
      "name": "Schedule in Buffer",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [900, 100]
    },
    {
      "parameters": {
        "jsCode": "const buffer = $input.first().json;\nconst post = $('Enforce Platform Limits').first().json;\nconst outcome = buffer.data?.createPost;\nconst created = outcome?.post;\nconst error = (buffer.errors || []).map((item) => item.message).join('; ') || (!created ? outcome?.message || 'Buffer returned no post' : null);\nif (!created) throw new Error(`Buffer could not schedule the post: ${error}`);\nreturn [{ json: {\n  success: true,\n  buffer_post_id: created.id,\n  platform: post.platform,\n  scheduled_at: created.dueAt || post.scheduleAt,\n  asset_url: post.assetUrl,\n  filepost_file_id: post.filepostFileId,\n  text: created.text || post.text,\n  status: created.status || 'scheduled'\n} }];"
      },
      "id": "c8d61c4d-a65b-4aa2-b19a-f41998c463c0",
      "name": "Build Schedule Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1140, 100]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {"responseCode": 201}
      },
      "id": "1b363386-8ab0-440b-b613-31c48b86ef71",
      "name": "Return Schedule Result",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [1380, 100]
    }
  ],
  "connections": {
    "Receive Social Asset": {"main": [[{"node": "Validate Social Request", "type": "main", "index": 0}]]},
    "Validate Social Request": {"main": [[{"node": "Download Social Asset", "type": "main", "index": 0}]]},
    "Download Social Asset": {"main": [[{"node": "Host Asset with FilePost", "type": "main", "index": 0}]]},
    "Host Asset with FilePost": {"main": [[{"node": "Build Caption Request", "type": "main", "index": 0}]]},
    "Build Caption Request": {"main": [[{"node": "Generate Social Copy", "type": "main", "index": 0}]]},
    "Generate Social Copy": {"main": [[{"node": "Enforce Platform Limits", "type": "main", "index": 0}]]},
    "Enforce Platform Limits": {"main": [[{"node": "Schedule in Buffer", "type": "main", "index": 0}]]},
    "Schedule in Buffer": {"main": [[{"node": "Build Schedule Result", "type": "main", "index": 0}]]},
    "Build Schedule Result": {"main": [[{"node": "Return Schedule Result", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"},
  "versionId": "970a156f-3c88-49eb-b3af-c03327691268",
  "meta": {"templateCredsSetupCompleted": false},
  "tags": []
}
