# LinkedIn

<!-- BEGIN GENERATED: guide-map -->
## Guide map

Read only the section needed for the next decision. Line ranges are exact
for this generated runtime target.

| Section | What it covers | Lines |
| --- | --- | --- |
| [Purpose](#purpose) | Scope, ownership, and the central operating question. | 27-39 |
| [Use This When](#use-this-when) | Tasks and conditions that belong in this guide. | 40-52 |
| [Do Not Use This For](#do-not-use-this-for) | Boundaries and the correct routes for excluded work. | 53-64 |
| [Mental Model](#mental-model) | Concepts and state boundaries needed for correct operation. | 65-91 |
| [Preflight](#preflight) | State, authority, inputs, and readiness to verify before execution. | 92-108 |
| [Risk And Side Effects](#risk-and-side-effects) | Effects, approvals, cost, privacy, and retry boundaries. | 109-121 |
| [Canonical Operating Pattern](#canonical-operating-pattern) | The normal end-to-end execution sequence. | 122-137 |
| [Core SDK Surfaces](#core-sdk-surfaces) | Exact source-backed callable contracts owned by this guide. | 138-161 |
| [URL Normalization Helpers](#url-normalization-helpers) | Pure helpers that clean raw LinkedIn URLs from upstream sources (Crunchbase, CRM, CSV uploads, Clay). | 162-207 |
| [Data Contracts](#data-contracts) | Required identities, fields, evidence, handles, and durable shapes. | 208-291 |
| [Examples](#examples) | Representative usage patterns and code. | 292-363 |
| [Verification](#verification) | Evidence required before reporting success. | 364-380 |
| [Troubleshooting](#troubleshooting) | Failure diagnosis and corrective routes. | 381-393 |
| [Related References](#related-references) | Adjacent boundaries and when to cross them. | 394-405 |
<!-- END GENERATED: guide-map -->

## Purpose

Use this reference for Bridge/provider-backed LinkedIn work through
`vf.linkedin.*`: readiness checks, public or connected-account research,
relationship reads, chat history, one-off connection/message intents, and
LinkedIn post publishing.

This reference stays separate because LinkedIn crosses acquisition,
evidence-gathering, sender readiness, relationship state, and customer-visible
channel mutations. `linkedin.md` owns the backend LinkedIn SDK call contracts.
`host_browser.md` owns runtime browser/session LinkedIn adapters, and
`safety.md` owns the final approval rules for sends and posts.

## Use This When

- Searching LinkedIn posts, companies, or people through Bridge.
- Fetching public LinkedIn post comments/reactions or company/person activity
  rows.
- Reading relationship state for a LinkedIn profile from a connected sender's
  point of view.
- Reading chat history after `get_contact_relationship(...)` returned the
  member id.
- Checking LinkedIn sender readiness for backend LinkedIn actions.
- Queueing a one-off LinkedIn connection request or direct message.
- Publishing a LinkedIn post from a connected sender account.

## Do Not Use This For

| Task | Use Instead |
| --- | --- |
| Local browser sessions, cookies, live LinkedIn DOM, Chrome scraping, feed scraping, or screenshots. | `host_browser.md`; use `web.md` for explicit URL screenshot/page capture |
| People-at-company source jobs from fixed account lists or broad audience acquisition. | `audience_discovery.md` |
| Canonical person profile fields, recent authored posts as profile enrichment, person attributes, company profile fields, or company LinkedIn URL resolution. | `enrichment.md` |
| Sender account setup, matching, mailbox/account readiness, OAuth/app-password setup, or sandbox email tests. | `senders.md` |
| Sequence source staging, audience/import status, publish, activation, or runtime repair. | `sequences.md`, `sequence_authoring.md` |
| Row shaping, dedupe, qualification, joins, CSV work, or variable coverage. | `row_operations.md`, `artifacts_jobs.md` |
| Final approval policy for direct sends, posts, purchases, or sequence publish. | `safety.md` |

## Mental Model

There are four LinkedIn operation classes:

| Class | What It Does | Typical Surfaces |
| --- | --- | --- |
| Readiness | Checks whether a connected LinkedIn sender can support backend actions. | `vf.linkedin.status(...)`, `vf.senders.status(channel="linkedin")` |
| Research/evidence | Calls Bridge/public providers or connected-account LinkedIn search and returns rows or a job handle. | `get_post`, `search_posts`, `search_companies`, `fetch_company_posts`, `fetch_person_comments`, `fetch_person_reactions`, `get_post_comments`, `get_post_reactions`, `search_people_start` |
| Relationship/context | Reads account-specific relationship state and chat history from a connected sender vantage point. | `get_contact_relationship`, `get_chat_history` |
| Customer-visible action | Queues or publishes LinkedIn content visible to another person or the public. | `send_connect`, `send_message`, `create_post` |

Most LinkedIn research calls return an immediate `dict` with `items` and
provider/provenance metadata. Only `vf.linkedin.search_people_start(...)` and
`vf.linkedin.get_post_reactions(...)` return `PendingJob` handles. Do not wait
on non-job responses.

`vf.linkedin.get_post(...)` is the backend Bridge/Apify public-post metadata
path.
The old local host-browser post-fetch surface was removed; use host-browser
docs only for live feed scrolling, cookies, local DOM state, screenshots, or
browser evidence.

Some public LinkedIn research can run without a connected sender. Connected
sender context is required for people search, relationship reads, chat history,
messages, posts, and account-specific post filters such as member filters,
cursors, private company URNs/IDs, or sender-specific views.

## Preflight

- Confirm SDK auth and workspace identity before provider work.
- Call `vf.linkedin.status(sender_account_id=...)` before sends, posts,
  relationship reads, chat history, or connected-account searches.
- If multiple connected LinkedIn senders exist, pass `sender_account_id`
  explicitly.
- Print the operation class, input criteria, limits, freshness filters, and
  whether the call is a provider/credit call or a customer-visible mutation.
- For research jobs, preserve `job_id` immediately and `artifact_id` when
  present.
- For relationship/chat work, call `get_contact_relationship(...)` first and
  preserve the returned `id`; `get_chat_history(...)` expects that id.
- For direct sends or posts, get explicit confirmation immediately before the
  SDK call with the sender id, recipient/public identifier or post text, and
  schedule/window behavior.

## Risk And Side Effects

| Operation | Risk | Required Handling |
| --- | --- | --- |
| `vf.linkedin.status(...)`, `vf.linkedin.get_send_status(...)` | Platform read. | Preserve readiness/status fields and sender ids. |
| Public/Bridge research returning `dict` rows | Provider/credit call. | Print criteria and limits; preserve `items`, `provider`, `provider_semantics`, `provenance`, and `cursor` when present. |
| `vf.linkedin.get_post(...)` | Immediate public post metadata read through Bridge/Apify. | Preserve `urn`, `share_urn`, `text`, `author`, counters, `posted_at`, canonical `url`, provider metadata, and the source input. Do not call `.wait()`, `.status()`, or `.to_json()`. |
| `vf.linkedin.search_people_start(...)`, `vf.linkedin.get_post_reactions(...)` | Artifact job/provider call. | Preserve `PendingJob.job_id` and `artifact_id`; use `artifacts_jobs.md` for wait/page/export. |
| `vf.linkedin.get_contact_relationship(...)` | Connected-sender read with profile-read budget and cache behavior. | Preserve `id`, `public_identifier`, `member_urn`, relationship fields, sender id, and provider account id. |
| `vf.linkedin.get_chat_history(...)` | Connected-sender message read. | Use relationship `id`; preserve `items`, `cursor`, `pages_fetched`, sender id, and provider account id. |
| `vf.linkedin.send_connect(...)`, `vf.linkedin.send_message(...)` | Customer-visible outbound intent. | Require explicit approval immediately before invocation; preserve returned intent `id`, status, timing fields, and poll with `get_send_status(...)`. |
| `vf.linkedin.create_post(...)` | Customer-visible public post. | Require explicit approval immediately before invocation; preserve `post_id`, `post_url`, sender id, and provider account id. |

## Canonical Operating Pattern

1. Classify the request: research/evidence, relationship context, direct
   action, or rendered browser/session work.
2. Route rendered browser/session work to `host_browser.md`; keep
   `linkedin.md` for backend `vf.linkedin.*` calls.
3. Verify workspace and LinkedIn readiness when connected sender context is
   required.
4. Print criteria, limits, sender id, and side-effect class before execution.
5. For research, inspect returned rows or wait/page the returned `PendingJob`;
   do not assume provider counts imply row quality.
6. For relationship/chat, preserve the relationship `id` and use it for chat
   history.
7. For sends/posts, stop for explicit approval immediately before the mutation,
   then preserve the returned intent/post handles and verify status.

## Core SDK Surfaces

<!-- BEGIN GENERATED: core-sdk-surfaces -->
| Function | Use When | Inputs | Output And Handles To Preserve | Side Effect | Source |
| --- | --- | --- | --- | --- | --- |
| `vf.linkedin.create_post` | Use to publish or repost LinkedIn content from a connected sender account after explicit approval. | <strong>Required</strong>: `text` (string; required): LinkedIn post body. Backend requires this field; pass an empty string only when repost_post_id is supplied.<br><strong>Optional</strong>: `external_link` (string; optional): External link parameter for `linkedin.create_post`.; `media` (array; optional): Optional image/video media items; each item can use file_id, url, or base64_data plus type.; `repost_post_id` (string; optional): Existing LinkedIn post id to repost; allows empty text when present.; `sender_account_id` (string; optional): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required.; `video_thumbnail` (object; optional): Video thumbnail parameter for `linkedin.create_post`. | `dict` with LinkedIn post creation status and provider response metadata. | platform_write | installed SDK: `verbiflow/linkedin.py:160-189` |
| `vf.linkedin.fetch_company_posts` | Use to fetch recent posts authored by known LinkedIn company URLs. | <strong>Required</strong>: `company_urls` (array; required): LinkedIn company URLs for company-authored posts. Use company profile URLs, not domains or person URLs.<br><strong>Optional</strong>: `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `posted_limit_date` (string; optional): Optional public-provider company-post freshness filter accepted by the backend provider. | `dict` with LinkedIn post search results, pagination/provider metadata, and query filters. | provider_call | installed SDK: `verbiflow/linkedin.py:310-323` |
| `vf.linkedin.fetch_person_comments` | Use to fetch recent public comments from known LinkedIn person profile URLs. | <strong>Required</strong>: `profile_urls` (array; required): LinkedIn person profile URLs. Use public `/in/<slug>` URLs; do not pass company URLs or encrypted/internal provider identifiers.<br><strong>Optional</strong>: `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `posted_limit` (string; optional): Public-provider lookback filter for comments/reactions, such as any, 24h, week, month, 3months, 6months, or year. | `dict` with LinkedIn comment rows and provider metadata. | provider_call | installed SDK: `verbiflow/linkedin.py:280-293` |
| `vf.linkedin.fetch_person_reactions` | Use to fetch recent public reactions from known LinkedIn person profile URLs. | <strong>Required</strong>: `profile_urls` (array; required): LinkedIn person profile URLs. Use public `/in/<slug>` URLs; do not pass company URLs or encrypted/internal provider identifiers.<br><strong>Optional</strong>: `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `posted_limit` (string; optional): Public-provider lookback filter for comments/reactions, such as any, 24h, week, month, 3months, 6months, or year. | `dict` with LinkedIn reaction rows and provider metadata. | provider_call | installed SDK: `verbiflow/linkedin.py:295-308` |
| `vf.linkedin.get_chat_history` | Use after `linkedin.get_contact_relationship` returns a relationship/member id to read that sender's chat history. | <strong>Required</strong>: `id` (string; required): Relationship/member id returned by `vf.linkedin.get_contact_relationship(...)`; use that result before reading chat history.<br><strong>Optional</strong>: `cursor` (string; optional): Cursor parameter for `linkedin.get_chat_history`.; `sender_account_id` (string; optional): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required. | `dict` with LinkedIn thread/message rows, contact metadata, sender context, and provider status. | read | installed SDK: `verbiflow/linkedin.py:452-473` |
| `vf.linkedin.get_contact_relationship` | Use to read relationship state for one public LinkedIn profile from a connected sender's vantage point. | <strong>Required</strong>: `linkedin_url_or_slug` (string; required): LinkedIn profile URL such as https://www.linkedin.com/in/name/ or the bare public identifier slug.<br><strong>Optional</strong>: `sender_account_id` (string; optional): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required. | `dict` with relationship/message context for the selected LinkedIn contact. | read | installed SDK: `verbiflow/linkedin.py:431-450` |
| `vf.linkedin.get_post` | Use to fetch normalized metadata for one public LinkedIn post by URL, URN, share URN, or numeric activity id. | <strong>Required</strong>: `post` (string; required): Post parameter for `linkedin.get_post`.<br><strong>Optional</strong>: none | `dict` with normalized public LinkedIn post metadata from Bridge: preserve `urn`, `share_urn`, `text`, `author`, reaction/comment counters, `posted_at`, `url`, provider fields, and source input. This is an immediate read response, not a `PendingJob`. | read | installed SDK: `verbiflow/linkedin.py:210-240` |
| `vf.linkedin.get_post_comments` | Use to fetch public comments for one known LinkedIn post URL. | <strong>Required</strong>: `post_url` (str; required): LinkedIn post URL to read comments/reactions for.<br><strong>Optional</strong>: `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `posted_limit` (string; optional): Public-provider lookback filter for comments/reactions, such as any, 24h, week, month, 3months, 6months, or year.; `scrape_replies` (boolean; optional): Scrape replies parameter for `linkedin.get_post_comments`. | `dict` with LinkedIn comment rows and provider metadata. | provider_call | installed SDK: `verbiflow/linkedin.py:242-258` |
| `vf.linkedin.get_post_reactions` | Use to start an artifact-backed fetch of reactions for one known LinkedIn post URL. | <strong>Required</strong>: `post_url` (str; required): LinkedIn post URL to read comments/reactions for.<br><strong>Optional</strong>: `artifact_label` (string; optional): Human-readable label for the artifact created by the backend job.; `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `max_rows` (integer; optional): Maximum post-reaction rows for the artifact job. Backend accepts up to 10,000.; `reaction_type` (string; optional): Optional reaction type filter passed to the post-reactions artifact job. | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.stream_rows`, `vf.jobs.wait`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/linkedin.py:260-278` |
| `vf.linkedin.get_send_status` | Use to poll a one-off LinkedIn send intent returned by `send_connect` or `send_message`. | <strong>Required</strong>: `intent_id` (str; required): Outbound send intent id returned by a previous email or LinkedIn send call.<br><strong>Optional</strong>: none | `dict` with outbound send/status result and provider intent metadata. | read | installed SDK: `verbiflow/linkedin.py:145-158` |
| `vf.linkedin.search_companies` | Use to search LinkedIn companies through the Bridge-backed provider endpoint. | <strong>Required</strong>: `query` (string; required): Search query text.<br><strong>Optional</strong>: `company_sizes` (array; optional): Company sizes parameter for `linkedin.search_companies`.; `industry_ids` (array; optional): Industry ids parameter for `linkedin.search_companies`.; `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `locations` (array; optional): Locations to constrain people/company search.; `start_page` (integer; optional): Start page parameter for `linkedin.search_companies`. | `dict` with LinkedIn company search rows, pagination/provider metadata, and query filters. | provider_call | installed SDK: `verbiflow/linkedin.py:325-338` |
| `vf.linkedin.search_people_page` | Use after `linkedin.search_people_start` to page people search results. | <strong>Required</strong>: `job_id` (str; required): Backend job id returned by a start/search/enrichment/qualification call.<br><strong>Optional</strong>: `page` (int; optional; default `1`): One-based page number for page-style APIs.; `page_size` (int; optional; default `50`): Number of rows/results per page. | `dict` with LinkedIn people results, pagination metadata, and job status. | read | installed SDK: `verbiflow/linkedin.py:407-429` |
| `vf.linkedin.search_people_start` | Use to start backend LinkedIn people search and preserve the returned job id. | <strong>Required</strong>: `url` (string; required): LinkedIn search URL — Classic (`linkedin.com/search/results/people/...`) or Sales Navigator (`linkedin.com/sales/search/...`, `linkedin.com/sales/lists/people/...`). Build the URL in the LinkedIn UI; the bridge forwards it to Unipile's URL-passthrough mode.<br><strong>Optional</strong>: `max_contacts` (integer; optional): Hard cap on profiles returned (1–500). The workflow truncates the last page to land on exactly this number and skips remaining pages. Wins over `max_pages` when both are set.; `max_pages` (integer; optional): Pages to fetch. Hard caps per mode: Classic 15 (10/page), Sales Nav 20 (25/page). Optional when `max_contacts` is set — pages get sized to cover the request.; `sender_account_id` (string; optional): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required. | `PendingJob`: `job_id` immediately; wrap with `vf.jobs.result(pending).wait(...)`, then use `.artifact()`, `.rows()`, `.to_report()`, or `.stage_to_sequence(...)` when `artifact_id` is present.<br><strong>Common next calls</strong>: `vf.jobs.stream_rows`, `vf.jobs.wait`, `vf.artifacts.inspect` | artifact_job | installed SDK: `verbiflow/linkedin.py:340-405` |
| `vf.linkedin.search_posts` | Use to search LinkedIn posts by backend-native keyword, freshness, author, company, or connected-account filters. | <strong>Required</strong>: none<br><strong>Optional</strong>: `author_keywords` (string; optional): Author keywords parameter for `linkedin.search_posts`.; `content_type` (videos \| images \| live_videos \| job_posts \| documents; optional): MIME type for the uploaded file; omit to infer/default.; `cursor` (string; optional): Pagination cursor returned by a previous connected-account LinkedIn post search.; `date_posted` (past_24h \| past_week \| past_month; optional): LinkedIn post search freshness filter: past_24h, past_week, or past_month.; `extra_filters` (object; optional): Extra filters parameter for `linkedin.search_posts`.; `from_company` (array; optional): Company URL or vanity slug filters for public post search; LinkedIn URNs/IDs require a connected sender.; `from_member` (array; optional): Connected-account member filter. Requires a connected LinkedIn sender.; `keywords` (string; optional): LinkedIn search query text. This is the backend-native field; `query` is only accepted by the SDK as a compatibility alias.; `limit` (integer; optional): Maximum number of records, preview rows, or results to request.; `posted_by` (me \| my_network \| 1st_connections \| following; optional): Allowed values: me, my_network, 1st_connections, following.; `sender_account_id` (string; optional): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required.; `sort_by` (date \| relevance; optional): Allowed values: date, relevance. | `dict` with LinkedIn post search results, pagination/provider metadata, and query filters. | provider_call | installed SDK: `verbiflow/linkedin.py:191-208` |
| `vf.linkedin.send_connect` | Use to queue a one-off LinkedIn connection request after explicit approval. | <strong>Required</strong>: `public_identifier` (str; required): LinkedIn public profile slug from `/in/<slug>`, not an encrypted provider URN or company URL.<br><strong>Optional</strong>: `idempotency_key` (str \| None; optional; default `None`): Idempotency key parameter for `linkedin.send_connect`.; `note` (str \| None; optional; default `None`): Note parameter for `linkedin.send_connect`.; `respect_sender_window` (bool; optional; default `False`): Respect sender window parameter for `linkedin.send_connect`.; `scheduled_at` (str \| None; optional; default `None`): Scheduled at parameter for `linkedin.send_connect`.; `sender_account_id` (str \| None; optional; default `None`): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required. | `dict` with outbound send/status result and provider intent metadata.<br><strong>Common next calls</strong>: `vf.linkedin.get_send_status` | platform_write | installed SDK: `verbiflow/linkedin.py:73-107` |
| `vf.linkedin.send_message` | Use to queue a one-off LinkedIn direct message after explicit approval. | <strong>Required</strong>: `public_identifier` (str; required): LinkedIn public profile slug from `/in/<slug>`, not an encrypted provider URN or company URL.; `text` (str; required): Text parameter for `linkedin.send_message`.<br><strong>Optional</strong>: `idempotency_key` (str \| None; optional; default `None`): Idempotency key parameter for `linkedin.send_message`.; `respect_sender_window` (bool; optional; default `False`): Respect sender window parameter for `linkedin.send_message`.; `scheduled_at` (str \| None; optional; default `None`): Scheduled at parameter for `linkedin.send_message`.; `sender_account_id` (str \| None; optional; default `None`): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required. | `dict` with outbound send/status result and provider intent metadata.<br><strong>Common next calls</strong>: `vf.linkedin.get_send_status` | platform_write | installed SDK: `verbiflow/linkedin.py:109-143` |
| `vf.linkedin.status` | Use to check LinkedIn sender readiness before connected-account research, relationship reads, sends, or posts. | <strong>Required</strong>: none<br><strong>Optional</strong>: `sender_account_id` (str \| None; optional; default `None`): Connected LinkedIn sender account id. Pass it when the org has more than one LinkedIn sender or when account-specific filters/actions are required. | `linkedin_readiness` payload returned by the SDK operation. Inspect the documented function reference and `raw`/dictionary fields when debugging provider-specific data.<br><strong>Common next calls</strong>: `vf.senders.status`, `vf.linkedin.get_send_status` | read | installed SDK: `verbiflow/linkedin.py:44-71` |
<!-- END GENERATED: core-sdk-surfaces -->

## URL Normalization Helpers

Pure helpers that clean raw LinkedIn URLs from upstream sources (Crunchbase, CRM, CSV uploads, Clay). No network call. **Never inline regex against LinkedIn URLs — call these instead.**

### Batch (default — use this for any list of URLs)

```python
result = vf.linkedin.standardize_company_urls(urls)
result["canonical"]   # list[str | None] — index-aligned with input; None where dropped
result["dropped"]     # list[{"url": str, "reason": str}] — every failed input with its reason

result = vf.linkedin.standardize_personal_urls(urls)   # same shape
```

Pattern:

```python
out = vf.linkedin.standardize_company_urls([row["linkedin_url"] for row in rows])
for row, canonical in zip(rows, out["canonical"]):
    row["linkedin_url"] = canonical          # None where the URL couldn't be normalized
clean = [r for r in rows if r["linkedin_url"]]
# Show out["dropped"] to the user as-is.
```

### Single-URL (use when you have one URL, not a list)

`vf.linkedin.standardize_company_url(url)` and `vf.linkedin.standardize_personal_url(url)` return `{ok, canonical_url, slug|public_identifier, shape, reason}`. The batch functions wrap these.

### What gets recovered, what gets dropped

Recovered (returned in `canonical`):
- locale subdomains (`de.linkedin.com/company/foo`)
- trailing path/query/fragment (`/company/foo/about?trk=...`)
- authwall-wrapped URLs (single- and double-encoded `sessionRedirect`)
- missing slash (`linkedin.com/companyfoo` → `foo`)
- bare-slug path (`linkedin.com/finopsly`)
- domain-as-slug fallback (`aethexai.com` → `aethexai`; flag this may not resolve)

Dropped (in `dropped` with a reason):
- `/in/...` URL passed as a company URL — founder profile, not the company page
- `/company/...` URL passed as a personal URL
- `/showcase/...` for either function
- empty / null / unparseable input

Both rejection cohorts are upstream data problems. The helpers cannot guess a company page from a founder profile. Show `dropped` to the user verbatim; they decide whether to fix the source.

## Data Contracts

### Public Profile Identity

Use public person profile URLs such as:

```text
https://www.linkedin.com/in/example/
```

Do not pass company URLs to person-profile fields. Do not pass encrypted or
provider-internal identifiers as if they were public profile slugs. If a source
returns internal-looking `/in/ACw...` identifiers or provider URNs, resolve or
verify them before profile enrichment, relationship reads, direct sends, or
sequence staging. The canonical profile/enrichment surfaces live in
`enrichment.md`.

### Research Rows

Preserve:

- `items` or result rows;
- `schema`;
- `provider`;
- `provider_semantics`;
- `provenance`;
- `cursor` or `next_cursor` when present;
- source inputs such as `post_url`, `profile_urls`, `company_urls`, `keywords`,
  `from_company`, or `sender_account_id`.

Research rows are evidence or candidates. They are not enriched contacts, CRM
suppression results, sequence-ready rows, or send approval by themselves.

### Job Handles

Only these LinkedIn SDK calls return real job handles:

- `vf.linkedin.search_people_start(...)`;
- `vf.linkedin.get_post_reactions(...)`.

Preserve `job_id`, `artifact_id`, `status`, `phase`, `progress_current`,
`progress_total`, and summary metadata. Use `vf.jobs.result(...)`,
`vf.jobs.wait(...)`, `vf.artifacts.*`, or `vf.jobs.export_csv(...)` through
`artifacts_jobs.md` after that point.

### Relationship And Chat

`vf.linkedin.get_contact_relationship(...)` returns the member/relationship
context from one connected sender's vantage point. Preserve:

- `id`, which is the value to pass to `vf.linkedin.get_chat_history(...)`;
- `public_identifier`;
- `member_urn`;
- `network_distance`;
- `is_connection`;
- `pending_invitation`;
- `sender_account_id`;
- `unipile_account_id`.

Do not call chat history with a guessed URL when the relationship call can
return the provider id.

### Sends And Posts

For `send_connect` and `send_message`, preserve:

- returned intent `id`;
- `status`;
- `sender_account_id`;
- `action`;
- `scheduled_at`;
- `next_eligible_at`;
- `sent_at`;
- `accepted_at`;
- `replied_at`;
- `detail`.

For `create_post`, preserve:

- `post_id`;
- `post_url`;
- `sender_account_id`;
- `unipile_account_id`.

## Examples

### Public Post Research

```python
import verbiflow as vf

vf.auth.ensure_logged_in()

post = vf.linkedin.get_post(
    post="https://www.linkedin.com/feed/update/urn:li:activity:7353412384264228865/",
)
print(post.get("urn"), post.get("author"), str(post.get("text", ""))[:300])

posts = vf.linkedin.search_posts(
    keywords="product launch",
    date_posted="past_month",
    limit=10,
)
print(posts["provider"], len(posts.get("items", [])), posts.get("cursor"))
```

### Relationship Then Chat History

```python
import verbiflow as vf

vf.auth.ensure_logged_in()
print(vf.linkedin.status(sender_account_id="sa_..."))

relationship = vf.linkedin.get_contact_relationship(
    linkedin_url_or_slug="https://www.linkedin.com/in/example/",
    sender_account_id="sa_...",
)
history = vf.linkedin.get_chat_history(
    id=str(relationship["id"]),
    sender_account_id=str(relationship["sender_account_id"]),
)
print(relationship["network_distance"], history["pages_fetched"], len(history["items"]))
```

### Artifact-Backed People Search

```python
import verbiflow as vf

job = vf.linkedin.search_people_start(
    url="https://www.linkedin.com/sales/search/people?...",
    max_contacts=50,
    sender_account_id="sa_...",
)
result = vf.jobs.result(job).wait(timeout=300)
rows = result.rows(limit=25)
print(result.job_id, result.artifact_id, len(rows))
```

### Approved One-Off Connection Request

```python
import verbiflow as vf

# Run only after explicit human approval for this sender, recipient, and note.
intent = vf.linkedin.send_connect(
    public_identifier="example-person",
    note="Saw your launch post and thought this may be relevant.",
    sender_account_id="sa_...",
    idempotency_key="linkedin-connect-example-person-2026-06-19",
    respect_sender_window=True,
)
print(intent["id"], intent["status"], intent.get("next_eligible_at"))
```

## Verification

- The selected operation class matches the user request and the owning
  reference.
- Workspace and LinkedIn sender readiness were checked when sender context was
  required.
- Provider/credit calls printed input criteria and limits before launch.
- Research responses were sampled and inspected for expected fields before
  enrichment, row shaping, workflow checkpointing, or sequence staging.
- `job_id` and `artifact_id` were preserved for job-backed calls.
- Relationship/chat calls preserved the relationship `id`, sender id, and
  provider account id.
- Sends/posts were confirmed immediately before invocation and verified with
  returned intent/post ids or `get_send_status(...)`.
- Local browser/cookie/DOM work was routed to host docs instead of treated as
  backend LinkedIn research.

## Troubleshooting

| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `search_posts(...)` says keywords are required | The SDK call used stale `query` examples or omitted public-search keywords | Use `keywords=...`; the SDK accepts `query` only as a compatibility alias. |
| People search starts but rows are unrelated | `location` expects LinkedIn geoUrn ids and `network` expects LinkedIn network filters, not fuzzy prose | Resolve location ids first or use a different audience source. |
| A research call returns `dict`, but the agent tries `.wait()` | It is an immediate provider call, not a `PendingJob` | Wait only on `search_people_start` and `get_post_reactions`; inspect `items` for other calls. |
| Relationship read fails with sender ambiguity | Multiple connected LinkedIn senders exist | Call `vf.linkedin.status()` / `vf.senders.status(channel="linkedin")`, choose a sender, and pass `sender_account_id`. |
| Chat history fails after relationship read was skipped | `get_chat_history(...)` needs the relationship/member `id` | Call `get_contact_relationship(...)` first and pass the returned `id`. |
| Public company search returns provider unavailable | Public LinkedIn provider is temporarily unavailable or missing provider configuration | Do not silently fall back to unrelated scraping; report provider/provenance and retry when provider status is healthy. |
| Profile enrichment returns empty fields for a LinkedIn URL | The input is a company URL, encrypted/internal identifier, or stale profile shape | Route to `enrichment.md`, verify public `/in/<slug>` person URLs, and inspect source provenance. |
| The task needs feed scrolling, cookies, rendered DOM, or screenshots | Backend LinkedIn routes do not own local browser state | Use `host_browser.md` and preserve local host action/session ids. |

## Related References

| If You Are Doing This | Read Next |
| --- | --- |
| Local browser sessions, cookies, rendered LinkedIn DOM, feed scraping, screenshots, or host evidence | `host_browser.md`; use `web.md` for explicit URL page capture |
| People-at-company source jobs, broad people discovery, or acquisition source choice | `audience_discovery.md` |
| Profile enrichment, recent authored posts, company LinkedIn URL resolution, person attributes, or phone/email enrichment | `enrichment.md` |
| Sender account readiness, sender id selection, sender matching, or mailbox/account setup | `senders.md` |
| Waiting on jobs, paging artifacts, exporting rows, or staging artifact rows | `artifacts_jobs.md` |
| Row shaping, dedupe, qualification, or variable coverage after LinkedIn rows exist | `row_operations.md` |
| Sequence source staging, publish, activation, and audience/import state | `sequence_authoring.md`, `sequences.md` |
| Approval gates for direct LinkedIn sends/posts or other customer-visible mutations | `safety.md` |
