L

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

Available free v1.1.0 Browser LLM
$ sidebutton install linkedin
Download ZIP

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. 1.
    Navigate to a URL
    url
    https://www.linkedin.com/messaging/?searchTerm={{person_name}}
    browser.navigate
  2. 2.
    Wait
    selector
    main
    timeout
    12000
    browser.wait
  3. 3.
    Wait
    ms
    1500
    browser.wait
  4. 4.
    Extract text from a selector
    selector
    .msg-conversations-container__conversations-list, [class*="conversation-list"]
    as
    thread_list
    browser.extract
  5. 5.
    Click an element
    selector
    li[class*="conversation"]:has-text("{{person_name}}")
    browser.click
  6. 6.
    Wait
    selector
    .msg-s-message-list-content
    timeout
    8000
    browser.wait
  7. 7.
    Wait
    ms
    1000
    browser.wait
  8. 8.
    Extract text from a selector
    selector
    .msg-s-message-list-content
    as
    history
    browser.extract
  9. 9.
    Navigate to a URL
    url
    {{profile_url}}
    browser.navigate
  10. 10.
    Wait
    selector
    main
    timeout
    12000
    browser.wait
  11. 11.
    Scroll the page
    direction
    down
    amount
    1400
    browser.scroll
  12. 12.
    Wait
    ms
    800
    browser.wait
  13. 13.
    Scroll the page
    direction
    down
    amount
    1400
    browser.scroll
  14. 14.
    Wait
    ms
    800
    browser.wait
  15. 15.
    Scroll the page
    direction
    down
    amount
    1400
    browser.scroll
  16. 16.
    Wait
    ms
    800
    browser.wait
  17. 17.
    Extract text from a selector
    selector
    main
    as
    profile_blob
    browser.extract
  18. 18.
    llm generate
    as
    draft_text
    prompt
    >
    llm.generate
  19. 19.
    Navigate to a URL
    url
    https://www.linkedin.com/messaging/?searchTerm={{person_name}}
    browser.navigate
  20. 20.
    Wait
    selector
    main
    timeout
    12000
    browser.wait
  21. 21.
    Wait
    ms
    1500
    browser.wait
  22. 22.
    Click an element
    selector
    li[class*="conversation"]:has-text("{{person_name}}")
    browser.click
  23. 23.
    Wait
    selector
    .msg-form__contenteditable
    timeout
    8000
    browser.wait
  24. 24.
    Click an element
    selector
    .msg-form__contenteditable
    browser.click
  25. 25.
    browser injectJS
    id
    sb-linkedin-clear-compose
    as
    clear_result
    js
    |
    browser.injectJS
  26. 26.
    Type into a field
    selector
    .msg-form__contenteditable
    text
    {{draft_text}}
    browser.type
  27. 27.
    Wait
    ms
    300
    browser.wait
  28. 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}}