{
  "name": "AI LinkedIn Poster",
  "nodes": [
    {
      "parameters": {
        "content": "## AI LinkedIn Poster v1.0\n\n`POST /webhook/linkedin-ai-post`\n\n1. Set env vars in **Configuration**\n2. Assign OpenAI + LinkedIn OAuth credentials\n3. Set `WEBHOOK_SECRET` for production\n4. Import `linkedin-ai-poster-errors.json` (optional)\n\nDocs: https://github.com/Ai-assistant-KIRA/n8n-ai-linkedin-poster",
        "height": 300,
        "width": 400,
        "color": 4
      },
      "id": "sticky-main",
      "name": "Workflow Notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -200,
        80
      ]
    },
    {
      "parameters": {
        "content": "## Env vars\n| Variable | Required |\n|----------|----------|\n| `LINKEDIN_PERSON_ID` | \u2705 |\n| `WEBHOOK_SECRET` | prod \u2705 |\n| `LINKEDIN_API_VERSION` | optional (202502) |\n\n**Credentials:** OpenAI API, LinkedIn OAuth2",
        "height": 280,
        "width": 340,
        "color": 5
      },
      "id": "sticky-config",
      "name": "Configuration Notes",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        300,
        60
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "linkedin-ai-post",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        400
      ],
      "notesInFlow": true,
      "notes": "Pass X-Webhook-Secret header when WEBHOOK_SECRET is set."
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "personId",
              "name": "personId",
              "value": "={{ $env.LINKEDIN_PERSON_ID || 'YOUR_PERSON_ID' }}",
              "type": "string"
            },
            {
              "id": "linkedinApiVersion",
              "name": "linkedinApiVersion",
              "value": "={{ $env.LINKEDIN_API_VERSION || '202502' }}",
              "type": "string"
            },
            {
              "id": "webhookSecret",
              "name": "webhookSecret",
              "value": "={{ $env.WEBHOOK_SECRET || '' }}",
              "type": "string"
            },
            {
              "id": "httpMaxRetries",
              "name": "httpMaxRetries",
              "value": 3,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "configuration",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        400,
        400
      ],
      "notesInFlow": true
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const raw = $input.first().json;\nconst body = raw.body ?? raw;\nconst headers = raw.headers || {};\nconst config = $('Configuration').first().json;\n\nconst topic = (body.topic || '').trim();\nconst caption = (body.caption || body.text || '').trim();\nconst imagePrompt = (body.image_prompt || body.imagePrompt || '').trim();\nconst imageUrl = (body.image_url || body.imageUrl || '').trim();\nconst imageBase64 = body.image_base64 || body.imageBase64 || null;\nconst tone = (body.tone || 'professional').trim();\nconst hashtags = Array.isArray(body.hashtags) ? body.hashtags : [];\nconst dryRun = Boolean(body.dry_run ?? body.dryRun ?? false);\nconst publish = body.publish !== false;\nconst forceImage = Boolean(body.generate_image ?? body.generateImage ?? false);\n\nconst secret = (config.webhookSecret || '').trim();\nif (secret && secret !== 'DISABLED') {\n  const provided = (\n    headers['x-webhook-secret'] ||\n    headers['X-Webhook-Secret'] ||\n    body.webhook_secret ||\n    body.webhookSecret ||\n    ''\n  ).trim();\n  if (provided !== secret) {\n    return [{\n      json: {\n        unauthorized: true,\n        success: false,\n        error: 'Invalid or missing X-Webhook-Secret header',\n        hint: 'Set WEBHOOK_SECRET in env and pass header X-Webhook-Secret on every request.',\n      },\n    }];\n  }\n}\n\nconst hasImageUrl = !!imageUrl;\nconst hasImageBase64 = !!imageBase64;\nconst skipAiImage = dryRun && !forceImage;\nconst needsAiImage = !hasImageUrl && !hasImageBase64 && !skipAiImage && (imagePrompt || topic || forceImage);\nconst needsCaption = !caption && (topic || body.generate_caption);\nconst shouldPost = !dryRun && publish;\nconst captionOnlyPreview = dryRun && !hasImageUrl && !hasImageBase64 && !needsAiImage;\n\nreturn [{\n  json: {\n    topic,\n    caption,\n    imagePrompt: imagePrompt || (topic ? `Professional LinkedIn infographic about: ${topic}. Clean, modern, high-contrast, no text overlays.` : ''),\n    imageUrl,\n    imageBase64,\n    tone,\n    hashtags,\n    dryRun,\n    publish,\n    shouldPost,\n    forceImage,\n    needsCaption,\n    hasImageUrl,\n    hasImageBase64,\n    needsAiImage,\n    captionOnlyPreview,\n    personId: config.personId,\n    linkedinApiVersion: config.linkedinApiVersion,\n    httpMaxRetries: config.httpMaxRetries,\n    mediaTitle: (body.media_title || body.mediaTitle || topic || 'AI Generated Post').slice(0, 200),\n    visibility: body.visibility || 'PUBLIC',\n  },\n}];"
      },
      "id": "parse",
      "name": "Parse Webhook Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        580,
        400
      ],
      "onError": "continueErrorOutput",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "unauth",
              "leftValue": "={{ $json.unauthorized }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-unauthorized",
      "name": "Unauthorized?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        760,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c",
              "leftValue": "={{ $json.needsCaption }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-needs-caption",
      "name": "Needs Caption?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        960,
        400
      ]
    },
    {
      "parameters": {
        "resource": "chat",
        "operation": "complete",
        "chatModel": "gpt-4o-mini",
        "prompt": {
          "messages": [
            {
              "role": "system",
              "content": "You write high-performing LinkedIn posts. Output ONLY the post text \u2014 no titles, no markdown fences. Use short paragraphs, 1-2 emojis max, 3-5 relevant hashtags at the end."
            },
            {
              "role": "user",
              "content": "=Write a LinkedIn post.\n\nTopic: {{ $json.topic || 'automation and AI productivity' }}\nTone: {{ $json.tone }}\nExtra hashtags: {{ $json.hashtags.join(' ') }}\n\nKeep under 1300 characters. Start with a strong hook."
            }
          ]
        },
        "options": {
          "temperature": 0.7,
          "maxTokens": 800
        }
      },
      "id": "openai-caption",
      "name": "AI Generate Caption",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        1180,
        280
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const parsed = $('Parse Webhook Input').first().json;\nconst ai = $input.first().json;\nconst generated = ai.message?.content || ai.text || ai.output || '';\nif (!parsed.caption && !generated.trim()) throw new Error('AI caption generation returned empty text.');\nreturn [{ json: { ...parsed, caption: (parsed.caption || generated).trim(), captionSource: parsed.caption ? 'provided' : 'ai-generated' } }];"
      },
      "id": "merge-caption",
      "name": "Merge Caption",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        280
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const p = $input.first().json; return [{ json: { ...p, captionSource: 'provided' } }];"
      },
      "id": "pass-caption",
      "name": "Use Provided Caption",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1180,
        520
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "id": "merge-branches",
      "name": "Merge Caption Branches",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        1620,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "b",
              "leftValue": "={{ $json.hasImageBase64 }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-base64",
      "name": "Has Base64 Image?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1840,
        400
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const item = $input.first().json;\nconst raw = item.imageBase64 || '';\nconst dataUriMatch = raw.match(/^data:(image\\/[a-zA-Z0-9.+-]+);base64,(.+)$/);\nconst mimeType = dataUriMatch ? dataUriMatch[1] : 'image/jpeg';\nconst b64 = dataUriMatch ? dataUriMatch[2] : raw.replace(/^data:image\\/\\w+;base64,/, '');\nif (!b64) throw new Error('image_base64 is empty');\nconst ext = mimeType.includes('png') ? 'png' : 'jpg';\nreturn [{\n  json: { ...item, imageSource: 'base64' },\n  binary: {\n    data: {\n      data: b64,\n      mimeType,\n      fileName: `linkedin-post.${ext}`,\n    },\n  },\n}];"
      },
      "id": "decode-base64",
      "name": "Decode Base64 Image",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2060,
        280
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "u",
              "leftValue": "={{ $json.hasImageUrl }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-url",
      "name": "Has Image URL?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2060,
        480
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.imageUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "download-url",
      "name": "Download Image URL",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2280,
        400
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "a",
              "leftValue": "={{ $json.needsAiImage }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-ai-image",
      "name": "Needs AI Image?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2280,
        580
      ]
    },
    {
      "parameters": {
        "resource": "image",
        "operation": "generate",
        "prompt": "={{ $json.imagePrompt }}",
        "options": {
          "size": "1024x1024",
          "style": "vivid"
        }
      },
      "id": "openai-image",
      "name": "AI Generate Image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        2500,
        500
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "url": "={{ $json.data[0].url || $json.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "download-ai",
      "name": "Download AI Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2720,
        500
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "co",
              "leftValue": "={{ $json.captionOnlyPreview }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-caption-only",
      "name": "Caption Only Preview?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2500,
        700
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const item = $input.first().json;\nif (!item.caption && !item.topic) {\n  throw new Error('Preview requires a topic or caption.');\n}\nreturn [{\n  json: {\n    success: true,\n    dryRun: true,\n    captionOnly: true,\n    caption: item.caption || `(caption will be AI-generated from topic: ${item.topic})`,\n    captionSource: item.captionSource || (item.caption ? 'provided' : 'ai-generated'),\n    imageSource: null,\n    imageSkipped: true,\n    mediaTitle: item.mediaTitle,\n    linkedinApiVersion: item.linkedinApiVersion,\n    message: 'Caption-only preview \u2014 set generate_image: true to preview with AI image, or dry_run: false to publish.',\n  },\n}];"
      },
      "id": "caption-preview",
      "name": "Build Caption Preview",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2720,
        820
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const content = $('Merge Caption Branches').first().json;\nconst input = $input.first();\nconst binary = input.binary;\n\nif (!binary) {\n  throw new Error('No image binary available. Provide image_url, image_base64, or enable AI image generation.');\n}\n\nconst binaryKey = Object.keys(binary).find((k) => binary[k]?.data) || 'data';\nconst bin = binary[binaryKey];\nif (!bin?.data) throw new Error('Image binary is empty.');\n\nlet imageContentType = bin.mimeType || 'application/octet-stream';\nconst fileName = bin.fileName || 'linkedin-post.jpg';\nif (!imageContentType || imageContentType === 'application/octet-stream') {\n  const ext = (fileName.split('.').pop() || 'jpg').toLowerCase();\n  const map = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' };\n  imageContentType = map[ext] || 'image/jpeg';\n}\n\nconst linkedinAccepted = ['image/jpeg', 'image/png'];\nconst imageFormatWarning = linkedinAccepted.includes(imageContentType)\n  ? null\n  : `LinkedIn accepts JPEG/PNG only. Detected ${imageContentType}. Add a conversion step or override to image/jpeg.`;\n\nconst personId = (content.personId || '').trim();\nif (!personId || personId === 'YOUR_PERSON_ID') {\n  throw new Error('Set LINKEDIN_PERSON_ID in docker-compose/.env or the Configuration node.');\n}\n\nconst imageSource = content.imageSource || (content.imageUrl ? 'url' : content.imageBase64 ? 'base64' : 'ai-generated');\n\nreturn [{\n  json: {\n    ...content,\n    personUrn: `urn:li:person:${personId}`,\n    imageFileName: fileName,\n    imageContentType,\n    imageFormatWarning,\n    imageSource,\n    linkedinApiVersion: content.linkedinApiVersion || '202502',\n  },\n  binary: { data: bin },\n}];"
      },
      "id": "prepare",
      "name": "Prepare LinkedIn Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2940,
        400
      ],
      "onError": "continueErrorOutput",
      "notesInFlow": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "sp",
              "leftValue": "={{ $json.shouldPost }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-should-post",
      "name": "Should Post?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        3160,
        400
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json.captionOnly ? $json : { success: true, dryRun: true, caption: $json.caption, captionSource: $json.captionSource, imageSource: $json.imageSource, imageContentType: $json.imageContentType, imageFormatWarning: $json.imageFormatWarning, mediaTitle: $json.mediaTitle, linkedinApiVersion: $json.linkedinApiVersion, message: 'Preview only \u2014 set dry_run: false and publish: true to post.' } }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-preview",
      "name": "Respond Preview",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        3380,
        280
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.linkedin.com/rest/images?action=initializeUpload",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "linkedInOAuth2Api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "LinkedIn-Version",
              "value": "={{ $json.linkedinApiVersion || '202502' }}"
            },
            {
              "name": "X-Restli-Protocol-Version",
              "value": "2.0.0"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ initializeUploadRequest: { owner: $json.personUrn } }) }}",
        "options": {}
      },
      "id": "linkedin-init",
      "name": "LinkedIn Init Upload",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3380,
        520
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 3000,
      "onError": "continueErrorOutput",
      "credentials": {
        "linkedInOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const prepared = $('Prepare LinkedIn Payload').first();\nconst init = $input.first();\nconst value = init.json.value || init.json.body?.value || init.json;\nif (!value?.uploadUrl || !value?.image) throw new Error(`LinkedIn init upload failed: ${JSON.stringify(init.json).slice(0, 500)}`);\nreturn [{ json: { ...prepared.json, uploadUrl: value.uploadUrl, imageUrn: value.image }, binary: prepared.binary }];"
      },
      "id": "merge-upload",
      "name": "Merge Upload Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3600,
        520
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "={{ $json.uploadUrl }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "={{ $json.imageContentType || 'image/jpeg' }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "binaryData",
        "inputDataFieldName": "data",
        "options": {}
      },
      "id": "linkedin-upload",
      "name": "LinkedIn Upload Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3820,
        520
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 3000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.linkedin.com/rest/posts",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "linkedInOAuth2Api",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "LinkedIn-Version",
              "value": "={{ $('Merge Upload Data').first().json.linkedinApiVersion || '202502' }}"
            },
            {
              "name": "X-Restli-Protocol-Version",
              "value": "2.0.0"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ author: $('Merge Upload Data').first().json.personUrn, commentary: $('Merge Upload Data').first().json.caption, visibility: $('Merge Upload Data').first().json.visibility, distribution: { feedDistribution: 'MAIN_FEED', thirdPartyDistributionChannels: [] }, lifecycleState: 'PUBLISHED', content: { media: { title: $('Merge Upload Data').first().json.mediaTitle, id: $('Merge Upload Data').first().json.imageUrn } } }) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "id": "linkedin-post",
      "name": "LinkedIn Create Post",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4040,
        520
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 3000,
      "onError": "continueErrorOutput",
      "credentials": {
        "linkedInOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const merged = $('Merge Upload Data').first().json;\nconst post = $input.first();\nconst headers = post.json.headers || {};\nconst postUrn = headers['x-restli-id'] || headers['X-Restli-Id'];\nif (!postUrn) throw new Error('LinkedIn create post missing x-restli-id header');\nreturn [{ json: { success: true, dryRun: false, postUrn, shareUrl: `https://www.linkedin.com/feed/update/${encodeURIComponent(postUrn)}/`, caption: merged.caption, captionSource: merged.captionSource, imageSource: merged.imageSource, imageContentType: merged.imageContentType, mediaTitle: merged.mediaTitle, linkedinApiVersion: merged.linkedinApiVersion, api: `linkedin-rest-v${merged.linkedinApiVersion || '202502'}` } }];"
      },
      "id": "finalize",
      "name": "Finalize Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4260,
        520
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "respond-success",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        4480,
        520
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const item = $input.first();\nconst j = item.json || {};\nif (j.unauthorized) {\n  return [{ json: { ...j, statusCode: 401 } }];\n}\nconst executionError = j.error || j;\nconst message =\n  executionError.message ||\n  executionError.description ||\n  j.message ||\n  (typeof executionError === 'string' ? executionError : null) ||\n  'Workflow execution failed';\nreturn [{\n  json: {\n    success: false,\n    error: String(message).slice(0, 1000),\n    failedNode: String(j.$node?.name || j.node || 'unknown'),\n    hint: 'See docs/troubleshooting.md \u2014 verify LINKEDIN_PERSON_ID, OAuth, WEBHOOK_SECRET, LINKEDIN_API_VERSION.',\n    docs: 'https://github.com/Ai-assistant-KIRA/n8n-ai-linkedin-poster/blob/main/docs/troubleshooting.md',\n    statusCode: j.statusCode || 500,\n  },\n}];"
      },
      "id": "format-error",
      "name": "Format Error Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4260,
        720
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "e401",
              "leftValue": "={{ $json.statusCode }}",
              "rightValue": 401,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-error-code",
      "name": "Error Status?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        4480,
        720
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseCode": 500
        }
      },
      "id": "respond-error",
      "name": "Respond Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        4700,
        820
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseCode": 401
        }
      },
      "id": "respond-unauthorized",
      "name": "Respond Unauthorized",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        4700,
        620
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "throw new Error('Image required to publish. Provide image_url, image_base64, or enable AI image generation (generate_image: true).');"
      },
      "id": "format-missing-image",
      "name": "Missing Image Error",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2720,
        700
      ],
      "onError": "continueErrorOutput"
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Parse Webhook Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Webhook Input": {
      "main": [
        [
          {
            "node": "Unauthorized?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unauthorized?": {
      "main": [
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs Caption?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Caption?": {
      "main": [
        [
          {
            "node": "AI Generate Caption",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Use Provided Caption",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Generate Caption": {
      "main": [
        [
          {
            "node": "Merge Caption",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Caption": {
      "main": [
        [
          {
            "node": "Merge Caption Branches",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use Provided Caption": {
      "main": [
        [
          {
            "node": "Merge Caption Branches",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Caption Branches": {
      "main": [
        [
          {
            "node": "Has Base64 Image?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Base64 Image?": {
      "main": [
        [
          {
            "node": "Decode Base64 Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Has Image URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decode Base64 Image": {
      "main": [
        [
          {
            "node": "Prepare LinkedIn Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Image URL?": {
      "main": [
        [
          {
            "node": "Download Image URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs AI Image?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Image URL": {
      "main": [
        [
          {
            "node": "Prepare LinkedIn Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs AI Image?": {
      "main": [
        [
          {
            "node": "AI Generate Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Caption Only Preview?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Generate Image": {
      "main": [
        [
          {
            "node": "Download AI Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download AI Image": {
      "main": [
        [
          {
            "node": "Prepare LinkedIn Payload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Caption Only Preview?": {
      "main": [
        [
          {
            "node": "Build Caption Preview",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Missing Image Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Caption Preview": {
      "main": [
        [
          {
            "node": "Respond Preview",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Missing Image Error": {
      "main": [
        [],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare LinkedIn Payload": {
      "main": [
        [
          {
            "node": "Should Post?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Post?": {
      "main": [
        [
          {
            "node": "LinkedIn Init Upload",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Preview",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Init Upload": {
      "main": [
        [
          {
            "node": "Merge Upload Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Upload Data": {
      "main": [
        [
          {
            "node": "LinkedIn Upload Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Upload Image": {
      "main": [
        [
          {
            "node": "LinkedIn Create Post",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Create Post": {
      "main": [
        [
          {
            "node": "Finalize Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Finalize Result": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Error Response": {
      "main": [
        [
          {
            "node": "Error Status?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Status?": {
      "main": [
        [
          {
            "node": "Respond Unauthorized",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "errorWorkflow": "",
    "callerPolicy": "workflowsFromSameOwner"
  },
  "active": false,
  "versionId": "1.2.0",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": [
    {
      "name": "linkedin"
    },
    {
      "name": "ai"
    },
    {
      "name": "social-media"
    }
  ]
}