Conversation
LinkedIn messages join the inbox the same way the other agent-held channels do: the agent mirrors them through /api/ingest and sends queued replies from /api/outbox. LinkedIn offers no messaging API for member accounts, so the agent works from its own signed-in browser, and the channel is deliberately narrow: - A reply is accepted only once the contact has written in that thread, and it never expires. A thread cannot be started from the app, and the channel is left out of "New conversation". - Every LinkedIn reply is written by a signed-in person; the agent delivers it but cannot author one. Replies are text only. - A contact's address is their public profile URL in one canonical form, so the variants LinkedIn produces land in one thread, and a /sales/ link that only opens for a signed-in seat never becomes an address. A thread nobody has answered shows "Waiting for them to write" in place of the composer. agent.md sets the agent's rules for this channel: stay inside the browser session, never extract the session cookie or sign in with a password, send only outbox items, keep a slow pace, and stop at any sign-in page, check or limit notice.
LinkedIn was reply-only. A signed-in person can now start a LinkedIn thread from "New conversation" with a profile URL, and send one opening message. The thread then waits until the contact replies; a send that failed does not count, so it can be retried. The agent can still never open a thread or write a message. The app cannot see someone's connections, so that check sits with the agent: outbox items carry `opening: true` for the first message in a thread, and agent.md tells the agent to send it only to a 1st-degree connection and to fail it otherwise, never sending a connection request instead. The dialog asks for a LinkedIn profile URL, skips the people-app picker (its records carry phone and email, not a LinkedIn profile), and says plainly that messaging first carries more restriction risk than replying. The composer names an opening message as one.
An org's LinkedIn account now sends at most 50 messages and 20 opening messages over a rolling 24 hours. A restriction lands on a real person's account, so the app enforces this itself instead of trusting how much people queue. Messages past a limit are not refused and do not fail: they stay queued, and the outbox holds them back until the rolling day has room again. The counts use when a message actually left, not when it was queued, so two new message columns record that: sent_at (set when the agent confirms a send, or from the original time of a message it mirrors in as sent) and opening (set when a person queues a thread's first message). Mirrored messages count too, since the limit protects the account whoever sent from it; failed sends do not. The defaults sit at the cautious end of what LinkedIn automation vendors publish, and LINKEDIN_DAILY_MESSAGES / LINKEDIN_DAILY_OPENERS change them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
LinkedIn joins the inbox as a channel the agent holds, like Telegram or Slack:
POST /api/ingest.GET /api/outbox.LinkedIn has no messaging API for member accounts, so the agent works from its own signed-in browser. Automated use of a member account is against LinkedIn's User Agreement and can get the account restricted, so the channel is kept narrow.
Rules
Enforced by the server:
POST /api/conversationswithlinkedinneeds a signed-in user (403 for the agent), and the handle must be a profile URL (422 otherwise).LINKEDIN_DAILY_MESSAGES/LINKEDIN_DAILY_OPENERSoverride them). Anything past a limit stays queued and is held back fromGET /api/outboxuntil there's room; nothing fails. The counts use when a message actually left (newsent_atcolumn), set when the agent confirms a send or from the original time of a message it mirrors in. A newopeningcolumn marks openers. Mirrored sends count; failed sends don't.https://www.linkedin.com/in/<slug>). Country subdomains, capitals and tracking parameters all land in the same thread, and a/sales/…link never becomes an address.Enforced by the agent:
opening: truefor a thread's first message.agent.mdtells the agent to send it only to a 1st-degree connection, and otherwise to mark it failed ("Not a LinkedIn connection"), never sending a connection request instead. The app can't see anyone's connections, so this check sits with the agent.UI
Agent contract (
agent.md)A LinkedIn section. The agent must:
Checked
tsc --noEmitclean, andvite buildpasses.node --test tests/*.test.mjs: 23 pass: 10 intests/linkedin.test.mjsand 4 intests/linkedin-limits.test.mjs, running against the real worker in Miniflare.wrangler devproxy repeatedly dropped with "Network connection lost", so the send flow was verified through the tests rather than clicked through.agent.md.Notes
pnpm-workspace.yamlstill has placeholder values (esbuild: set this to true or false), sopnpm installexits non-zero without building esbuild or workerd. That's already onmain; this PR doesn't change it.