AutomationFlowsSocial Media › Generate and Post to Linkedin

Generate and Post to Linkedin

01 - Generate and Post to LinkedIn. Uses postgres, httpRequest. Scheduled trigger; 7 nodes.

Cron / scheduled trigger★★★★☆ complexity7 nodesPostgresHTTP Request
Social Media Trigger: Cron / scheduled Nodes: 7 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Postgres 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 →

Download .json
{
  "name": "01 - Generate and Post to LinkedIn",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "id": "trigger-daily",
      "name": "Daily Trigger 9am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -220,
        0
      ],
      "notes": "Runs in the n8n instance timezone, not UTC. Set GENERIC_TIMEZONE in .env to your IANA zone (e.g. Asia/Kolkata, Europe/Zurich) - it defaults to America/New_York. No per-workflow timezone is set, so this one setting controls every schedule."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE topics\nSET last_used_at = now()\nWHERE id = (\n  SELECT id FROM topics\n  WHERE active = true\n  ORDER BY last_used_at NULLS FIRST, id\n  LIMIT 1\n  FOR UPDATE SKIP LOCKED\n)\nRETURNING id AS topic_id, topic, angle;",
        "options": {}
      },
      "id": "pick-topic",
      "name": "Pick Topic",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        0,
        0
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "notes": "Atomically claims the least-recently-used active topic and stamps last_used_at, so topics rotate evenly and two concurrent runs can never pick the same one. Returns zero rows if no topics are active - the workflow then stops here."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: \"claude-sonnet-5\",\n  max_tokens: 600,\n  thinking: { type: \"disabled\" },\n  messages: [{\n    role: \"user\",\n    content: `Write a LinkedIn post for a business in this space. Topic: ${$json.topic}. Focus on: ${$json.angle}.\\n\\nRequirements:\\n- 100-150 words\\n- Friendly, helpful, non-salesy tone\\n- Start with a hook, not a greeting\\n- Include one practical, genuinely useful tip\\n- End with a soft call-to-action (e.g. inviting questions in comments)\\n- 3-5 relevant hashtags at the end\\n- At most 1-2 emojis\\n- Output ONLY the post text, nothing else`\n  }]\n}) }}",
        "options": {}
      },
      "id": "call-claude",
      "name": "Generate Post with Claude",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "notes": "Credential must be Header Auth with Name = x-api-key and Value = your sk-ant-... key.\n\nthinking is explicitly disabled: Claude Sonnet 5 thinks by default, and thinking tokens count against max_tokens, which would truncate the post."
    },
    {
      "parameters": {
        "jsCode": "// Validate Claude's response before anything gets published.\nconst res = $input.first().json;\n\nif (res.stop_reason === 'refusal') {\n  throw new Error('Claude declined to generate this post. Review the topic/angle wording in the topics table.');\n}\n\nif (res.stop_reason === 'max_tokens') {\n  throw new Error('Post was truncated (hit max_tokens). Raise max_tokens in the \"Generate Post with Claude\" node.');\n}\n\nconst textBlock = (res.content || []).find((b) => b.type === 'text');\n\nif (!textBlock || !textBlock.text || !textBlock.text.trim()) {\n  throw new Error('Claude returned no text content. Raw response: ' + JSON.stringify(res).slice(0, 500));\n}\n\nconst picked = $('Pick Topic').first().json;\n\nreturn [{\n  json: {\n    topic_id: picked.topic_id,\n    topic: picked.topic,\n    model: res.model,\n    postText: textBlock.text.trim()\n  }\n}];"
      },
      "id": "extract-text",
      "name": "Extract Post Text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "notes": "Fails loudly on refusal, truncation, or an unexpected response shape - a half-finished post should never reach LinkedIn."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.linkedin.com/rest/posts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "LinkedIn-Version",
              "value": "202601"
            },
            {
              "name": "X-Restli-Protocol-Version",
              "value": "2.0.0"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  author: $env.LINKEDIN_PERSON_URN,\n  commentary: $json.postText,\n  visibility: \"PUBLIC\",\n  distribution: {\n    feedDistribution: \"MAIN_FEED\",\n    targetEntities: [],\n    thirdPartyDistributionChannels: []\n  },\n  lifecycleState: \"PUBLISHED\",\n  isReshareDisabledByAuthor: false\n}) }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "id": "post-linkedin",
      "name": "Publish to LinkedIn",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "onError": "continueErrorOutput",
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "notes": "Credential must be Header Auth with Name = Authorization and Value = Bearer YOUR_ACCESS_TOKEN.\n\nfullResponse is REQUIRED: LinkedIn returns the new post's URN in the x-restli-id response header, and without this the body-only response has no way to reach it. No URN means workflow 02 can never fetch engagement for this post.\n\nOn error, execution continues down the second output so the failure is still recorded."
    },
    {
      "parameters": {
        "operation": "insert",
        "schema": {
          "value": "public"
        },
        "table": {
          "value": "posts"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "topic_id": "={{ $('Extract Post Text').first().json.topic_id }}",
            "topic": "={{ $('Extract Post Text').first().json.topic }}",
            "post_text": "={{ $('Extract Post Text').first().json.postText }}",
            "model": "={{ $('Extract Post Text').first().json.model }}",
            "linkedin_urn": "={{ $json.headers['x-restli-id'] }}",
            "status": "posted"
          }
        },
        "options": {}
      },
      "id": "log-post",
      "name": "Log Published Post",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        900,
        -100
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "insert",
        "schema": {
          "value": "public"
        },
        "table": {
          "value": "posts"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "topic_id": "={{ $('Extract Post Text').first().json.topic_id }}",
            "topic": "={{ $('Extract Post Text').first().json.topic }}",
            "post_text": "={{ $('Extract Post Text').first().json.postText }}",
            "model": "={{ $('Extract Post Text').first().json.model }}",
            "status": "failed",
            "error": "={{ ($json.error && ($json.error.message || $json.error)) || 'LinkedIn publish failed' }}"
          }
        },
        "options": {}
      },
      "id": "log-failure",
      "name": "Log Failed Post",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        900,
        120
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "notes": "Keeps the generated text when LinkedIn rejects the request, so nothing is lost and the dashboard can show what failed."
    }
  ],
  "connections": {
    "Daily Trigger 9am": {
      "main": [
        [
          {
            "node": "Pick Topic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Topic": {
      "main": [
        [
          {
            "node": "Generate Post with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Post with Claude": {
      "main": [
        [
          {
            "node": "Extract Post Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Post Text": {
      "main": [
        [
          {
            "node": "Publish to LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Publish to LinkedIn": {
      "main": [
        [
          {
            "node": "Log Published Post",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Failed Post",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

01 - Generate and Post to LinkedIn. Uses postgres, httpRequest. Scheduled trigger; 7 nodes.

Source: https://github.com/kapoordeepanshu/linkedin-autopilot-n8n/blob/main/n8n-workflows/01-generate-and-post.json — original creator credit. Request a take-down →

More Social Media workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Social Media

Automatically discovers trending topics in your niche and generates ready-to-use content ideas with AI. Twitter/X trending topics and hashtags Reddit hot posts from niche subreddits Google Trends dail

HTTP Request, Email Send, Slack +1
Social Media

Automatically generates engaging marketing posts using OpenAI and publishes them across LinkedIn, Twitter (X), and Facebook. Creates platform-optimized content with hashtags, emojis, and proper format

HTTP Request, Postgres
Social Media

Opportunity Grab Workflow. Uses httpRequest, reddit, twitter, postgres. Scheduled trigger; 8 nodes.

HTTP Request, Reddit, Twitter +1
Social Media

02 - Fetch Post Engagement. Uses postgres, httpRequest. Scheduled trigger; 7 nodes.

Postgres, HTTP Request
Social Media

Send Connection Requests. Uses postgres, httpRequest. Scheduled trigger; 6 nodes.

Postgres, HTTP Request