LinkedIn Page Publisher¶
Publishes the approved post to a LinkedIn organisation Page. One approved conversation produces exactly one public Page post.
For posting as a person rather than a Page, see LinkedIn Member Publisher. A journey may run either, both, or neither.
At a glance¶
| Needs | The approved LinkedIn copy, the video URL, and a valid Page connection |
| Produces | A public LinkedIn post and its URL |
| Waits when | The copy or the video URL isn't ready yet |
| Re-running | Safe after success — it makes no call at all. Some states refuse reset entirely |
What it does¶
Posts the approved copy to the Page, with the video URL appended once (it won't duplicate a URL already in the text). The Page is the author. Participants don't connect LinkedIn accounts, don't install anything, and don't leave a browser running — they simply receive the resulting public post URL afterwards so they can view and reshare it.
An earlier design tried to publish through each participant's own browser via the Chrome extension. It could never actually complete, and it made a human-operated queue responsible for a publication that has to be reliable. Any leftover queued actions from that design are ignored by this step; cancel them in the outreach UI.
The safety model¶
Publishing publicly is not undoable, so this step is deliberately cautious.
- Consent is re-checked immediately before posting, not just when the step started. Waiting means retry later; an opt-out is a permanent stop.
- The record is written before the network call. A rerun after a successful publish makes no HTTP call at all, and two workers can't both create a post.
- Uncertain results never retry automatically. LinkedIn's post creation has no way for a caller to say "this is the same request as before", so a timeout, a server error, or a response METIS can't interpret leaves the result unknown — a permanent stop needing a human to check LinkedIn. Retrying could publish the same thing twice, publicly, and that is worse than waiting for a person.
- Reset is refused while the result is submitting, unknown, or published — clearing METIS's record doesn't remove a LinkedIn post. Reset is allowed from failed or retryable.
absent ──checks pass──> submitting ──created──────────> published
├─rejected──────────────> failed
├─rate limited / pre-send> retryable ──> submitting
└─timeout / error ───────> unknown (manual only)
Settings¶
| Setting | Meaning |
|---|---|
| Page | Which Page to publish as. After Connect LinkedIn, this is a list of the Pages you administer — pick one. If the list could not be fetched, it falls back to a text field for the Page's API identity, urn:li:organization:<numeric id> — not the number in a linkedin.com/company/... URL |
| Author name | Display name, shown to operators and used to label the reshare link. Filled in from the selected Page; only editable when the Page was entered by hand |
| Access token | A LinkedIn token permitted to post as that Page. Never displayed, never logged. Set by the Connect LinkedIn button, or pasted in |
| Token expiry | Optional. Publishing fails before any network call once past it; a warning appears within 7 days |
| Max posts per run | How many posts this step may publish each time the schedule ticks. Blank means all — every approved conversation waiting here publishes at once. Anything over the limit is deferred to the next run, never skipped |
| Message template | Optional. Wraps the approved copy in your own text — branding, a call to action, extra links |
Message template¶
Left blank (the default), the post is the approved copy with the video URL placed above its trailing hashtags.
A template lets you add material around that copy. {post} is required and receives the
approved copy unedited — a template can never rewrite, trim, or reword what participants
approved, which is why the placeholder is mandatory rather than optional.
{post}
▶ Watch the full conversation: {video_url}
— The Coherence Company
{video_url} is optional and places the video link yourself. When a template is set the URL
is no longer inserted automatically — otherwise a template that positions its own link would
get it twice, in two places. If you want the link, put it in the template.
A template that omits {post}, or that uses an unrecognised {placeholder}, is refused when
you save — not at publish time, when a silently branded post with no conversation in it would
already be public.
Connect LinkedIn runs LinkedIn's consent flow and saves the token and its expiry. It also looks up the Pages you administer: if there is exactly one and the step has no Page set yet, it fills it in for you; if there are several the Page setting becomes a list to pick from. It never changes a Page you have already set — reconnecting to refresh an expiring token cannot move where the step publishes, so if the message says the Page it found differs from the one configured, change it yourself in the step's config. When the lookup isn't available the Page falls back to a text field you fill in yourself (and any list from a previous connection is dropped, so you are never offered Pages the new token cannot use). LinkedIn doesn't issue most apps a refreshable connection, so the connection expires (around 60 days) and has to be reconnected the same way — which also refreshes the Page list.
If the LinkedIn app registration has no permission to list Pages at all, LinkedIn refuses the whole consent screen rather than granting the rest. Connect notices that and immediately asks again without the listing permission, so you still get a working publishing connection and fill the Page in by hand.
Checking a post before it goes out¶
The step inspector on a conversation has a Preview post… button. It shows the exact commentary this conversation would publish — approved copy, video URL, and any template branding — with its character count, plus everything that would stop the run (missing credentials, an expired token). It also tells you whether a Publish Waiter is holding the conversation upstream, or that the journey has no waiter at all — neither stops this step, they stop the conversation reaching it.
It makes no call to LinkedIn, and this is a real limitation rather than caution: publishing
relies on w_organization_social, a write-only scope. Connecting also asks for read permission
so it can list your Pages, but that is a convenience LinkedIn may decline and it does not
confirm posting rights either. There is no read endpoint to
confirm "this token can post as this Page" without actually posting, and LinkedIn accepts only
PUBLISHED when creating a post — there is no draft to send instead. So a clean preview means
the copy and the configuration are right; it is not proof the credentials work. The first
proof of that is the step running.
Available whatever the step's state. After a publish it shows what a fresh run would send,
which is what you need when reconciling an unknown record against what is actually on
LinkedIn.
Troubleshooting¶
| Symptom | Likely cause | What to do |
|---|---|---|
| Nothing arrives at this step | A Publish Waiter earlier in the journey is holding the conversation | Deliberate — check the waiter's state |
| Nothing arrives, note mentions an opt-out | A participant opted out, so the waiter holds the conversation | Deliberate. Resolve it with them. The hold is the conversation's position in the journey, not a check inside this step — see what an opt-out actually stops |
| Error before any network call, mentioning expiry | The stored connection has expired | Click Connect LinkedIn on the step and reconnect |
| Error: rejected by LinkedIn | The token doesn't permit posting as that Page, or the author URN is wrong | Check the author URN is the organisation's API id, and that the token was granted for that Page |
| Result shows unknown | LinkedIn's response was inconclusive | Do not retry. Check the Page on LinkedIn: if the post is there, reconcile the record manually; if not, it can be republished deliberately |
| Nothing happened on a re-run after success | Working as designed — the record is a ledger | No action; a published conversation is never posted twice |
| Reset button refuses | The result is published, submitting, or unknown | Deliberate. Remove the post on LinkedIn first if that's the intent |
| Post appeared without the video link | The copy already contained the URL | The URL is only appended when it isn't already present |
Technical reference¶
| Step type | linkedin_publisher |
| Runs after | youtube_video_upload (video URL). Place it after a publish_waiter when participant consent must come first — the journey's order is what holds it back, nothing inside this step |
| Feeds | linkedin_link_resolver, and through it publish_live_notifier, telegram_distributor |
| Reads | fields.linkedin_post, records.youtube.video_url — content only. This step does not read consent state |
| Writes | records.linkedin.organization_post — status, author URN and name, post URN, post_url, content hash, API version, request id, attempt count, timestamps, last error |
| API | LinkedIn's official Posts API; the dated API version is a project-wide constant, not a per-step setting |
post_url is required for a published result — the pipeline never notifies participants with
only an internal identifier, and no downstream step reconstructs a URL from one.
Punctuation in the copy¶
LinkedIn's API does not take the post text as plain text: it parses it as a small markup
language in which \ | { } @ [ ] ( ) < > # * _ ~ are reserved. The step escapes them for you
before publishing, so the copy publishes exactly as written — including asides in
parentheses, which unescaped would cause LinkedIn to silently drop the parenthesis and
everything after it while still reporting the post as created.
The one exception is a # that starts a hashtag (#SystemsChange): it is left as-is so it
still publishes as a hashtag. A # used any other way (C#, #1) is shown literally.
Two URLs, and which one participants get¶
LinkedIn's API returns only the post's URN, and the URL built from it —
https://www.linkedin.com/feed/update/urn:li:share:<id>/ — is what LinkedIn's own
documentation calls viewable "by an authorized member" — and someone opening it without a
LinkedIn session meets the sign-in wall rather than the post. That is post_url: the identity
link, correct for operators and for reconciliation, useless in an email asking people to go
and look.
The link a participant actually needs is the https://www.linkedin.com/posts/… permalink, the
one LinkedIn's own "Copy link to post" produces, which a signed-out visitor can read. No API
returns it, and this step does not look it up: that is
LinkedIn Link Resolver's only job, as its own step immediately
after this one. The lookup needs its own cadence — the publisher is scheduled slowly so it
cannot spam posts, and LinkedIn often cannot serve a new post's page for some minutes — and
its result is a dependency other steps must be able to see and wait on.
So post_url is what this step stores, and it is the identity link, not the link anyone is
sent. Nothing downstream announces the post until the resolver has written the public one;
that step holds the pipeline rather than letting a sign-in-walled link go out, and an
operator can release it by pasting the link in. Posts published before the resolver existed
still carry a public_url inside this record — the resolver adopts it rather than asking
LinkedIn again.