{
  "id": "4bdpFHsJhB5CrpTt",
  "name": "LinkedIn | Scheduled Posting (Twenty CRM \u2192 Unipile)",
  "tags": [],
  "nodes": [
    {
      "id": "ec0e4c03-c85b-4c71-af86-4e97faa0b786",
      "name": "Manual Trigger - Test Run",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        144,
        784
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "d97ff359-0318-4f2f-9546-4416aff99488",
      "name": "Fetch Scheduled Posts (Twenty CRM)",
      "type": "n8n-nodes-base.postgres",
      "position": [
        368,
        880
      ],
      "parameters": {
        "query": "-- Replace \"your_schema\" with your own Postgres/Twenty CRM schema name that stores your scheduled-post tracking table\nSELECT\n  sp.id,\n  sp.name,\n  sp.\"scheduledAt\",\n  sp.status,\n  sp.\"retryCount\",\n  sp.\"lastError\",\n  sp.name AS \"postText\",\n  sp.\"attachmentUrlPrimaryLinkUrl\" AS \"attachmentUrl\",\n  sp.\"linkedinAccountId\" AS \"linkedinAccountRowId\",\n  la.\"accountId\" AS \"unipileAccountId\",\n  la.name AS \"linkedinAccountName\"\nFROM your_schema.\"_linkedinScheduledPost\" sp\nJOIN your_schema.\"_linkedinAccountUnipile\" la ON la.id = sp.\"linkedinAccountId\"\nWHERE\n  sp.status = 'SCHEDULED'::your_schema.\"_linkedinScheduledPost_status_enum\"\n  AND sp.\"scheduledAt\" <= now()\n  AND COALESCE(sp.\"retryCount\", 0) < 2\n  AND (la.active = true)\nORDER BY sp.\"scheduledAt\" ASC\nLIMIT 10;",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "69d3e974-0ee3-48e3-a6a1-e3dbe14f09fa",
      "name": "Cron - Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        144,
        976
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "caf68daf-f85c-4e8e-a455-cca3bb05d7a1",
      "name": "Check Attachment Present?",
      "type": "n8n-nodes-base.if",
      "position": [
        816,
        800
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "exists"
              },
              "leftValue": "={{ $json.attachmentUrl }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "f16810c2-0058-4a5b-b303-8883730752d3",
      "name": "Build SharePoint ShareId",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        704
      ],
      "parameters": {
        "jsCode": "// Input: item.json.attachmentUrl = your SharePoint sharing link\nconst url = $input.first().json.attachmentUrl;\nconst b64 = Buffer.from(url, 'utf8').toString('base64');\nconst b64url = b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/g, '');\nreturn [{ json: { shareId: `u!${b64url}` } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "c47a92ba-bd03-48f3-b439-97159493e276",
      "name": "SharePoint - Resolve Drive Item",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1264,
        704
      ],
      "parameters": {
        "url": "=https://graph.microsoft.com/v1.0/shares/{{$json.shareId}}/driveItem",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftEntraOAuth2Api"
      },
      "typeVersion": 4.4
    },
    {
      "id": "959581f8-56f6-461b-8274-cc28be04f2e2",
      "name": "SharePoint - Download Attachment (Binary)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1488,
        704
      ],
      "parameters": {
        "url": "=https://graph.microsoft.com/v1.0/drives/{{$json.parentReference.driveId}}/items/{{$json.id}}/content",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "microsoftEntraOAuth2Api"
      },
      "typeVersion": 4.4
    },
    {
      "id": "9dd12013-ad35-48bb-a8ea-dd68abf6814a",
      "name": "Unipile - Create Post (With Attachment)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1712,
        704
      ],
      "parameters": {
        "url": "https://api30.unipile.com:16038/api/v1/posts",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "account_id",
              "value": "={{ $('Fetch Scheduled Posts (Twenty CRM)').item.json.unipileAccountId }}"
            },
            {
              "name": "text",
              "value": "={{ $('Fetch Scheduled Posts (Twenty CRM)').item.json.postText }}"
            },
            {
              "name": "attachments",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "2f8af441-aaff-4a70-bb21-6c4050c053e7",
      "name": "Unipile - Get Post Details",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1936,
        800
      ],
      "parameters": {
        "url": "=https://api30.unipile.com:16038/api/v1/posts/{{ $json.post_id }}",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "account_id",
              "value": "={{ $('Fetch Scheduled Posts (Twenty CRM)').item.json.unipileAccountId }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "198805a4-80a1-42cf-bca8-ad715156016f",
      "name": "Mark Post as POSTED (Twenty CRM Update)",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2160,
        880
      ],
      "parameters": {
        "query": "UPDATE your_schema.\"_linkedinScheduledPost\"\nSET\n  status = 'POSTED'::your_schema.\"_linkedinScheduledPost_status_enum\",\n  \"postedAt\" = now(),\n  \"updatedAt\" = now(),\n  \"postId\" = '{{ $json.id }}',\n  \"postUrlPrimaryLinkUrl\" = '{{ $json.share_url }}'\nWHERE\n  \"id\" = '{{ $(\"Fetch Scheduled Posts (Twenty CRM)\").item.json.id }}';",
        "options": {},
        "operation": "executeQuery"
      },
      "typeVersion": 2.6
    },
    {
      "id": "2cd41c18-a082-4f8a-b887-69fdd55e7a8f",
      "name": "Process Posts (Split in Batches)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        592,
        880
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "45f41c63-a94b-4b9d-8d3c-c75479377f16",
      "name": "Unipile - Create Post (Text Only)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1712,
        896
      ],
      "parameters": {
        "url": "https://api30.unipile.com:16038/api/v1/posts",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "account_id",
              "value": "={{ $('Fetch Scheduled Posts (Twenty CRM)').item.json.unipileAccountId }}"
            },
            {
              "name": "text",
              "value": "={{ $('Fetch Scheduled Posts (Twenty CRM)').item.json.postText }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "199b0e49-17b7-43d7-93bd-fcafa0a721f2",
      "name": "Sticky Note dd8392fa",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1552,
        400
      ],
      "parameters": {
        "color": 4,
        "width": 1424,
        "height": 612,
        "content": "LinkedIn Auto-Publish & Tracking (Twenty CRM \u2192 Unipile)\n\nWho it's for:\nTeams using a Postgres-backed content calendar (e.g., Twenty CRM) wanting auto-publishing and tracking for LinkedIn.\n\nHow it works:\nRuns every 5 mins, fetches due posts from Postgres, and publishes them to LinkedIn via Unipile (with optional SharePoint images). Writes the live URL back to the CRM.\n\nSetup:\n\nConnect credentials for Postgres, SharePoint (Azure AD), and Unipile.\n\nUpdate your_schema in the Postgres nodes.\n\nEnable the Cron trigger for production.\n\nRequirements:\n\u2022 Postgres DB with a scheduled-posts table\n\u2022 Unipile account connected to LinkedIn\n\u2022 Microsoft 365/SharePoint (if using images)"
      },
      "typeVersion": 1
    },
    {
      "id": "d221d32c-ec2d-48c6-acbe-da4b0b4ddc2b",
      "name": "Sticky Note 3ee98d6a",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        448
      ],
      "parameters": {
        "color": 6,
        "width": 300,
        "height": 200,
        "content": "Phase 0 - Triggers\n\u2022 Cron Node: Runs every 5 mins (Production)\n\u2022 Manual Node: For testing only"
      },
      "typeVersion": 1
    },
    {
      "id": "86220a79-95e4-49fe-8424-57c3432d40dd",
      "name": "Sticky Note 206db8db",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "Phase 1 - Fetch Due Posts\nQueries Twenty CRM (Postgres) for up to 10 posts where:\n\u2022 Status = SCHEDULED\n\u2022 scheduledAt <= NOW()\n\u2022 Retry < 2"
      },
      "typeVersion": 1
    },
    {
      "id": "04a0a782-ecb2-401f-923b-43a37ae0c7de",
      "name": "Sticky Note 15275059",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        752,
        448
      ],
      "parameters": {
        "color": 4,
        "width": 300,
        "height": 200,
        "content": "Phase 2 - Batching & Routing\nProcesses posts one-by-one.\n\u2022 Route A: Text-only posts\n\u2022 Route B: Posts with attachments"
      },
      "typeVersion": 1
    },
    {
      "id": "bd23ff58-216b-4bc3-a9b3-96a306f9fe27",
      "name": "Sticky Note 8ad5702d",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        448
      ],
      "parameters": {
        "color": 2,
        "width": 300,
        "height": 200,
        "content": "Phase 3A - Attachment (SharePoint)\nConverts Share URL \u2192 ID \u2192 Metadata, then downloads the binary file.\n(Note: Keep the binary field name \"data\" for Unipile)."
      },
      "typeVersion": 1
    },
    {
      "id": "d2386123-9237-4426-abb3-d9af283d9b4c",
      "name": "Sticky Note a91d73e4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        448
      ],
      "parameters": {
        "width": 300,
        "height": 200,
        "content": "Phase 3B - Publish (Unipile)\nPublishes to LinkedIn. Ensure account_id and text are mapped to the correct Unipile account and post content."
      },
      "typeVersion": 1
    },
    {
      "id": "b516f265-5703-4f5d-a651-04399598b617",
      "name": "Sticky Note 6021a440",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 200,
        "content": "Phase 4 - Verify & Update\nFetches the live LinkedIn URL from Unipile. Updates the source CRM row with: Status = POSTED, Live URL, and Post ID."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "5741d061-ab3d-4048-a978-da20cd26efe3",
  "nodeGroups": [],
  "connections": {
    "Cron - Every 5 Minutes": {
      "main": [
        [
          {
            "node": "Fetch Scheduled Posts (Twenty CRM)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build SharePoint ShareId": {
      "main": [
        [
          {
            "node": "SharePoint - Resolve Drive Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Attachment Present?": {
      "main": [
        [
          {
            "node": "Build SharePoint ShareId",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unipile - Create Post (Text Only)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger - Test Run": {
      "main": [
        [
          {
            "node": "Fetch Scheduled Posts (Twenty CRM)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unipile - Get Post Details": {
      "main": [
        [
          {
            "node": "Mark Post as POSTED (Twenty CRM Update)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SharePoint - Resolve Drive Item": {
      "main": [
        [
          {
            "node": "SharePoint - Download Attachment (Binary)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Posts (Split in Batches)": {
      "main": [
        [],
        [
          {
            "node": "Check Attachment Present?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unipile - Create Post (Text Only)": {
      "main": [
        [
          {
            "node": "Unipile - Get Post Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Scheduled Posts (Twenty CRM)": {
      "main": [
        [
          {
            "node": "Process Posts (Split in Batches)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Post as POSTED (Twenty CRM Update)": {
      "main": [
        [
          {
            "node": "Process Posts (Split in Batches)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unipile - Create Post (With Attachment)": {
      "main": [
        [
          {
            "node": "Unipile - Get Post Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SharePoint - Download Attachment (Binary)": {
      "main": [
        [
          {
            "node": "Unipile - Create Post (With Attachment)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}