I like sharing what I build. Writing a blog post and then also manually copy-pasting it into a LinkedIn post (writing a caption, pasting the link, hitting share) is exactly the kind of small, repetitive chore that feels harmless until you've done it forty times. So I automated it.
The idea is simple: publish a post here, and it shows up on my LinkedIn profile a few seconds later as a normal update with a link-preview card back to the full post. No copy-pasting, no re-typing a caption, no context switching into another app.
The shape of the pipeline
I run a self-hosted automation server (n8n) as part of my homelab, and the flow is a short chain of steps:
- Trigger — fires whenever a new post goes live on this site.
- Format the post — takes the title, URL, and a short summary, and shapes them into what LinkedIn's API actually wants: post text, plus the fields for a rich link preview (title, description, source URL).
- Resolve identity — asks LinkedIn who I am (my member ID), since every post has to be attributed to a specific author URN.
- Publish — sends the actual post to LinkedIn's API, as a public update with an article-style link card pointing back here.
Four steps, nothing exotic. The part that actually took the time wasn't the design. It was getting LinkedIn's API to cooperate.
Where it got annoying
LinkedIn versions its API monthly, and every version is only guaranteed to stay active for about a year before it's retired. That's a reasonable policy on its own. The problem was that the pre-built LinkedIn integration I started with had a version number baked into its code, quietly, with no way to override it from the outside. Since that version had aged out, every single post request came back rejected with an "unsupported version" error, and no amount of reconfiguring the node itself changed anything, because the bug wasn't in my configuration. It was upstream, hardcoded.
The fix was to stop trusting the prebuilt LinkedIn-specific integration entirely and talk to LinkedIn's REST API directly, by hand, setting my own headers, including the version I actually wanted. That fixed the version problem, but it surfaced a second one: the LinkedIn-specific credential type I was using turned out to have the same stale version baked into it as a hidden default, silently overriding the header I was explicitly setting. Two layers of the same bug, stacked.
The actual fix was to sidestep LinkedIn-specific tooling altogether and authenticate using a plain, generic OAuth2 credential (one with no LinkedIn-specific assumptions baked in), configured by hand with LinkedIn's real authorization endpoints, scopes, and token exchange settings. Once that was in place, nothing was silently overriding anything anymore, and the pipeline started working end to end.
What I'd tell past-me
When an integration built specifically for a well-known service still fails on something as basic as "the API version is wrong," the polished pre-built tooling is usually hiding an assumption you can't see. The fastest fix is often to go one layer more primitive: talk to the raw API yourself, where every header and parameter is visible and in your control.
The other half of it was just persistence through a wall of unhelpful error messages. Most of the debugging here was reading terse OAuth error strings, forming a hypothesis, testing it, and repeating. Unglamorous, but it's most of what real integration work actually looks like.
What's next
Right now the caption is a summary I write by hand alongside the post. The next iteration is having the pipeline read the full post content and draft the LinkedIn caption itself, so publishing a post really is a single action from start to finish. I'll write about that once it's running.