LinkedIn Outreach Platform Agentic Workflow
Prepare Tailored Message — LinkedIn Outreach Platform Agentic Workflow
Full per-person outreach pass: reads the existing conversation, briefs the profile, has the LLM write a short tailored first message, and types it into the compose box — draft only, never sends
sidebutton install linkedin One call instead of the eight-step manual loop: conversation history → profile research → tailored draft → compose box. Built from the 2026-08-25 manual outreach session, where per-person research changed the message every single time (a 2022 inbound pitch, a developer contact who is now a business angel, a "CEO" who turned out to run conferences).
The draft follows the campaign style current at authoring time (see campaign_context
to override): ~35 words, English, builder opener, unlimited-context mechanism, exactly
one personalization clause in the person's own vocabulary, no call CTA, no em dashes.
Never sends. The draft lands in the compose box for human review; sending is a
human Enter. Requires an existing thread with the person (see
linkedin_draft_outreach for the boundary); for thread-less contacts run
linkedin_profile_brief and draft manually.
Steps
- 1. Navigate to a URL
- url
- https://www.linkedin.com/messaging/?searchTerm={{person_name}}
browser.navigate - 2. Wait
- selector
- main
- timeout
- 12000
browser.wait - 3. Wait
- ms
- 1500
browser.wait - 4. Extract text from a selector
- selector
- .msg-conversations-container__conversations-list, [class*="conversation-list"]
- as
- thread_list
browser.extract - 5. Click an element
- selector
- li[class*="conversation"]:has-text("{{person_name}}")
browser.click - 6. Wait
- selector
- .msg-s-message-list-content
- timeout
- 8000
browser.wait - 7. Wait
- ms
- 1000
browser.wait - 8. Extract text from a selector
- selector
- .msg-s-message-list-content
- as
- history
browser.extract - 9. Navigate to a URL
- url
- {{profile_url}}
browser.navigate - 10. Wait
- selector
- main
- timeout
- 12000
browser.wait - 11. Scroll the page
- direction
- down
- amount
- 1400
browser.scroll - 12. Wait
- ms
- 800
browser.wait - 13. Scroll the page
- direction
- down
- amount
- 1400
browser.scroll - 14. Wait
- ms
- 800
browser.wait - 15. Scroll the page
- direction
- down
- amount
- 1400
browser.scroll - 16. Wait
- ms
- 800
browser.wait - 17. Extract text from a selector
- selector
- main
- as
- profile_blob
browser.extract - 18. llm generate
- as
- draft_text
- prompt
- >
llm.generate - 19. Navigate to a URL
- url
- https://www.linkedin.com/messaging/?searchTerm={{person_name}}
browser.navigate - 20. Wait
- selector
- main
- timeout
- 12000
browser.wait - 21. Wait
- ms
- 1500
browser.wait - 22. Click an element
- selector
- li[class*="conversation"]:has-text("{{person_name}}")
browser.click - 23. Wait
- selector
- .msg-form__contenteditable
- timeout
- 8000
browser.wait - 24. Click an element
- selector
- .msg-form__contenteditable
browser.click - 25. browser injectJS
- id
- sb-linkedin-clear-compose
- as
- clear_result
- js
- |
browser.injectJS - 26. Type into a field
- selector
- .msg-form__contenteditable
- text
- {{draft_text}}
browser.type - 27. Wait
- ms
- 300
browser.wait - 28. control stop
- message
- |
control.stop
Workflow definition
schema_version: 1
version: "1.0.0"
last_verified: "2026-08-25"
id: linkedin_outreach_prep
title: "Prepare Tailored Message"
description: "Full per-person outreach pass: reads the existing conversation, briefs the profile, has the LLM write a short tailored first message, and types it into the compose box — draft only, never sends"
overview: |
One call instead of the eight-step manual loop: conversation history → profile research
→ tailored draft → compose box. Built from the 2026-08-25 manual outreach session,
where per-person research changed the message every single time (a 2022 inbound pitch,
a developer contact who is now a business angel, a "CEO" who turned out to run
conferences).
The draft follows the campaign style current at authoring time (see `campaign_context`
to override): ~35 words, English, builder opener, unlimited-context mechanism, exactly
one personalization clause in the person's own vocabulary, no call CTA, no em dashes.
**Never sends.** The draft lands in the compose box for human review; sending is a
human Enter. Requires an existing thread with the person (see
`linkedin_draft_outreach` for the boundary); for thread-less contacts run
`linkedin_profile_brief` and draft manually.
category:
level: task
domain: sales
reusable: true
params:
person_name:
type: string
description: Name as shown in the inbox, used for search and thread selection
required: true
profile_url:
type: string
description: Full LinkedIn profile URL (e.g. https://www.linkedin.com/in/username/)
required: true
campaign_context:
type: string
description: Optional override for the campaign framing baked into the prompt (product one-liner, style rules, current test variant)
required: false
policies:
allowed_domains:
- "*.linkedin.com"
steps:
- type: browser.navigate
url: https://www.linkedin.com/messaging/?searchTerm={{person_name}}
- type: browser.wait
selector: main
timeout: 12000
- type: browser.wait
ms: 1500
- type: browser.extract
selector: '.msg-conversations-container__conversations-list, [class*="conversation-list"]'
as: thread_list
- type: browser.click
selector: 'li[class*="conversation"]:has-text("{{person_name}}")'
- type: browser.wait
selector: .msg-s-message-list-content
timeout: 8000
- type: browser.wait
ms: 1000
- type: browser.extract
selector: .msg-s-message-list-content
as: history
- type: browser.navigate
url: "{{profile_url}}"
- type: browser.wait
selector: main
timeout: 12000
- type: browser.scroll
direction: down
amount: 1400
- type: browser.wait
ms: 800
- type: browser.scroll
direction: down
amount: 1400
- type: browser.wait
ms: 800
- type: browser.scroll
direction: down
amount: 1400
- type: browser.wait
ms: 800
- type: browser.extract
selector: main
as: profile_blob
- type: llm.generate
as: draft_text
prompt: >
Write ONE short LinkedIn message from Max to this first-degree connection.
OUTPUT THE MESSAGE TEXT ONLY — no quotes, no preamble, no alternatives, no
commentary. It will be typed verbatim into the compose box.
## Campaign framing (override applies if non-empty)
{{campaign_context}}
Default framing: Max spent the last months building SideButton and uses it daily —
teams of AI coding agents with unlimited context on your own systems, so any coding
or engineering task gets finished. For non-technical readers translate the
mechanism into plain words (agents that fully know your systems and finish any
software task), for technical readers "unlimited context" may stand as is.
## Hard style rules
- At most 35 words, greeting and sign-off included. Sign off "Best Max".
- English unless the existing conversation with this person is in another language.
- Structure: greeting with first name, one product sentence starting from
"I built SideButton", one relevance clause tailored to the person, short closer.
- Exactly ONE personalization clause, using the person's own vocabulary from their
profile (their company, venture, event, or queue) — never a generic compliment.
- No meeting ask, no "quick call?", no links, no em dashes.
- Match the ask to who they are: buyers get the product line; agencies and
fractional execs get a white-label angle; investors, coaches and community
owners get a your-take/your-portfolio angle; conference organizers get a
contributor offer.
- If the history shows THEY once wrote first (pitch, invite), acknowledge it in
passing without apologizing for the delay and without quoting it. Ignore stale
pleasantries older than a year otherwise.
## Existing conversation with this person
---
{{history}}
---
## Their profile (raw extract)
---
{{profile_blob}}
---
- type: browser.navigate
url: https://www.linkedin.com/messaging/?searchTerm={{person_name}}
- type: browser.wait
selector: main
timeout: 12000
- type: browser.wait
ms: 1500
- type: browser.click
selector: 'li[class*="conversation"]:has-text("{{person_name}}")'
- type: browser.wait
selector: .msg-form__contenteditable
timeout: 8000
- type: browser.click
selector: .msg-form__contenteditable
- type: browser.injectJS
id: sb-linkedin-clear-compose
as: clear_result
js: |
(() => {
const box = document.querySelector('.msg-form__contenteditable');
if (!box) return 'compose box not found';
box.focus();
document.execCommand('selectAll', false, null);
document.execCommand('delete', false, null);
return 'cleared';
})();
- type: browser.type
selector: .msg-form__contenteditable
text: "{{draft_text}}"
- type: browser.wait
ms: 300
- type: control.stop
message: |
Draft typed into the thread with {{person_name}} — NOT sent. Review and press Enter to send.
## Draft
{{draft_text}}
## Threads found for this name
{{thread_list}}
## History of the opened thread
{{history}}