Skip to main content
Glama

Server Details

LinkedIn outreach from Claude: invites under safe caps, follow-ups, replies and inbox triage.

Ownership verified
Status
Healthy
Uptime
97.9% over 29 days
OAuth
Works in Glama
Last Tested
Transport
Streamable HTTP · MCP 2025-11-25
URL

TDQS

A4.1/5.0

Scored across 104 tools

Disambiguation4/5

Given ~100 tools in one domain, the boundaries are unusually explicit: descriptions name their neighbours and say when NOT to use a tool (li_cancel_steps vs li_mark_stopped, li_conversation_history vs li_read_conversation, li_send_reply vs li_send_message vs li_send_bulk_messages, li_check_targets vs the built-in collision report). Overlap remains in the read surfaces — list_conversations / read_conversation / read_conversations_bulk / conversation_history / replies_to_answer all expose threads — and several near-synonym pairs (li_pending_invites vs li_connections, li_connections vs li_new_connections) require careful reading rather than being self-evident from the name.

Naming Consistency4/5

Almost everything follows li_<verb>_<noun> snake_case (li_create_campaign, li_mark_outcome, li_cancel_steps), with the li_ prefix applied uniformly across all 104 tools. Deviations are few but real: a handful are bare nouns or noun phrases (li_connections, li_bookings, li_proxies, li_replies_to_answer, li_account_activity, li_outcome_summary, li_selector_canary), so the verb is not always predictive of read-vs-write.

Tool Count1/5

At 104 tools this is far past any practical ceiling for reliable agent selection, and the surface spans at least four unrelated domains in one server: outreach operations, enrichment/research, triage, and operator-only billing/proxy/infrastructure admin. Whole clusters could be collapsed (e.g. one conversation-read tool with a scope flag, one bulk-vs-single send, one campaign-create/ingest) rather than each variant being its own tool.

Completeness5/5

Every lifecycle has full coverage including the awkward parts: create/edit/cancel/launch/pause/resume campaigns, hold steps, reset contacts, undo mistaken marks, per-person and campaign-wide copy edits, dry-run previews, pagination with explicit truncation flags, and job-based recovery for timed-out async work. Operator surfaces (billing, proxies, invites breaker, health) and safety surfaces (do-not-contact, install-wide holds, send confirmations) are all present rather than left as gaps.

Available Tools

104 tools
li_accepted_pending_messageAccepted, Awaiting First MessageA
Read-only
Inspect

List contacts whose invite was ACCEPTED but whose first message has not gone out yet — the Day-1 starter queue. Normal for connectionWaitHours (24h by default) after acceptance, and longer outside the account's sending hours; the worker sends them by itself. DB-backed and instant; up to limit rows (default 100), and truncated says when there are more. Openers already past due are li_due_messages; every timed step is li_scheduled_steps; to record new acceptances first, li_poll_acceptances.

NameRequiredDescriptionDefault
limitNoMost rows to return (default 100); `truncated` says when there are more
sessionIdNoLimit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account.
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already carry the safety profile (readOnlyHint=true, destructiveHint=false, openWorldHint=false). The description adds genuinely useful behavior beyond that: DB-backed and instant, the worker sends these by itself (no manual send needed), and results are capped with a `truncated` signal. It stops short of describing row fields, but that is a minor gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core purpose, then timing behavior, then the alternative-tool routing, in compact sentences with no filler. Every clause carries information an agent would otherwise have to infer.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only list tool with no output schema, the description still discloses result sizing (limit default 100) and the truncation signal, plus the routing to sibling tools. Nothing needed to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents limit, sessionId, and sessionLabel fully (including the default and how to resolve ids). The description restates the limit/truncated behavior but adds no syntax or format detail beyond the schema. Baseline 3 is correct when structured data does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource with an exact scope: contacts whose invite was ACCEPTED but first message not yet sent (the Day-1 starter queue). It names the sibling it is not (li_due_messages for past-due openers) so an agent can disambiguate without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes to alternatives with the selecting condition: past-due openers go to li_due_messages, timed steps to li_scheduled_steps, and new acceptances are recorded via li_poll_acceptances. It also explains when rows naturally appear (connectionWaitHours / sending hours), which is the 'when this is normal' context an agent needs.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_accept_invitationsAccept Inbound Connection RequestsA
Destructive
Inspect

Accept connection requests sent TO this account. Provide profileUrls to accept only specific people; omit to accept all pending up to maxAccept. State-changing — requires confirmAccept=true.

RETURNS A JOB ID AND DOES NOT WAIT. Reading a long invitations backlog takes minutes, and holding the call open used to exceed the client's own idle limit while the run carried on regardless. Read the outcome with li_job_result(jobId) — it carries the per-person outcomes and the pageState. Pass wait=true only for a handful of people.

CONNECTION REQUESTS ONLY. A newsletter-subscription or page-follow invitation is listed against the INVITER's profile, so naming that person used to subscribe the account to their newsletter — which nothing in this system can undo. Those are now refused per person and reported as not_a_connection_invite; acceptNonConnection=true overrides it deliberately.

Every named person comes back in outcomes with a reason: accepted, not_on_page (we read the WHOLE list and their invitation is not on it), page_not_fully_read (we ran out of budget — this says nothing about them), not_a_connection_invite, click_not_trusted or click_failed. pageState says how much was actually read, so a zero-accept run is diagnosable instead of a guess. ok:false means the run went wrong or did not finish reading, NOT that somebody simply had no invitation pending.

AUTO-MARKS THE TRIAGE MEMORY. Every confirmed acceptance is recorded as decision accepted, and click_failed / click_not_trusted / page_not_fully_read as failed with that reason (so they come back under li_received_invitations onlyUnreviewed for a retry). not_on_page writes nothing. triageMemory in the result says how many were written. Do not mark accepted people yourself.

NameRequiredDescriptionDefault
waitNoBlock until the run finishes. Default false — reading a long invitations list takes minutes.
maxAcceptNoCap how many to accept this run. Defaults to the number of profileUrls given, or 20 when accepting whatever is pending.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
profileUrlsNoAccept only these profile URLs; omit to accept all pending.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
confirmAcceptYesMust be true to accept; false reads the page and previews what would happen
acceptNonConnectionNoAlso accept newsletter subscriptions and page follows. Default false. There is no way to undo one from here.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the destructive/openWorld annotations: it discloses that the tool returns a job id without waiting and why, that non-connection invitations (newsletter/page-follow) are irreversible if wrongly accepted and are now refused by default, that confirmAccept=true is required for mutation, and that the run auto-writes triage memory decisions including failed-retry reasons.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core action, scoping, and the confirmAccept requirement in the first sentence. It is long (five paragraphs), but with no output schema the outcome/triage detail is load-bearing rather than filler; only minor tightening is possible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Because there is no output schema, the description carries the full return contract: job-id async pattern, per-person outcome reasons, pageState diagnosability, the meaning of ok:false, and the triageMemory side effect. An agent can call and interpret this tool without further documentation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: why wait defaults false, how maxAccept defaults differ (profileUrls count vs 20 pending), what confirmAccept=false produces ('reads the page and previews'), and the irreversible consequence guarded by acceptNonConnection.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource with explicit scope: 'Accept connection requests sent TO this account.' The 'TO this account' / inbound framing distinguishes it from outbound siblings like li_send_connection_now, and the CONNECTION REQUESTS ONLY constraint sharpens the boundary further.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear conditional guidance: pass profileUrls to target specific people, omit to accept all pending up to maxAccept, and 'Pass wait=true only for a handful of people.' It also routes retries to li_received_invitations onlyUnreviewed and reading to li_job_result. It does not explicitly name a near-alternative tool for the accept action itself, but the operational conditions are well specified.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_account_activityWhat Every Account Is Doing Right NowA
Read-only
Inspect

Live per-account picture: what is RUNNING this second, what is queued behind it, what is scheduled next, plus today's invite/message counts, the pending-invite backlog, the invite-cleanup state, notes (personalized invite notes sent this month and whether LinkedIn says the monthly allowance is used up; LinkedIn publishes no number) and inviteHold (new invites held after repeated identical failures; clear with li_clear_invite_hold). Read this before telling the user an account is stuck or idle — work runs one browser session at a time, so "queued" usually means something ahead of it is still running, not that anything is wrong. live.summary is a ready-made one-line answer per account. Omit the selector for every account you can see. DB- and queue-backed, so it costs no browser time. Configuration (hours, plan, booking) is li_list_accounts; the day's totals are li_daily_digest; one job's outcome is li_job_result.

NameRequiredDescriptionDefault
sessionIdNoLimit to one account by id
sessionLabelNoLimit to one account by label

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/non-destructive/non-openWorld, but the description adds real behavioral context: work runs one browser session at a time so 'queued' is normal, the tool is DB/queue-backed and costs no browser time, and inviteHold/notes semantics are explained including LinkedIn's lack of a number. Only return shape for other fields is left implicit.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but front-loaded: the core picture comes first, then the stuck/idle guidance, then the sibling routing. It runs long as one paragraph, but nearly every clause carries distinct information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by describing the returned content (live.summary one-liner, notes, inviteHold) and the cost model. An agent has everything needed to call and interpret it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so both params are documented there; the description still adds useful meaning by stating that omitting the selector returns every visible account, which the schema does not convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope: a live per-account picture of running, queued, and scheduled work plus invite/message counters. It explicitly distinguishes itself from li_list_accounts (configuration), li_daily_digest (day totals), and li_job_result (one job's outcome).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use rule ('Read this before telling the user an account is stuck or idle') and names the alternative tools for adjacent questions. It also states the default behavior when no selector is passed and the recovery tool for inviteHold.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_booking_dueBookings Due For A Nudge Or A CloseA
Read-only
Inspect

People who AGREED to a call and have not booked, whose check date has arrived — the list the booked-call routine works from. DB-backed and instant.

Each row: name, firstName/lastName, company, companyDomain, headline, accountId/accountLabel, campaignName, bookingLinkSentAt (null = the link has not gone out yet — send it rather than a nudge), agreedDayText, agreedDate, nudgeCount of maxNudges, and nextAction: nudge while nudges remain, else close. Plus thread — the last messages of the stored conversation on that account, oldest first — so the nudge can refer to what they said.

DROPPED and counted in excluded: anyone who wrote after the link or their agreement (they are owed an answer: li_replies_to_answer lists them flagged bookingPending), anyone on a do-not-contact list, anyone on a paused account, and anyone whose verdict was changed by hand. Someone only sent the link, who never agreed, is never here.

Nothing here sends anything. Send a nudge with li_send_reply(bookingNudge: true) and a close with li_send_reply(bookingClose: true) — both re-check every rule and refuse in words.

NameRequiredDescriptionDefault
limitNoMost rows to return (default 50, max 200); more are flagged
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
sessionLabelNoScope to one account label
sessionLabelsNoSeveral accounts in ONE call, by label. Leaving every selector off already spans all your accounts.
threadMessagesNoHow many recent messages of each thread to attach (default 8; 0 = none)

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and destructiveHint=false, and the description reinforces this with 'Nothing here sends anything,' then adds substantial non-obvious behavior: which cohorts get dropped and counted in `excluded`, that `nextAction` flips from nudge to close when nudges run out, and that the send tools re-check every rule and refuse in words. This is meaningfully more than the annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is front-loaded with the one-line definition of the population, then structured into row-field, exclusion, and action paragraphs where each sentence carries information. It is dense and long, but nearly every clause earns its place; only marginal tightening is possible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description enumerates the returned row fields (name, company, bookingLinkSentAt, nextAction, thread, etc.) and explains their meaning, the exclusion accounting, and the follow-up tools. For a five-parameter read tool with annotations covering safety, nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all five parameters (limit, sessionId, sessionLabel, sessionLabels, threadMessages) are already documented in the schema with defaults and bounds. The description adds field-level meaning via the `thread` / bookingLinkSentAt explanation, but says nothing about selector or limit semantics beyond what the schema provides, so the baseline 3 is correct.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a precise population and scope: people who AGREED to a call, have not booked, and whose check date has arrived — the worklist for a booking routine. It explicitly distinguishes itself from li_replies_to_answer (those who wrote back) and from the case of someone only sent the link who never agreed, so an agent can separate it from siblings like li_bookings without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It routes the agent concretely: nudge via li_send_reply(bookingNudge: true) and close via li_send_reply(bookingClose: true), and points the excluded cohort at li_replies_to_answer. It doesn't phrase an explicit 'use this when X, not Y' rule, but the worklist framing plus named alternatives supplies clear usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_bookingsBooked Calls (and other booking states)A
Read-only
Inspect

List bookings on the accounts you can see, each with calendarEventId, meetingStart, bookedAt, the contact and the account. Defaults to status booked; pass status for others (agreed, cancelled, closed, link_sent). from/to filter on meetingStart. DB-backed and instant; up to limit rows (default 100), with truncated when there are more. The booked-call routine reads this to spot a booking whose calendar event has vanished — then li_mark_booking_cancelled. Who is due a nudge is li_booking_due; booking conversion rates are li_outcome_summary.

NameRequiredDescriptionDefault
toNomeetingStart on or before, ISO 8601
fromNomeetingStart on or after, ISO 8601
limitNoMost rows to return (default 100, max 500)
statusNoBooking states to include (default ["booked"])
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see.
sessionLabelsNoSeveral accounts in ONE call, by label. Leaving every selector off already spans all your accounts.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and destructiveHint=false, so safety is covered. The description goes beyond that by disclosing the storage/performance profile ('DB-backed and instant'), the truncation behavior ('truncated when there are more'), the default status, and the returned fields — genuinely useful behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose, then filtering/limits, then routing to sibling tools. It is dense but every clause carries information (returned fields, defaults, truncation, workflow), with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still tells the agent what comes back (calendarEventId, meetingStart, bookedAt, contact, account) and the truncation signal. For a read-only list tool with a rich schema, this is complete enough to call correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description reinforces key semantics: the status default and alternative values, from/to filtering on meetingStart, and the limit default (100). It adds marginal framing beyond the schema rather than substantial new syntax.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (List) and resource (bookings) with scope ('accounts you can see') and enumerates the key returned fields. It distinguishes itself from relatives by naming li_booking_due and li_outcome_summary for adjacent needs, so an agent can route without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use via the booked-call routine ('reads this to spot a booking whose calendar event has vanished — then li_mark_booking_cancelled') and names alternatives with their distinct conditions (li_booking_due for nudges, li_outcome_summary for conversion rates). Both the action and the exclusions are spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_bulk_edit_stepsRewrite One Sentence Across A Campaign’s Unsent CopyA
Destructive
Inspect

Find an exact sentence in a campaign's UNSENT message steps and replace it, across every contact in one call.

A strategy's text is snapshotted PER PROSPECT at import, so a retired line exists once per person; use this for the SAME sentence campaign-wide, li_edit_step for per-person corrections.

⚠️ EXACT SUBSTRING, NEVER A REGEX or a pattern of any kind. $& and $1 in the replacement are literal characters. That is a safety property, not a simplification: there is no prior text stored anywhere, so a pattern typed against hundreds of live queues could not be undone.

⚠️ dryRun DEFAULTS TO TRUE. The first call previews: it reports how many steps match, what each would become, and how many CONTACTS hold no match at all. Pass dryRun=false to write.

⚠️ READ contactsUnmatched — it is the field that tells you your find-string was wrong. "191 changed, 23 people do not have that sentence" is a completely different fact from "done", and only that number says which. truncated means the cap stopped the scan, so the sweep is PARTIAL: run it again, or raise limit.

Every write goes through the same per-step guards as li_edit_step, so a step that sends between the preview and the write is refused rather than overwritten and lands in refused. Nothing is rescheduled — only the words change.

NameRequiredDescriptionDefault
findYesThe exact text to find, matched as a plain substring. An empty string is refused — it matches every step.
kindNoNarrow to ONE step of the sequence (e.g. followup_1). Omitted sweeps all of them.
limitNoHow many unsent steps to scan (max 500). The reply says `truncated` when the cap stopped it, so a partial sweep never reads as a complete one.
dryRunNoDEFAULTS TO TRUE — preview only, nothing written. Pass false to apply the rewrite.
replaceYesWhat to put in its place, inserted literally. Pass "" to delete the text.
campaignIdYesThe campaign whose unsent copy to sweep — from li_list_campaigns.
afterStepIdNoContinue a capped sweep from here — pass the `resumeAfterStepId` the previous reply gave you. Without it a second call re-reads the SAME window, and because the first run already replaced those lines they come back as `contactsUnmatched` — the field that means your find-string was wrong. So "run it again" needs this, or it reports the opposite of what happened. 1,630 steps against a cap of 500 is three calls.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true but say nothing about mechanics; the description fills that gap richly: substring-only matching with no undo because no prior text is stored, dryRun defaulting to true, per-step guards that refuse steps mid-send rather than overwrite, and no rescheduling. This is exactly the extra context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action, then warnings in descending priority. Every warning maps to a real failure mode (regex assumptions, accidental writes, misreading contactsUnmatched, partial sweeps), so the length is justified, though the emoji-prefixed callouts make it denser than strictly necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description takes on return-value explanation and does it well, naming contactsUnmatched, truncated, refused, and resumeAfterStepId and what each means. For a destructive 7-parameter bulk mutation, nothing an agent needs to call it safely is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: the afterStepId re-run pitfall (re-reading the same window makes replaced lines surface as contactsUnmatched), the semantics of truncated vs a complete sweep, and that find/replace are literal. That materially improves correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: find an exact sentence across a campaign's UNSENT steps and replace it, campaign-wide in one call. It explicitly distinguishes itself from the sibling li_edit_step (per-person corrections), so an agent can route correctly without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use rule (the SAME sentence across every prospect) and names the alternative (li_edit_step) with the condition that selects it. It also covers the operational follow-ups: dryRun must be passed false to write, and afterStepId is required to continue a capped sweep.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_campaign_contactsLinkedIn Campaign ContactsA
Read-only
Inspect

Contacts in a campaign with both stages, per-step sequence progress, the next planned action, what the CONVERSATION is doing, and the failure — filed under the stage it came from.

PAGED. Without limit this returns the first 50 contacts (max 200), and a ROWS reply always carries totalContacts, returned, offset and truncated — a page is never the whole campaign, and the rest are unread rather than absent. The summary view carries totalContacts and counts over every contact, so it has nothing to truncate and reports no page. Walk on with offset (nextOffset). FILTERS run over the whole campaign before the page is cut: connectionStatus ("none" = imported already connected), conversationState, hasInbound; the reply then adds matchingContacts, and truncated is about the matches. For "how is this campaign doing", pass view="summary": counts over EVERY contact by contact status, message status, connection status, conversation state and failure category, with no per-contact rows at all.

THREE statuses, each NAMED for its stage because they share four words. contactStatus is the CONTACT roll-up (pending | active | replied | handed_off): it only moves forward and says nothing about what is queued. messageStatus is the MESSAGE lifecycle (awaiting_connection | pending | in_progress | replied | handed_off | completed | failed), where pending really does mean a message is queued to send. connectionStatus is the INVITE lifecycle (not_sent | invited | accepted | withdrawn | declined | failed). Read the one whose name says the stage you mean.

messageStatus is CORRECTED: a failed message sitting on a LIVE invite reads awaiting_connection, because acceptance revives it. messageStatusRaw is what the database stores and messageStatusSuperseded is true when the two differ.

TWO failures, never both at once. connectionFailure is the INVITE stage (the connection ended failed/declined/withdrawn — the retry is a fresh invite, and a withdrawn one is blocked for about three weeks). messageFailure is the MESSAGE stage (the send failed on a connection we already have — the retry is a re-send to someone who may already have the text). Never offer one as the other. Both carry category/isPermanent/retryable; retryable is narrower than !isPermanent, because an invite LinkedIn quietly ignored is not permanent but is time-gated.

COMPACT BY DEFAULT: a row carries no steps array and no copy, only stepCounts (its steps by status) and connectionNoteLength with connectionNoteOmitted: true. Pass includeSteps=true, or any text mode, for every step and its copy. With steps, text picks how much copy: "preview" (the default) clips the note and each step to 120 characters and says so per field (textLength/textTruncated, connectionNoteLength/connectionNoteTruncated), "full" returns them whole (to review what will send), "none" keeps only lengths (textOmitted: true). ⚠️ THE TWO "truncated"S ANSWER DIFFERENT QUESTIONS: the reply's truncated is about CONTACTS; a field's textTruncated is about that string. The reply echoes copyMode and stepsIncluded. format: "csv" returns the rows as one CSV string (csv) with dotted columns (conversation.state, steps.0.id).

With steps included, every step carries its id and position, so li_edit_step and li_cancel_steps can be driven straight off this read rather than through a dossier per person.

conversation.state folds both stages and the thread into one answer (invite_pending, awaiting_reply, needs_reply, send_failed, no_reply …), and a human-marked outcome (won / meeting_booked / interested / not_interested / no_outcome) always wins over it.

conversation.seenByThemAt is when the other side read the LATEST message we sent — the LinkedIn read receipt, captured whenever the stored thread is read. Null means NO RECEIPT — they have not opened the thread, or read receipts are switched off on either side (a mutual LinkedIn setting); the two are indistinguishable, so never read a null as unseen or not-read — only the positive claim (seen, with a time) is honest.

NameRequiredDescriptionDefault
textNoHow much COPY each row carries — the connection note and every step text. Setting it includes the steps. "preview" (the default with steps) clips each to 120 characters and says so per field; "full" returns them complete (to review what will send); "none" keeps only lengths.
viewNo"summary" returns the counts for the WHOLE campaign — every status, conversation state and failure category, with no per-contact rows. It is the cheap way to ask "how is this campaign doing"; "rows" (the default) is for acting on named people.
limitNoHow many contacts to return (default 50, max 200). The reply always says the real total and whether it was cut.
formatNo"csv" = one header + rows string instead of the `contacts` array (rows view only)
offsetNoSkip this many contacts — how to walk past the first page.
campaignIdYesCampaign id from li_list_campaigns
hasInboundNotrue = only contacts who have written to you; false = only those who have not
includeStepsNotrue = each row carries its `steps` (ids for li_edit_step / li_cancel_steps). Default false: rows carry `stepCounts` only.
connectionStatusNoOnly contacts whose invite is in one of these statuses ("none" = imported already connected)
conversationStateNoOnly contacts whose `conversation.state` is one of these (a marked outcome wins)

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only cover the safety profile (readOnly), so the description carries the rest and does so richly: pagination semantics and the meaning of `truncated`, the three overlapping status vocabularies, the messageStatus correction rule, the two mutually exclusive failure stages with retry gating, compact-by-default behavior, and the seenByThemAt null caveat. This is exactly the beyond-annotations context the rubric rewards.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded and the paragraphing is logical, but the entry is very long for a 10-parameter read tool and duplicates the `text` mode explanation already fully present in the schema. The all-caps emphasis and repeated 'truncated' warnings are earned, yet the overall length is heavier than necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists and the tool is complex, but the description documents the return shape thoroughly (totalContacts/returned/offset/truncated, matchingContacts, csv dotted columns, stepCounts vs steps, conversation.state and outcome precedence). An agent can call this correctly and interpret the reply without further context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real semantics the schema does not: filters run over the whole campaign before the page is cut, the reply then adds `matchingContacts` and `truncated` refers to matches, and connectionStatus "none" means already-connected imports. That is genuine meaning beyond the per-parameter schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: it returns the contacts in a campaign, along with per-contact sequence progress, next action, conversation state and failure. It also positions itself against siblings, noting that with steps included the rows drive li_edit_step/li_cancel_steps 'rather than through a dossier per person' and pointing to li_list_campaigns for the id.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing: 'For "how is this campaign doing", pass view="summary"' vs rows 'for acting on named people'; offset/nextOffset for paging; the filter list and what it filters over. Alternatives and the conditions that select them are named rather than implied.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_campaign_statusLinkedIn Campaign StatusA
Read-only
Inspect

Counts for ONE campaign across both stages: connection statuses (not_sent, invited, accepted, withdrawn, declined, failed) and message statuses (awaiting_connection, pending, in progress, replied, handed off, completed, failed), plus the strategy, connectionWaitHours and ICP attached, priority (true = its invites go before the account's other campaigns), and timezoneCoverage (which contacts have a known timezone, for prospect-local sending windows). DB-backed and instant; counts only, no people. Get the id from li_list_campaigns. For the people behind the counts, conversation states and failure categories use li_campaign_contacts (view="summary" for counts over every contact); for what sends next, li_scheduled_steps; to compare campaigns, li_outreach_analytics.

NameRequiredDescriptionDefault
campaignIdYesCampaign id from li_list_campaigns

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish read-only, non-destructive, closed-world behavior, so the bar is lower; the description still adds that the call is DB-backed and instant and returns counts only, no people, which tells the agent about latency and payload shape. It stops short of any pagination or limit disclosure, but for a single-campaign counter that gap is minor.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core answer (counts for one campaign), then notation of the extra fields, then routing to siblings. It is dense and long-winded in places, but every clause carries information an agent needs, particularly the status enumerations that substitute for a missing output schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the return-value burden and does so fully: it enumerates connection and message statuses, names the extra attached fields, and clarifies priority semantics and what timezoneCoverage means. Nothing an agent needs to interpret or call the tool correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% for the single campaignId parameter, and the description's 'Get the id from li_list_campaigns' merely restates the schema's own note. No additional format or validation meaning is added, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource with scope: counts for ONE campaign across both connection and message stages, plus attached strategy, connectionWaitHours, ICP, priority and timezoneCoverage. The enumerated status vocabularies make clear exactly which counts are returned, so an agent can distinguish it from siblings without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes to alternatives by intent: li_campaign_contacts for the people behind the counts, li_scheduled_steps for what sends next, li_outreach_analytics for campaign comparison. It also names the prerequisite source for the id (li_list_campaigns), leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_cancel_scheduled_postCancel Scheduled PostA
Destructive
Inspect

Cancel ONE scheduled post that has not gone out yet, by its id from li_scheduled_posts; it stays listed there as cancelled. Refuses if it has already been published — cancelling something already posted must not read as if it un-posted it, and nothing here deletes a live post. Touches only our queue: no browser, instant. Returns the cancelled id.

NameRequiredDescriptionDefault
idYesScheduled post id from li_scheduled_posts

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only say destructiveHint=true; the description goes well beyond that by disclosing the resulting state ('stays listed there as `cancelled`'), the refusal condition, the explicit non-effect on live posts, and the cost profile ('no browser, instant'). It even states the return value, which no output schema provides.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the action and scope, and each subsequent clause carries a distinct piece of information (state transition, refusal, side-effect boundary, return, latency). Slightly dense but no throwaway sentences.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter mutation tool with a destructiveHint annotation and no output schema, the description covers what changes, what is refused, what is untouched, and what is returned. Only auth/permission requirements are unstated.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single `id` parameter and its description already says it comes from li_scheduled_posts, so the description's mention of the same origin adds little. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb (cancel) plus a precisely scoped resource: 'Cancel ONE scheduled post that has not gone out yet, by its id from li_scheduled_posts.' It also distinguishes itself from the adjacent create/schedule/list siblings by clarifying this acts on the queue only and deletes nothing live.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a clear precondition (must not have gone out yet) and a refusal case (already published), and points to li_scheduled_posts as where the id comes from. It stops short of naming an explicit alternative tool for the already-published case, which is the only gap.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_cancel_stepsHold Queued Messages (cancel scheduled steps)A
Destructive
Inspect

Cancel specific scheduled sequence messages so they are never sent, and NOTHING ELSE. This is the tool for "hold tomorrow's Day-3, the wording is wrong" — a narrow hold on the steps you name, and nothing more.

🚨 WHAT MAKES IT DIFFERENT FROM li_mark_stopped AND li_mark_outcome, which were the only way to do this: those hand the SEQUENCE off. They set the message to handed_off and advance the contact, and handed_off is terminal for the sequence — no further automated step goes out, the contact reads as closed everywhere, and acceptance can no longer revive them (only a new message from them reopens the conversation, which the inbox sweep (about hourly) still watches for). So "hold one message" could only be said as "this person is finished with". This writes MessageStep.status = 'canceled' on the steps you name and touches no message status, no contact status and no outcome.

SO THE SEQUENCE STAYS ALIVE. The remaining steps are still pending on a live message and the drip carries on from the next one — and the next one is RE-CHAINED: if you hold the step the clock was pointing at, the one after it is re-stamped with its own delay so it actually comes due, instead of sitting pending with no time on it for ever. Each result says which step was re-pointed and when, under rescheduled. Absent means no re-chain was needed (the held step was not the scheduled one, or nothing follows it) — never that one was skipped.

Step ids come from li_campaign_contacts (every step carries its id) or li_contact_dossier. Holding several at once is fine and expected; a step named later in the same call is never used as a re-chain target, so holding Day-3 and Day-7 together cannot strand Day-14.

Refused per row, with the rest of the batch still applied: a step that is not pending (nothing is queued for it, so holding it would change nothing), a step whose sequence is replied / handed_off / completed / failed (it will never be sent), and a step on a campaign you cannot see (reported as 'Not found', the same as one that does not exist).

⚠️ A HOLD CANNOT CURRENTLY BE UNDONE, and no other tool will do it either — stated plainly because an instruction that cannot run is worse than none. li_edit_step only touches a step that is still pending, so it refuses a held one; li_reset_contact refuses a sequence that is pending / in_progress / awaiting_connection, which is exactly what this tool leaves behind. So hold a step when you want it NOT sent, and if the message is one you still want to send, reach for li_edit_step on it BEFORE holding it — rewriting the wording is reversible where holding is not. To stop reaching somebody altogether, that IS li_mark_stopped or li_mark_outcome.

NameRequiredDescriptionDefault
reasonNoWhy — written onto the timeline, ONE entry per sequence rather than one per step, so six held steps across two people is two entries.
stepIdsYesThe steps to hold, up to 200 — their `id` from li_campaign_contacts or li_contact_dossier. Each is applied and reported on its own.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag destructiveHint=true, but the description goes far beyond that: it discloses exactly which field is written, that no message/contact status or outcome is touched, the re-chaining of the next step, per-row refusal semantics, and the critical fact that the hold is irreversible. This is rich behavioral context the annotations alone cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and its scope before any of the comparison or caveat material. The length is largely justified by the irreversibility warning and sibling disambiguation, though the emphatic ALL-CAPS phrasing and emoji headers add some bulk that a tighter edit could remove.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema, the description explains the return surface (the `rescheduled` field and what its absence means) and covers edge cases an agent must know: batch partial failure, non-pending steps, invisible campaigns reported as 'Not found', and irreversibility. Nothing needed to invoke this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: step ids come from li_campaign_contacts or li_contact_dossier, multiple steps can be held in one call, and a step named later in the same call is never used as a re-chain target. The reason field's per-sequence (not per-step) timeline behavior is also clarified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource with scope: 'Cancel specific scheduled sequence messages so they are never sent, and NOTHING ELSE,' then pins the exact state change (MessageStep.status = 'canceled'). It explicitly distinguishes itself from li_mark_stopped and li_mark_outcome, so an agent can route correctly without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives the triggering scenario ('hold tomorrow's Day-3, the wording is wrong'), names the alternatives (li_mark_stopped/li_mark_outcome for stopping entirely, li_edit_step for rewording), and states the ordering constraint ('reach for li_edit_step on it BEFORE holding it'). Both when-to-use and when-not-to-use are covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_check_acceptanceCheck Connection AcceptanceA
Read-only
Inspect

Ask LinkedIn right now whether invites were accepted. Read-only: it records nothing. With profileUrl: that one person's state (connected | pending | not_connected | unknown). Without it: the account's pending sent invites and recent connections as public-id lists with counts (pendingInvites, connections) — the signals the poller compares. Opens a browser session on the account, so it queues behind other work. ok: false with pendingListRendered: false means the sent list could not be read, NOT that nothing is pending. To record acceptances and schedule first messages use li_poll_acceptances; for invites already recorded use li_connections (DB, instant).

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
profileUrlNoOptional single profile URL to check; omit for the batch signals scan
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the readOnlyHint/destructiveHint annotations by disclosing that it opens a browser session and queues behind other work, and by pre-empting a critical misread: ok:false with pendingListRendered:false means the sent list could not be read, NOT that nothing is pending. That is exactly the kind of behavioral context annotations cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core purpose and then layers behavior, error semantics, and alternatives with no filler sentences. It is dense, but each clause carries distinct information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, so the description carries the return contract, and it does: the per-profile state enum, and the batch fields pendingInvites and connections as public-id lists with counts. Nothing an agent needs to call it and interpret results is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real semantic value by spelling out the behavioral difference between supplying profileUrl (single-person state, with an inline enum of connected|pending|not_connected|unknown) versus omitting it (batch pending/connection signals). Useful beyond the schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Ask LinkedIn right now whether invites were accepted') and then precisely delineates the two modes from profileUrl. It distinguishes itself from siblings li_poll_acceptances and li_connections by name, so an agent can route without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes: use li_poll_acceptances to record acceptances and schedule first messages, and li_connections for already-recorded invites from the DB. Both the when and the when-not are stated with the selecting condition, leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_check_proxyCheck a Proxy’s Real Egress (operator only)AInspect

OPERATOR ONLY. Asks ipinfo, THROUGH the proxy, which IP it really exits from, then what type that IP's network (ASN) is. verdict: pass = isp, reject = hosting (a datacenter — a known LinkedIn risk), review = anything else or the type lookup failed, failed = the proxy could not be reached (the error says why; credentials are never in it). "ISP proxy" is a vendor label — this is the check that counts, and vendors have changed the network under an unchanged label, so re-check after any renewal. Pass proxyId from li_proxies, or default for the install-wide env proxy. No browser, no LinkedIn traffic; the result is stored on the proxy.

NameRequiredDescriptionDefault
proxyIdYesA proxy id from li_proxies, or "default"

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnlyHint=false, openWorldHint=true, destructiveHint=false) it discloses that no browser or LinkedIn traffic is involved, that the result is persisted on the proxy, that credentials never appear in error messages, and it enumerates the verdict semantics (pass/reject/review/failed). This is substantial behavioral context the annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but front-loaded: the operator restriction and the core action come first, followed by verdict semantics and the parameter hint. Slightly long with some parenthetical asides, but nearly every sentence carries information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by defining all four verdict values and the failure mode, plus what the call does and does not touch. An agent has everything needed to invoke it and interpret the result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the single parameter's description already covers the id-or-"default" contract, so the description adds no new syntax or format detail beyond the schema. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource: it asks ipinfo through the proxy which IP the proxy really exits from and classifies that IP's network type. It is clearly distinct from the sibling li_proxies (which merely lists proxies), so an agent can pick between them without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says OPERATOR ONLY, tells the caller to pass a proxyId from li_proxies or "default" for the install-wide env proxy, and instructs re-checking after any renewal. It even explains why the check is needed over the vendor's own label.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_check_targetsWho Is Already In The Pipeline (pre-import check)A
Read-only
Inspect

Read-only: who among these profile URLs has ALREADY been approached from the accounts you can see. It writes nothing, queues nothing and BLOCKS nothing — a collision is information, not an error, and importing one is allowed.

WHEN NOT TO CALL IT: not before an import. li_ingest_prospects and li_create_campaign run this very check themselves, just before they write, and hand the report straight back as collisions — so calling this first does the same work twice and returns the same answer twice. For li_ingest_prospects theirs is also the BETTER answer: it knows which campaign the batch is landing in, so people already in it read as in_this_campaign, an idempotent update that costs nothing, while a call made out here without a campaignId files those same people as a clash somewhere else. (li_create_campaign CREATES the campaign, so there is nothing to be already in — its report is the same one you would get here, minus the extra call.) Reach for this when you have NOT decided to import — triaging a list somebody just sent you, deciding whether a batch is worth paying to research before you spend lookups on it, or answering "do we already know these people" as a question of its own.

Why it matters: re-importing someone into the SAME campaign is idempotent (their note and unsent step text are refreshed, nothing new is queued). Importing them into a DIFFERENT campaign creates a second invite the drip will attempt, and what happens is decided at send time — LinkedIn showing a pending invite yields "already invited" and someone already 1st-degree yields "already connected", neither of which sends anything, but BOTH still spend a ~50s browser session that this account then has to wait behind. Anything else sends a REAL second invite, charged against each account's rolling 7-day invite allowance this system enforces, set by the account's detected LinkedIn tier — 63-70 a week (5-10 a day) new or unverified, 90-100 a week (18-24 a day) established, 135-150 a week (25-30 a day) Premium, redrawn within the range each week. Our own policy, not a published LinkedIn limit; the daily band exists only to stop a burst. li_list_accounts gives each account's actual numbers.

Each person lands in exactly one bucket, most advanced first: in_this_campaign (benign — an idempotent update), messaged_elsewhere, connected_elsewhere, invited_elsewhere, unreachable_elsewhere (a PERMANENT invite failure is on record, so retrying spends a session for nothing), closed_elsewhere, and queued_elsewhere (another campaign has them queued and has NOT invited them yet — the only collision still cheap to avoid, since both campaigns will now invite them and the second is a duplicate out of the same daily budget). collisions names each one with its campaign and account, counts totals the buckets, fresh is how many none of your accounts has touched, and summary is one true sentence safe to relay verbatim.

THE BUCKET IS ONLY THE HEADLINE. A person can be in several campaigns on several accounts: otherCampaigns counts the rest per person, and view: "people" lists EVERY membership of every person (campaign, account, campaign status, connection, message, outcome, inbound) as one compact table — up to 1000 URLs in one call, so it is also the way to ask which of a list are in which campaigns. Building several new campaigns at once? Pass campaignNamePrefix (or campaignId): anyone already in one of them is named in batch.members whatever their bucket says.

Set groupByCompany: true to also group these people by employer and say which firms you already have a contact at. Read a warm firm BOTH ways: somebody there already knows you, or the same note is about to land twice inside one company. Only you can tell which, so this reports it and holds nothing. Employers come from profiles already looked up plus your own contact rows, so unknownCompany means "not matched", never "no employer".

The report is BOUNDED BY WHAT YOU CAN SEE: it covers your own accounts and reports nothing at all — no name, no count, no bucket — about anything outside them, so read a clean result as "not in your pipeline" rather than "nobody has ever approached them". scopeNote says the same in the reply. Pass campaignId when you already know the campaign you are importing into; without it, people who are already IN that campaign are reported as collisions elsewhere, which overstates the cost.

The reply is GROUPED by default (view: "summary"): the explanation is said ONCE per bucket instead of once per person, each campaign is described once in sites instead of on every row, and every person named keeps their URL, their name and a site index pointing at their own campaign and account — measured at 56% smaller on a batch with sixty collisions, and the saving grows with the batch. ⚠️ IT IS CAPPED, so read peopleHidden: a bucket names at most 50 people and states how many it did not, while count stays the true bucket size — sixty in one bucket gives you fifty names and peopleHidden: 10, and those ten are NOT fresh. view: "full" names every one of them, in the order you submitted them, each carrying its own copy of the sentence.

NameRequiredDescriptionDefault
viewNoHow much to return. "summary" (the default) groups the colliding people by bucket and lists each campaign once in `sites`, naming up to 50 people per bucket (`peopleHidden` counts the rest) and `otherCampaigns` per person. "people" is EVERY campaign each person is in, one table row per person per campaign (account, campaign status, connection, message, outcome, inbound), uncapped and compact — for "which of these URLs are in which campaigns". "full" returns flat rows with every sentence, plus `people` with whole membership objects.
fieldsNoview "people" only: which columns besides profile and campaign, from name, bucket, connection, message, outcome, inbound. Default all.
formatNoview "people" only: "json" (default) returns `rows` as arrays matching `columns`; "csv" returns the same table as `csv` text.
sessionIdNoAccount you intend to import from (id)
campaignIdNoThe campaign you are importing INTO, when it already exists. People already in it are then reported as `in_this_campaign` (an idempotent update) instead of as a collision elsewhere.
profileUrlsYesLinkedIn profile URLs to check, max 1000 per call. A longer list is REFUSED rather than silently trimmed — split it and call again.
sessionLabelNoAccount you intend to import from, e.g. "Deven". Validated only — naming it does NOT narrow the check, because a collision on another of your accounts is still a collision and hiding it would defeat the point.
groupByCompanyNoAlso group these people by employer and name the firms you already have a contact at. Off by default because it costs extra queries. Nothing is held either way.
campaignNamePrefixNoThe campaigns you are BUILDING, by name prefix (case-insensitive), e.g. "PR test Oct". Anyone already in one of them is listed in `batch.members` (never capped) and marked `inBatch`, whatever their bucket says. Use it when splitting a list across several new campaigns.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare read-only and non-destructive, but the description goes far beyond them: it states it writes nothing, queues nothing and blocks nothing, documents collision-as-information semantics, invite-allowance costs, the bounded-by-visibility scope, result capping via peopleHidden, and default grouping. Rich context that annotations alone could never provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Strongly front-loaded (read-only guarantee first, then exclusions, then positive cases) and organized into clear sections. It is very long and includes illustrative tangents (56% size reduction, ~50s browser sessions, tier allowance bands) that are informative but not all essential to invocation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description fully carries the burden of explaining returns: it details every bucket, collisions/counts/fresh/summary, the view modes, the people table, otherCampaigns, batch.members, and the peopleHidden cap. Nothing an agent needs to interpret the reply is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so the schema already documents each parameter; the baseline would be 3. The description adds genuine meaning on top, notably the consequence of passing campaignId (idempotent in_this_campaign vs overstated collisions without it), that groupByCompany costs extra queries, and how campaignNamePrefix marks batch members.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Read-only: who among these profile URLs has ALREADY been approached from the accounts you can see') and immediately distinguishes scope from siblings by naming li_ingest_prospects and li_create_campaign. An agent can tell exactly what this returns without opening anything else.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Contains an explicit 'WHEN NOT TO CALL IT' section that names the alternatives (li_ingest_prospects, li_create_campaign), explains why they already do this check and why theirs is the better answer, and then states the positive cases (triaging a list, deciding whether a batch is worth researching). This is textbook when/when-not/alternatives guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_clear_invite_holdClear an Account’s Invite HoldAInspect

Lift the INVITE BREAKER's hold on one account. The breaker stops NEW invites (messages keep going) after 3 invites in a row to different people failed the same way, and says so in inviteHold on li_account_activity and li_campaign_status. A hold for LinkedIn asking for emails lifts by itself after a day; a hold for an unrecognised invite window (SELECTOR_CHANGED, UNKNOWN, NOTE_LIMIT) waits for this. Clear it only after someone has looked: open the account on LinkedIn and try one invite by hand, or read the saved screenshot. If the cause is still there, the next 3 failures hold it again. Returns the hold that was standing (null = there was none).

NameRequiredDescriptionDefault
sessionIdNoAccount id (or use sessionLabel)
sessionLabelNoAccount label, e.g. "Deven"

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint=false, destructiveHint=false, openWorldHint=true, and the description is consistent with them while adding real context the annotations cannot: the breaker's trigger condition (3 consecutive invites to different people failing the same way), which invite paths it suppresses and which it does not (messages keep going), the auto-expiry rules, the re-hold risk, and the return contract. No contradiction with structured data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The action is front-loaded in the first sentence, followed by trigger mechanics, lifting rules, the safety precondition, and the return value. It is dense and multi-clause, but each sentence carries distinct operational information; only the 'says so in inviteHold on...' reference to sibling tools is slightly tangential.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description closes that gap by stating the return value ('Returns the hold that was standing (null = there was none)'). Combined with the trigger, expiry, and precondition detail, an agent has everything needed to invoke this safely and correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and both parameters (sessionId, sessionLabel) are documented in the schema itself, so the baseline is 3. The description adds no identifier syntax or selection guidance beyond what the schema already provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and object ('Lift the INVITE BREAKER's hold on one account') and immediately distinguishes it from state-of-the-world readers like li_campaign_status by naming those as the places the hold is reported, not cleared. An agent can tell this apart from siblings such as li_hold_sending or li_pause_account.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use and when-not: self-lifting holds (LinkedIn email-request holds clear after a day) versus holds that wait for this call (SELECTOR_CHANGED, UNKNOWN, NOTE_LIMIT). It also prescribes a precondition ('Clear it only after someone has looked: open the account on LinkedIn and try one invite by hand, or read the saved screenshot') and names the consequence of misuse ('If the cause is still there, the next 3 failures hold it again').

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_connectionsLinkedIn ConnectionsA
Read-only
Inspect

List the invites this system tracks (one row per campaign connection), optionally filtered by status or campaign, plus account-health stats: pending invites, rolling weekly invites sent, and the 14-day acceptance rate. DB-backed and instant — it does not ask LinkedIn: for LinkedIn's own pending list, including invites sent by hand, use li_pending_invites; to record new acceptances first, li_poll_acceptances. Rows are capped by limit (default 50); total is the full count and statsScope says whether the stats are one account's or blended. Below 25% an account is worth a look — that is OUR operating policy, not a LinkedIn-published rule, and nothing pauses on it, so report it as something to look at rather than as a fault.

NameRequiredDescriptionDefault
limitNoMost invite rows to return (default 50, max 200); `total` is the full count
statusNoOptional connection status filter
sessionIdNoLimit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account.
campaignIdNoOptional campaign filter
sessionLabelNoScope the stats to ONE account. The acceptance rate is only meaningful per account; omitted, the figures are install-wide across every account.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, non-destructive, not open-world), and the description adds substantial context on top: it is DB-backed and instant and does not query LinkedIn, rows are capped by limit with total as the full count, statsScope indicates whether stats are per-account or blended, and the 25% acceptance threshold is explicitly framed as an internal heuristic that pauses nothing. This is exactly the kind of behavior an agent needs and cannot infer from annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action, then routing, then output/limit semantics, then the policy caveat — a logical order with no wasted sentences. It runs five sentences and is denser than strictly necessary, but each one carries information an agent would otherwise lack.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the burden of explaining returns and does so: one row per connection, capped row count with a separate total, and a stats block (pending, rolling weekly sent, 14-day acceptance rate) whose scope is reported via statsScope. It also flags the cross-account scoping caveat, so an agent has what it needs to call and interpret the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so every parameter is already documented (limit default/max, status enum, sessionId/sessionLabel scoping, campaignId). The description restates the limit/total relationship and the per-account vs install-wide stats distinction, but the schema's own sessionLabel description already carries that meaning. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List the invites this system tracks, one row per campaign connection') along with the optional status/campaign filtering and the accompanying account-health stats. It also explicitly names the siblings it is not (li_pending_invites for LinkedIn's own list, li_poll_acceptances for recording acceptances), so an agent can distinguish it without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing: use li_pending_invites for LinkedIn's own pending list including hand-sent invites, and li_poll_acceptances to record new acceptances first. It also states the operational policy context (the 25% threshold is 'OUR operating policy, not a LinkedIn-published rule') and how to report it, which is genuine usage guidance rather than filler.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_contact_dossierContact Dossier (profile + our history)A
Read-only
Inspect

The full DB record for one contact — or, with profileUrls, for MANY in one call: profile metadata (name, role, company, ICP, fit, hook), every sequence with its steps (text/status/sent), the event history, the stored LinkedIn thread with them (conversation: both sides, oldest first), AND enriched — the research already bought for this person (about, experience, education, follower count, open-to-work), read from cache at no cost and never re-fetched. enriched.fetchedAt says when it was looked up; enriched: null means nobody has looked them up, never that they are uninteresting. company is their CURRENT employer from its LinkedIn company page — the same compact block li_enrich_profiles returns as companyPage (website and domain, industry, size band, HQ, founded, one-line description, specialities) — for judging ICP fit and writing the sequence; company: null means not known yet (look the person up with li_enrich_profiles — the employer comes with it at no extra lookup), and companyUnreadable: true means the read failed. companyWebPages is any web research li_enrich_company already fetched on the firm. posts (on a single contact by default; with profileUrls only when named in fields) is their latest posts as li_fetch_posts last read them, from cache at no cost and never re-fetched; posts: null means never read (use li_fetch_posts), posts.posts: [] means read and nothing of their own, and postsUnreadable: true means the read failed. This is the tool to open before drafting a reply — it is DB-backed and instant, so it costs no browser time. An empty conversation means the thread has never been read, NOT that they never wrote; use li_read_conversation to fetch it live and it will be stored for next time. Rows WE sent carry seenByThemAt — the LinkedIn read receipt, captured whenever the thread is read. Null means NO RECEIPT — they have not opened the thread, or read receipts are switched off on either side (a mutual LinkedIn setting); the two are indistinguishable, so never read a null as unseen or not-read — only the positive claim (seen, with a time) is honest.

Each sequence also carries conversationState (what that conversation is doing, folding both stages and the thread into one word), the human outcome if anyone has marked one, and the failure filed under its STAGE — connectionFailure when the invite ended badly (the retry is a fresh invite) or messageFailure when the send failed on a connection we already have (the retry is a re-send). Never both. Each carries category/isPermanent/retryable, so you can tell a permanent refusal from something worth trying again.

SIZE. One full dossier measures about 14 KB, and roughly four fifths of that is the bought research. Working a reply queue person by person is therefore both the most calls and the most bytes of anything here. Two ways to cut it: name several people in profileUrls (one call, and it leaves the bought research out unless you ask), and name only the blocks you need in fields.

NameRequiredDescriptionDefault
fieldsNoWhich blocks to return: contact, sequences, steps, events, conversation, enriched, company, posts. Naming one of about, audience, signals, currentRole, experience, education, skills narrows "enriched" to that part of the bought research. profileUrl and name always come back. Omit this and one profileUrl returns everything, while profileUrls returns everything except the bought research.
profileUrlNoOne contact, by canonical LinkedIn profile URL (the identity key)
profileUrlsNoSeveral contacts in ONE call, max 50. Each is resolved independently: one refusal does not stop the rest, and the reply reports every one by URL. Unless `fields` says otherwise this leaves out enriched and company and posts — they are the largest blocks and a whole queue rarely wants them — and says so in the reply.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare readOnlyHint/destructiveHint/openWorldHint, but the description adds rich behavior beyond them: cache semantics (never re-fetched, no cost), unreachable flags (companyUnreadable, postsUnreadable), null-means-never-read distinctions, the seenByThemAt receipt ambiguity (mutual setting makes null indistinguishable), failure typing by stage, and the ~14 KB size/byte-cost tradeoff.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is long, but the density is operational rather than filler: the identity of the tool is front-loaded, then the return-field semantics, then the size guidance. A few clauses (e.g. repeating unreadable/null rules) could be trimmed, keeping it just short of a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the full burden and discharges it — it maps every major return field (enriched and its fetchedAt, company vs companyPage, companyWebPages, posts and its three null states, conversation, conversationState/outcome/failure) and disambiguates each null. Nothing an agent needs to interpret results or call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema documents all three params. The description still adds meaning: fields narrows enriched to sub-blocks, profileUrl is 'the identity key', profileUrls resolves each independently so one refusal doesn't stop the rest, and it states default field subsets per call shape.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope: 'the full DB record for one contact — or, with profileUrls, for MANY in one call'. It enumerates exactly which blocks come back (profile metadata, sequences+steps, events, conversation, enriched), so an agent knows precisely what this tool is versus the siblings it names.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use: 'This is the tool to open before drafting a reply — it is DB-backed and instant'. It also routes to alternatives by condition — li_read_conversation for a live thread, li_fetch_posts for posts never read, li_enrich_profiles to populate a missing employer — and explains the single-vs-many call choice and the fields filter.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_conversation_historyStored Chat History With One ContactA
Read-only
Inspect

The saved LinkedIn thread with one person — THEIR messages as well as ours, oldest first. Read this for context before drafting a reply: it is DB-backed and instant, whereas li_read_conversation opens a browser session and takes minutes. History is written automatically whenever a thread is read or a follow-up's reply-check runs, so it can lag; an empty result means the thread has never been read, NOT that they never replied — fetch it with li_read_conversation and it will be stored. Rows WE sent carry seenByThemAt — the LinkedIn read receipt, captured whenever the thread is read. Null means NO RECEIPT — they have not opened the thread, or read receipts are switched off on either side (a mutual LinkedIn setting); the two are indistinguishable, so never read a null as unseen or not-read — only the positive claim (seen, with a time) is honest.

NameRequiredDescriptionDefault
limitNoKeep only the most recent N messages (default 50). Still returned oldest-first.
profileUrlNoLinkedIn profile URL — vanity, the /in/ACoAA… form li_list_conversations returns, or a bare member id
conversationUrnNoThe thread id from li_list_conversations. Works for a person in NO campaign, whose thread is stored by thread. Give this or profileUrl.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With readOnlyHint and destructiveHint=false already covering safety, the description goes well beyond: it discloses that history is written automatically and can lag, that an empty result means 'never read' rather than 'no reply', and it carefully disambiguates a null seenByThemAt as 'no receipt (or receipts off)' rather than 'unseen'. These are exactly the failure modes an agent would otherwise misread.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the essential contrast and storage model, and every sentence carries operational weight. The final seenByThemAt passage is long, but it guards against a specific and serious misinterpretation, so it earns its length rather than being padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description legitimately supplies return semantics: ordering (oldest-first), the seenByThemAt receipt field and its null ambiguity, and the write-back behavior. Combined with the 100%-covered input schema, an agent has everything needed to call and interpret this tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so limit, profileUrl, and conversationUrn are already fully documented. The description adds no syntax or format guidance for any parameter; its extra detail is about returned fields (seenByThemAt), not inputs. Baseline 3 applies when the schema carries parameter semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('the saved LinkedIn thread with one person'), including scope ('THEIR messages as well as ours, oldest first') and storage backing ('DB-backed'). It explicitly differentiates from the sibling li_read_conversation, so an agent can choose between them without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when ('Read this for context before drafting a reply') and a when-not with rationale ('li_read_conversation opens a browser session and takes minutes'). It also spells out the empty-result branch and the recovery action, so the agent knows exactly what to do on each outcome.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_create_campaignCreate LinkedIn Outreach CampaignAInspect

Create a two-stage campaign from prefilled targets under a sending strategy. Each target needs a profileUrl, an optional connectionNote sent with the invite, and text for every post-acceptance strategy step. Accepts a JSON targets array, CSV text, or both. Rows missing step text are rejected. Does not send anything. With NO targets it creates an EMPTY campaign (strategy, ICP and account as usual) to fill later with li_ingest_prospects(campaignId) or li_move_contact.

AT MOST 50 TARGETS PER CALL, counting the JSON array and the CSV rows together. Split a longer list across calls under the same strategy, or use li_ingest_prospects, which takes 500 at a time and buckets them into a managed campaign for you.

result.collisions reports who among these targets had already been approached from the accounts you can see, measured before the campaign was created. It is information and refused nothing — every target was imported — but each person already invited or connected on another campaign is attempted again at send time, spending a browser session even when it sends nothing. You do NOT need li_check_targets before this — the same check runs here and the same report comes back. That tool is for deciding whether to import at all, before you have a batch.

Targets on the do-not-contact list are NOT imported: they come back in result.suppressed with the reason, and the campaign is refused outright when every target is on it.

Engage-before-invite: give a target engagePostUrl (one of their own posts, the one your note mentions) and the account reacts to it first; the invite follows about a day later, inside the window it goes out in (the person's own morning when the account times invites to each prospect, otherwise its sending hours; 18-30 hours with no window to aim at). Reactions are capped per account per day (15-25, our policy) and run in sending hours. To react to a post right now instead, use li_react_to_post.

NameRequiredDescriptionDefault
nameYesCampaign name, shown on the dashboard and in li_list_campaigns
icpIdNoOptional ICP label for analytics
csvTextNoCSV text with profile_url plus optional connection_note and step text columns (first_message, follow_up_1, follow_up_2, follow_up_3, closer), and optional engage_post_url / engage_reaction (a post of theirs the account reacts to about a day before the invite, never less than 16 hours; see engagePostUrl on targets). At most 50 rows, shared with `targets`.
icpNameNoOptional ICP name to resolve
targetsNoPrefilled targets, at most 50 per call (CSV rows count toward the same limit). For a longer list, split it across calls under the same strategy, or use li_ingest_prospects, which takes 500.
sessionIdNoOptional saved LinkedIn session ID to use for all campaign sends
strategyIdNoStrategy to follow (or use strategyName)
sessionLabelNoOptional saved LinkedIn session label to resolve and use for campaign sends
strategyNameNoStrategy name to resolve and follow
confirmCreateYesMust be true to write the campaign to the database

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare non-read-only, non-destructive, non-idempotent, closed-world. The description goes well beyond: it explicitly says nothing is sent, describes the 50-target cap shared across inputs, the collisions report as informative-but-not-refusing, do-not-contact suppression and outright refusal when all targets are suppressed, and the engage-before-invite timing and per-account reaction caps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose and the two hard constraints (50-target cap, no sending) before routing to alternatives. It is long and the engage-before-invite mechanics are largely repeated in the engagePostUrl schema description, which is the only real redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite no output schema, the description explains returns (result.collisions, result.suppressed with reasons) and covers the key edge cases an agent must handle: rejection on missing step text, suppression, the target cap, and engage-before-invite failure modes. Nothing material is left for the caller to guess.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds cross-parameter semantics the schema cannot: every target needs step text for each post-acceptance strategy step, rows missing step text are rejected, profileUrl is required with connectionNote optional, and the JSON/CSV targets share the row limit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Create a two-stage campaign from prefilled targets under a sending strategy') and immediately scopes what it is not: 'Does not send anything.' The empty-campaign behavior and the three input formats (JSON, CSV, both) let an agent distinguish it from li_ingest_prospects, li_check_targets, and li_move_contact without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Names alternatives and the exact conditions selecting them: split across calls under the same strategy or use li_ingest_prospects (>50), no need for li_check_targets before this, and li_react_to_post for immediate reactions. It also explains when to create an empty campaign to fill later via li_ingest_prospects or li_move_contact.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_create_icpCreate ICPAInspect

Create an ICP (ideal customer profile) label to attach to campaigns, so li_outreach_analytics(groupBy="icp") can compare audiences. Only a label: it targets, filters and sends nothing. Check li_list_icps first — names are unique per team and a clash is refused. Dry-run unless confirmCreate=true; the confirmed call returns the new ICP with its id. It belongs to YOUR team (the reply's owner says whose). An operator credential with no team must name the account whose team owns it (sessionLabel) or pass shared:true; it is never published to every team by default.

NameRequiredDescriptionDefault
nameYesLabel name, unique within your team (check li_list_icps first)
sharedNoOperator only: true publishes it as a shared template every team sees. Never the default.
sessionIdNoOperator only: the account whose TEAM owns it. Members' creates are their team's.
descriptionNoOptional note on who this audience is
sessionLabelNoOperator only: the account whose TEAM owns it. Members' creates are their team's.
confirmCreateYesMust be true to write the ICP to the database

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial behavior beyond the annotations: dry-run-by-default, the confirmCreate gate, that the confirmed call returns the new ICP with its `id`, team ownership of the created object, and the operator-credential edge case (must supply sessionLabel or shared:true, never published to every team by default). These are exactly the non-obvious traits an agent needs and none are derivable from the readOnlyHint/destructiveHint flags.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads purpose and the "only a label" scope, then proceeds to warnings and edge cases. It is dense and slightly long, but each sentence carries a distinct constraint (uniqueness, dry-run, ownership, operator handling), so little is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no output schema, the description supplies the missing return information ("returns the new ICP with its `id`"), the safety/confirmation model, and the credential-dependent ownership behavior. Together with the annotations, an agent has everything needed to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds cross-parameter logic the schema states only piecemeal: it ties sessionLabel and shared:true together as the alternatives an operator with no team must choose between, and reinforces the uniqueness constraint on `name` and the write-gate on confirmCreate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("Create an ICP (ideal customer profile) label") and immediately explains its downstream purpose (attaching to campaigns so li_outreach_analytics(groupBy="icp") can compare audiences). The clarification "Only a label: it targets, filters and sends nothing" sharply distinguishes it from campaign/outreach siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit preconditions and alternatives: "Check li_list_icps first — names are unique per team and a clash is refused" and "Dry-run unless confirmCreate=true." It names the sibling to consult and the exact gate that switches the tool from dry-run to a real write.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_create_postCreate Feed PostA
Destructive
Inspect

Publish ONE post (text + optional image) to the sending account's own LinkedIn feed NOW, through the worker browser, and wait for the result (result.postUrl on success). Dry-run unless confirmPost=true: the preview shows exactly what would be posted. The exact text is supplied by the caller — this system never generates content at send time. Publishing is public and nothing here can delete the post afterwards. Use li_schedule_post instead when a person chose a future time (it stays cancellable in li_scheduled_posts). Suited to a content routine whose posting cadence the user approved.

NameRequiredDescriptionDefault
textYesExact post text to publish (max 3000 chars — LinkedIn limit)
imageUrlNoPublic http(s) image URL to attach; downloaded server-side
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
confirmPostYesMust be true to publish; false returns a dry-run preview
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag destructive/openWorld/non-idempotent, but the description goes further: publishing is public, the post cannot be deleted afterwards, the call waits for a result and returns result.postUrl, and content is never generated at send time. These are exactly the behavioral traits an agent needs to reason about an irreversible write.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the action and its scope, and every clause carries distinct information (dry-run gate, source of text, irreversibility, alternative tool). It is dense and slightly run-on, but no sentence is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 5-parameter write tool with no output schema, the description covers the return contract (result.postUrl on success), the safety gate, the irreversibility, and the sibling alternative. Nothing an agent needs to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaning beyond the schema: text must be caller-supplied verbatim because the system never generates content at send time, and confirmPost's false branch is framed as a preview. sessionId/sessionLabel guidance is only echoed from the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Publish ONE post ... to the sending account's own LinkedIn feed NOW'), including scope (own feed, single post, text + optional image). It explicitly distinguishes itself from li_schedule_post, so an agent can route without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit alternative and the selecting condition: 'Use li_schedule_post instead when a person chose a future time (it stays cancellable in li_scheduled_posts).' It also states the operational gate (confirmPost=true to publish, otherwise dry-run preview).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_create_strategyCreate Sending StrategyAInspect

Create a reusable sequence template: which post-acceptance steps to send, the delay in hours before each, and connectionWaitHours (how long to wait after the invite is accepted before the first message). A sequence may hold AT MOST 4 messages, chosen from the five kinds (first, followup_1..3, closer) — a fifth message is refused. Check li_list_strategies first: an existing template may fit, and names are unique per team. Dry-run unless confirmCreate=true; the preview returns the validated steps and any errors, the confirmed call the new strategy with its id for li_create_campaign / li_ingest_prospects. Steps are snapshotted onto each prospect at import, so fixing copy already queued is li_bulk_edit_steps or li_edit_step, not a new strategy. It belongs to YOUR team (the reply's owner says whose). An operator credential with no team must name the account whose team owns it (sessionLabel) or pass shared:true; it is never published to every team by default.

NameRequiredDescriptionDefault
nameYesTemplate name, unique within your team (check li_list_strategies first)
stepsYesOrdered steps, at most 4; must start with {kind:"first", delayHours:0}
sharedNoOperator only: true publishes it as a shared template every team sees. Never the default.
sessionIdNoOperator only: the account whose TEAM owns it. Members' creates are their team's.
descriptionNoOptional note on when to use this template
sessionLabelNoOperator only: the account whose TEAM owns it. Members' creates are their team's.
confirmCreateYesMust be true to write the strategy to the database
connectionWaitHoursNoHours to wait after a connection is ACCEPTED before sending the first message (default 24)

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare the write/non-destructive/non-idempotent/open-world profile, and the description adds substantial context beyond that: a hard cap of 4 messages with a fifth refused, dry-run preview semantics vs confirmed write, snapshotting onto prospects at import, team ownership rules, and the operator-only sessionLabel/shared behavior with the guarantee it is never published by default.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single dense block that is front-loaded with the core action, then constraints, then routing, then ownership. Every sentence carries distinct information, though the run-on length makes it slightly harder to scan than an ideal definition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 8-parameter creation tool with no output schema, the description covers return shapes (preview returns validated steps and errors; the confirmed call returns the new strategy with its `id`), downstream consumers, ownership defaults, and failure modes — nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds genuine meaning: the at-most-4 message cap, the requirement to start with 'first', connectionWaitHours semantics (wait after acceptance before the first message), and what confirmCreate actually gates (dry-run vs real write). It stops short of documenting every operator-only field, so 4 rather than 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence gives a precise verb+resource ('Create a reusable sequence template') and immediately enumerates its constituents: steps, delays, and connectionWaitHours. It distinguishes itself from li_create_campaign, li_edit_step, and li_bulk_edit_steps by naming each and the condition that routes to it.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use: check li_list_strategies first for an existing fit, dry-run unless confirmCreate=true, and fix already-queued copy via li_bulk_edit_steps/li_edit_step rather than creating a new strategy. Both alternatives and exclusions are named, leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_daily_digestToday's Report For An AccountA
Read-only
Inspect

One account's end-of-day picture in a single call: invites sent today and this week, acceptances, messages sent, replies in, how many prospects are waiting for a first message, how many replies are waiting for an answer, the pending-invite backlog on LinkedIn, cleanup spent today, failures, and any campaign that has just finished. Whether the account is BLOCKED leads the report, because a number is meaningless if the account cannot act. DB- and Redis-backed, so it costs no browser time and is safe to call while campaigns are running. Defaults to your default account; call once per account for several. The same report reaches the owner's Telegram each evening; this is how to ask for it now, or to get it as data. For what is running right now use li_account_activity; for rates over time, li_outreach_analytics.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
sessionLabelNoWhich account to report on

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover safety (readOnlyHint=true, destructiveHint=false, openWorldHint=false), and the description adds genuinely useful context beyond them: it is DB- and Redis-backed, costs no browser time, is safe to call while campaigns are running, and leads with BLOCKED status. It does not, however, describe the response shape or any rate/recency caveats.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose and the BLOCKED-status rationale early; every clause maps to a returned metric or a usage constraint. The metric enumeration is long but earns its place given there is no output schema, though the single run-on sentence is denser than ideal.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by listing the report's contents in detail, plus notes on data source (DB/Redis, no browser), safety during running campaigns, the parallel Telegram delivery, and two sibling alternatives. An agent has everything needed to call it and interpret the result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% (both params documented, including the sessionId/sessionLabel relationship), so the schema carries the load. The description adds only a marginal note about defaulting to the default account and calling once per account, which is why the baseline 3 applies rather than higher.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope ('One account's end-of-day picture in a single call') and enumerates the exact metrics returned: invites sent, acceptances, messages, replies, backlogs, cleanup spend, failures, finished campaigns. It also explicitly distinguishes itself from li_account_activity and li_outreach_analytics.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing: 'For what is running right now use li_account_activity; for rates over time, li_outreach_analytics.' Also states the multi-account pattern ('call once per account for several') and that it defaults to the default account, so the agent knows both when and how to invoke it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_detect_account_tierDetect an Account Tier NowAInspect

Read one account's LinkedIn tier now — verified / premium / new-unverified — and store what it establishes. The tier sets that account's invitation allowance AND whether its connection notes are delivered at all; it is re-detected automatically once a day, and the stored value is in li_list_accounts — call this only when you cannot wait for that tick. There is no manual tier setter by design. facts is what LinkedIn actually said, and any field may be null meaning THE PAGE DID NOT SAY, which is not the same as no. wrote is what was stored; wrote: null means the read established nothing and the account was left alone. Opens a browser session, so expect it to queue; a wait timeout means it is still running, not that it failed.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
sessionLabelNoSending account label

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only give readOnlyHint=false, openWorldHint=true, destructiveHint=false; the description goes well beyond by disclosing the side effect (stores what it establishes), the queueing/browser-session behavior, and the critical 'a wait timeout means it is still running, not that it failed.' It also disambiguates nulls in `facts` (page did not say) and `wrote: null` (nothing established, account untouched).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and effect, then timing, then return-value semantics. Every clause earns its place, though the first sentence is long and dense enough to slow parsing slightly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still explains the return shape (`facts` vs `wrote` and their null meanings), the async execution model, and the alternative data source. Nothing an agent needs to call or interpret this tool is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so sessionId and sessionLabel are already documented (including the 'never guessed' constraint). The description adds no parameter-level syntax or format detail beyond referencing li_list_accounts as the id source, so the baseline of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Read one account's LinkedIn tier now') and enumerates the possible outcomes (verified / premium / new-unverified). It also explains the consequence of the value (invitation allowance and whether notes are delivered), and names li_list_accounts as where the stored value lives, which separates it from the read-only siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes the agent: the tier is re-detected daily and the stored value is in li_list_accounts, so 'call this only when you cannot wait for that tick.' It also pre-empts a search for a write alternative with 'There is no manual tier setter by design.'

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_due_messagesMessages Due To SendA
Read-only
Inspect

List contacts whose next message step is DUE now (scheduledAt ≤ now, still pending) — usually waiting for the account's sending hours, a hold, or a free browser. Each row has the profileUrl, account, step kind, and the exact prefilled text. The worker auto-sends these; a routine can also pull them to bulk-send. DB-backed and instant; up to limit rows (default 100), and truncated says when there are more. Everything with a FUTURE send time is li_scheduled_steps; accepted people with no opener yet are li_accepted_pending_message.

NameRequiredDescriptionDefault
limitNoMost rows to return (default 100); `truncated` says when there are more
sessionIdNoLimit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account.
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account.

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, destructiveHint=false and openWorldHint=false, so safety is covered. The description adds genuinely new context: it is DB-backed and instant, rows are capped by limit with a truncation signal, and each row carries prefilled send text. It does not say what happens to rows the worker has already claimed, which is the one remaining behavioral gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the definition of 'due', then row contents, then operational notes, then sibling routing. Five dense sentences with no filler; each sentence adds a distinct fact.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description compensates by naming the row fields (profileUrl, account, step kind, prefilled text) and the truncation flag. Combined with the sibling disambiguation, an agent has everything needed to call this correctly and interpret a response.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description goes slightly beyond by restating the limit/truncated contract and by signaling that account scoping (sessionId or sessionLabel) is needed when multiple accounts exist. The sessionId/sessionLabel disambiguation is largely carried by the schema text, so this lands just above baseline rather than at full credit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource with precise scope: contacts whose next message step is DUE now, defined mechanically as scheduledAt ≤ now and still pending. It further names the two nearest siblings (li_scheduled_steps and li_accepted_pending_message) and the boundary condition that distinguishes each, so an agent can route without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly frames both consumers ('the worker auto-sends these; a routine can also pull them to bulk-send') and draws hard exclusions: future send times belong to li_scheduled_steps, accepted people with no opener belong to li_accepted_pending_message. When-to-use and when-not-to-use are both stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_edit_campaignRename A Campaign, Change Its ICP, Or Make It PriorityA
Idempotent
Inspect

Change a running campaign's NAME, its ICP label, or its invite PRIORITY — the same edit as on the dashboard, in our database only.

PRIORITY (priority: true) sends this campaign's invites before every non-priority campaign on the same LinkedIn account: the others wait while it has anyone it may invite now, and resume within about an hour once it runs out. Several priority campaigns on one account take turns. It only reorders that account's invites — caps, sending hours and pacing are unchanged, and messages/follow-ups are unaffected. Set it only when the user asks for a campaign to go first; priority: false puts it back in turn.

⚠️ A RENAME CHANGES WHAT A BY-NAME IMPORT FINDS. li_ingest_prospects matches a managed campaign on its EXACT name, so once this is renamed an import still saying the old name will CREATE A SECOND CAMPAIGN rather than adding to this one. The reply says so, and names the new name. Pass campaignId to li_ingest_prospects instead and the name stops mattering.

TWO THINGS IT REFUSES, in words rather than silently: • THE STRATEGY, once the campaign holds anybody. Every prospect carries their OWN copy of the message text and delays, taken when they were imported — so repointing the strategy would change nothing for the people already in it and silently change every future import. That is two behaviours under one word, and a success here would read as "everyone's cadence is fixed". To change copy that is already queued, that is li_bulk_edit_steps (one sentence across the whole campaign) or li_edit_step (per person). On an EMPTY campaign nothing is snapshotted yet, so the strategy IS changeable and the reply says what it applies to. EXCEPTION: while every member is still untouched (no invite sent, no step sent or scheduled), resnapshot=true changes it AND re-copies everyone's step kinds and delays from the new strategy, keeping their own text; a person with no text for a new step kind is named unless stepTexts supplies it. dryRun previews. • THE SENDING ACCOUNT, always. A campaign is bound to the LinkedIn identity that is mid-conversation with its prospects; moving it would have a stranger send the next message. Create a campaign on the other account instead, or move people with li_move_contact.

Pass icpId: null to clear the ICP. Omitted fields are left exactly as they are — a call that renames does not clear the ICP as a side effect. Get the id from li_list_campaigns.

NameRequiredDescriptionDefault
nameNoA new name, up to 200 characters. Trimmed, because a by-name import trims what it is given — storing "Foo " would make an ingest for it quietly create a second campaign.
icpIdNoA new ICP from li_list_icps, or null to clear it. OMITTED leaves it alone — null and omitted are not the same thing here.
dryRunNoWith resnapshot: preview who would be re-copied, write nothing.
priorityNotrue = send this campaign's invites before every non-priority campaign on its LinkedIn account (they wait while it has anyone to invite); false = take turns again. Only when the user asks. It reorders invites only: it NEVER raises a daily or weekly cap and never changes sending hours or messages. Omitted leaves it alone.
sessionIdNoAccepted only so moving a campaign between accounts is refused in words.
stepTextsNoPer-person text for step kinds a person has no text for under the new strategy (or to replace theirs). Implies resnapshot. Text is never invented: a person missing text for a new step is named and nothing is written.
campaignIdYesFrom li_list_campaigns
resnapshotNoRe-copy every member's step kinds and delays from strategyId (or, without one, from the campaign's own strategy), keeping each person's own text. Only while EVERY member is untouched: invite not sent, no step sent or scheduled, nothing launched for them. Otherwise refused, naming who. Writes nothing on refusal.
strategyIdNoA new strategy. On an EMPTY campaign it simply applies. On one holding people it is refused unless resnapshot=true.
sessionLabelNoAccepted only so moving a campaign between accounts is refused in words.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only give the safety profile (readOnly=false, destructive=false, idempotent=true); the description adds far more: two hard refusals with reasoning, the rename→by-name-import side effect that creates duplicate campaigns, priority's in-place invite reordering without touching caps, and dryRun/resnapshot semantics. This is exactly the behavioral context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Primary purpose and priority semantics are front-loaded, and the warnings/refusals are grouped with bullet-like structure. It is long and dense, but nearly every sentence carries non-obvious operational detail; only slight trimming seems possible.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a multi-field mutation tool with no output schema and 10 parameters, the description covers refusals, side effects, preview behavior, resnapshot preconditions, and where to get the id. Nothing an agent needs to call it safely appears missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds real meaning: icpId null-vs-omitted distinction, priority's precise effect, and that sessionId/sessionLabel are accepted only so account-move attempts can be refused in words. It goes beyond restating the schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ("change a running campaign's NAME, its ICP label, or its invite PRIORITY") and immediately distinguishes itself from near-siblings li_bulk_edit_steps, li_edit_step, li_create_campaign and li_move_contact. An agent can identify the scope without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to set priority ("only when the user asks for a campaign to go first"), when not, and names the alternative tools for cases it refuses (li_bulk_edit_steps, li_edit_step, li_move_contact, and li_ingest_prospects with campaignId). The empty-vs-populated campaign branching is spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_edit_stepRewrite A Queued Message Before It SendsA
Destructive
Inspect

Change the text of message steps that have NOT gone out yet — the fix for copy that is already queued in live campaigns and should not go out as written.

Nothing else can do this. A retired line sitting in a hundred prospects' Day-3 message could be FOUND (li_campaign_contacts gives every step its id, position and text for a whole campaign; li_contact_dossier does the same one person at a time) and not changed: the only other route was re-importing each prospect, which requires resupplying all four of their messages to correct one sentence. So it takes an ARRAY — the reason to reach for this is that the same wrong line is in many people's queue. For the SAME sentence across a whole campaign, li_bulk_edit_steps does it in one call with a dry run first — reach for this one when the corrections differ per person.

Three things it REFUSES, each per row, with the rest of the batch still applied: • a step that is not pending — it has already been sent, and the stored text is the record of what that person actually received; • a step whose sequence is replied / handed_off / completed / failed — it will never be sent, so editing it would change nothing that reaches anybody; • a step on a campaign you cannot see — reported as 'Not found', the same as one that does not exist.

This does NOT reschedule anything. The step keeps its place and its time; only the words change. To stop it going out at all, use li_mark_stopped or li_mark_outcome.

NameRequiredDescriptionDefault
editsYesThe steps to rewrite. Each is applied and reported on its own.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare destructiveHint=true and readOnlyHint=false, and the description adds substantial behavioral detail beyond them: per-row refusal behavior, which step states are editable, that non-visible campaigns are reported as not found, and that scheduling is unchanged.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well structured, front-loading the core action before caveats and alternatives. The bullets make refusal behavior easy to scan, though a few sentences are more illustrative than strictly necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive batch-edit tool with no output schema, the description covers the critical operational details: partial application, per-row reporting, refusal cases, and that only text changes while scheduling stays intact.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the parameter definitions are already documented. The description still adds useful meaning by explaining why the tool takes an array and that each edit is applied and reported on independently, plus reinforcing where stepId comes from.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a precise verb and resource: changing the text of message steps that have not yet sent. It distinguishes this from sibling tools such as li_bulk_edit_steps, li_campaign_contacts, li_contact_dossier, li_mark_stopped, and li_mark_outcome.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance: reach for this when corrections differ per person, and use li_bulk_edit_steps when the same sentence must change across a whole campaign. It also names alternatives for finding steps and for stopping sends.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_enrich_companyResearch a CompanyAInspect

Fetch what the web says about a company — its own site or the top search results — returned as raw page text (pages) for YOU to reason over. Use it for the company's own words and recent news; for people use li_enrich_profiles, which already returns each person's employer from its LinkedIn company page at no extra lookup. This tool deliberately interprets nothing: deciding what the company does, its sector, and which recent development makes a good opener is your job, and every claim you make from it should trace to one of the returned pages. Costs ONE enrichment lookup from the owner's shared pool; a company researched in the last 30 days is FREE from a shared cache (cached), and the reply states creditsUsed and remaining. Pass website when known — fetching the real site beats searching — and it also fills a blank company domain on your contacts. Never invent a development that is not in the pages: a fabricated detail is worse than a generic opener.

NameRequiredDescriptionDefault
companyYesCompany name, e.g. "Acme Ltd"
refreshNoIgnore the cache and re-fetch. Costs a lookup.
websiteNoThe company site URL, when known — fetched directly instead of searched
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (which only flag openWorldHint and readOnlyHint=false): it discloses the cost model (ONE lookup), the 30-day free cache with a `cached` flag, the response accounting fields (`creditsUsed`, `remaining`), and a real side effect — filling a blank company domain on contacts, which explains why readOnlyHint is false. This is unusually rich behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and output shape are front-loaded in the first sentence, and each subsequent sentence carries distinct information (routing, cost, cache, parameter advice). It is somewhat long and drifts into editorial coaching ('Never invent a development that is not in the pages'), which is useful but slightly beyond tool mechanics.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description fully carries the burden: it names the returned field (`pages`), the cache indicator, and the credit accounting. Cost, cache, side effects, and the interpretation-vs-fetch division of labor are all covered, so nothing needed to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds genuine meaning: `website` beats searching and also backfills a contact's domain, and `refresh` is framed in terms of cache and cost. It stops short of explaining the sessionId/sessionLabel selection nuance, but the schema covers those.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Fetch what the web says about a company') and names the exact output shape ('raw page text (`pages`)'), plus explicitly distinguishes itself from the sibling li_enrich_profiles. An agent can identify this tool and its scope without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use ('the company's own words and recent news'), an explicit alternative for a different need ('for people use li_enrich_profiles'), and a conditional recommendation ('Pass `website` when known — fetching the real site beats searching'). Routing is fully resolved.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_enrich_profilesLook Up LinkedIn ProfilesAInspect

Look up who people are — name, headline, company, role, location — WITHOUT opening a browser at all. This reads a third-party data provider, so it costs enrichment lookups but takes NO browser time and cannot affect the LinkedIn account's standing. Lookups are SHARED across every account the owner holds: each account adds 400 for its whole trial or 1,500 a month once paid, so three accounts share one pool — sharedAcrossAccounts says how many, and remaining/allowance are the pool's. Each looked-up person also carries companyPage: their CURRENT employer read from its LinkedIn company page — name, website and domain, industry, size band, HQ, founding year, a one-line description, specialities and followers — at NO extra lookup (it comes with the profile, and a firm already read in the last 30 days is never re-fetched). Use it to judge ICP fit (industry, size, HQ) and to write the sequence (what the company does, in its own words). companyPage.status is ok/cached (facts present), not_found (the page returned nothing), failed (the read broke), skipped (not looked up) or no_company_page (the profile names no LinkedIn company page — self-employed, a typed-in employer) — never read a missing block as 'no company'; companyPages summarises the batch. Anyone already looked up in the last 30 days is returned FREE from a shared cache and costs nothing; pass refresh=true to force a re-fetch. Each profile comes back COMPACT by default — the flat fields plus what actually helps judge a person (about, audience size, open-to-work/hiring, account quality, current job, a short work and education history). The provider's own record is many times larger and is mostly people nobody asked about, followed companies and images, so it is never returned. For a LARGE batch, narrow it further with fields — e.g. fields: ['about','audience','currentRole'] — which keeps every person's identity and drops the rest; measured on real profiles, those three sections are about a FIFTH of the compact record and an empty fields list is about a TWELFTH, and the reply carries the payload twice — so a 46-profile batch that flooded a caller's context becomes something they can read. The reply then says which sections it carried and which it left out, so a trimmed record can never read as a whole one. Every row carries who the person is: publicProfileUrl (their public linkedin.com/in/ URL, the one to import; null when the provider named none), memberId (ACoAA…) and searchId (ACwAA…, when that is what you asked with). profileUrl stays the address you asked with, so rows pair with your list. Per-URL outcomes: 'ok' fetched, 'cached' free, 'not_found' means the provider looked and found nothing (a deleted, renamed or restricted profile — NOT an error), 'failed' means the lookup itself broke, 'skipped_no_credits' means the allowance ran out before reaching it. One caveat on not_found: if unmatched is above zero the provider DID return that many profiles which no requested URL could be tied to, so some of the not-found rows are a matching bug worth reporting rather than a fact about those people. Always relay message: it says what was charged and what is left. The reply also carries companies: the batch grouped by employer, on the LinkedIn company page where the provider gave one and a folded name otherwise, with counts for everyone it could NOT place. Relay companies.summary — a batch is usually far fewer firms than people, and that changes how it should be sent. It says nothing about your pipeline; li_check_targets with groupByCompany answers which of those firms you already have a contact at. Pass estimateOnly=true FIRST for a free preview of a big list — how many are already known, how many would actually be looked up, how many lookups that charges and what is left afterwards. It looks nothing up, charges nothing, and answers for a list longer than one real call takes, telling you how many calls to split it into. Use this before writing outreach, and before deciding whether to accept an inbound invitation. INPUTS: the only input passed to the data provider is the list of profile URLs (each person's employer page is then read from what comes back); the other inputs only preview the cost (estimateOnly), skip the cache (refresh), trim the reply (fields) or, when you have several LinkedIn accounts, say whose allowance pays (sessionId / sessionLabel).

NameRequiredDescriptionDefault
fieldsNoReturn only these parts of each profile, so a large batch does not flood the reply. The identity — profileUrl, publicProfileUrl, memberId, searchId, status, name, headline, company, role, location, fetchedAt, error — comes back on EVERY profile and never needs asking for. The sections you can add are: about, audience, signals, currentRole, experience, education, skills (audience is follower and connection counts; signals is open-to-work, hiring, premium, influencer, verified), plus companyPage for each person's employer block (it rides on the default view, and on this trimmed one only when named). Pass an empty array for identity only. An unknown name is refused naming the accepted set, and nothing is looked up or charged. Costs exactly the same as the full record — it only changes what is sent back.
refreshNoIgnore the cache and re-fetch, for when a cached profile is known to be out of date. Costs a lookup for every URL.
sessionIdNoOnly when you have more than one LinkedIn account: which one pays for the lookup (its plan and allowance gate the run). Account id from li_list_accounts. Never sent to the data provider.
profileUrlsYesThe LinkedIn profile URLs of the people to look up — the only input passed to the data provider. One real lookup takes at most 100 and a longer list is refused rather than silently truncated; estimateOnly answers for up to 500 and says how many calls to split it into.
estimateOnlyNoPreview instead of looking anything up. Reports how many are already known (free), how many would be fetched, how many lookups that charges and what is left. Charges nothing and reserves nothing.
sessionLabelNoSame as sessionId, by account label, LinkedIn name, profile slug or member id (li_list_accounts). Only when you have more than one account. Never sent to the data provider.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds enormous context beyond the annotations: credit cost and shared-pool semantics across accounts, 30-day shared cache and refresh override, per-URL outcome codes with the not_found/unmatched caveat, companyPage status vocabulary, and the requirement to relay `message`. With readOnlyHint=false and destructiveHint=false already set, this is far more than the annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the cost/no-browser distinction, but the body is an unusually dense wall of prose covering caching, statuses, trimming fractions, companyPage fields and batch grouping. Most sentences carry unique information, but the volume is heavy enough that scanning for a single fact is hard.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a no-output-schema tool with 6 params and substantial complexity, the description covers returns in detail: compact-by-default shape, the never-returned raw provider record, per-URL statuses, the `companies` grouping block, and the payload-duplication note. Nothing an agent needs in order to call it and interpret results is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds real meaning: fields trimming with concrete examples and measured size fractions, the fact trimming costs the same as the full record, empty-array identity-only behavior, and that sessionId/sessionLabel are never sent to the provider. Only refresh/estimateOnly largely restate their schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('look up who people are — name, headline, company, role, location') via a third-party provider with no browser, and explicitly contrasts the scope with li_check_targets and li_enrich_company. An agent can distinguish it from li_research_profiles and li_search_leads by the 'costs enrichment lookups / no browser time' framing.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear context ('Use this before writing outreach, and before deciding whether to accept an inbound invitation') and routes to an alternative for a related question (li_check_targets with groupByCompany). It also prescribes estimateOnly=true first for large lists. It does not explicitly say when NOT to use it versus li_research_profiles or li_search_leads, so it stops short of full when/when-not coverage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_experiment_assignSplit a List Across Experiment ArmsAInspect

Run an A/B(/n) test without dealing people out by hand. An experiment has arms (key, strategy, engage on/off); this tool assigns each prospect to an arm SERVER SIDE and imports them, staged, through the same multi-campaign import as li_ingest_prospects: one campaign per (arm, ICP, account), named " | | ". Nothing sends until those campaigns are launched (the reply's launch.campaignIds).

ASSIGNMENT is stratified by ICP and account (stratifyBy) and deterministic: a hash of the experiment and the person, then the arm with the fewest people in that stratum, so arms stay balanced to within one person per stratum across calls. It is STICKY: someone assigned before keeps their arm and campaign. requirePostForArms keeps people without a reactable engagePostUrl out of those arms; they go to controlArm and are marked forced (left out of the comparison). An engage arm keeps each person's post and reacts before the invite; other arms drop it.

COPY per arm, most specific first: the row's armCopy[arm], the row's own text, armSteps[arm], then defaultSteps ({first} is the only variable, rendered at import).

First call: experiment {name, arms, ...}; later calls: experimentId. Up to 500 prospects. Dry-run unless confirmAssign=true; everything (accounts, ICPs, strategies, campaigns, copy) is checked before anything is written. Results: li_outreach_analytics(groupBy "arm", experimentId).

NameRequiredDescriptionDefault
icpIdNoDefault ICP label id for rows naming none
icpNameNoDefault ICP label by name
armStepsNoShared text per step for ONE arm, keyed by arm; a step it does not name falls back to defaultSteps
prospectsYes1-500 people: profileUrl, their account and ICP (or the call's), text or armCopy, optional engagePostUrl
sessionIdNoDefault sending account id for rows naming none
experimentNoA NEW experiment. Sent again with the same name and arms, it is reused.
defaultStepsNoShared text per step (connectionNote, firstMessage, followUp1, followUp2, followUp3, closer) for every arm, for each step its armSteps does not name
experimentIdNoAn experiment you already have (from an earlier reply); or give `experiment`
sessionLabelNoDefault sending account for rows naming none
confirmAssignYesMust be true to write; false previews the split and the campaigns
idempotencyKeyNoSame key on a retry after a timeout returns the stored answer (24h)

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (readOnlyHint=false, destructiveHint=false, openWorldHint=true) by disclosing sticky/deterministic stratified assignment, the requirePostForArms/controlArm `forced` fallback, the copy-precedence ladder, validation performed before any write, and that nothing sends until the campaigns are launched.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long and dense, but front-loaded with the purpose and organized into labeled paragraphs (assignment, copy, first/later call) that earn their place for a tool with this much hidden behavior. Minor density cost, but little is redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no output schema, it covers most of what an agent needs: the reply's launch.campaignIds, searchIds resolution, messageStage handling, and per-account split reporting. Remaining reply-shape details (e.g. the full dry-run preview format) are only partially described.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: stratifyBy semantics (balance within each stratum), controlArm/requirePostForArms interaction, the armCopy > row text > armSteps > defaultSteps resolution order, {first} interpolation, and experiment reuse by name+arms.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ("Run an A/B(/n) test"), then names the mechanism precisely: assigns each prospect to an arm server-side and imports them through the same multi-campaign import as li_ingest_prospects. This clearly distinguishes it from the plain import sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit sequencing (first call with `experiment`, later calls with experimentId), states dry-run is the default and confirmAssign is required to write, and routes the user to li_outreach_analytics(groupBy "arm") for results. It does not explicitly contrast when to prefer this over li_ingest_prospects, but the context is otherwise clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_fetch_postsRead People's Latest LinkedIn PostsAInspect

Fetch each person's latest LinkedIn posts so you can personalise the first touch: pick ONE post and write the connection note or first message about it before importing, or choose the post to react to in the engage-before-invite step. Up to 2 posts per person, newest first, their own words only: plain reposts are left out, and a quote post is marked isRepost with a short cut of the post it shares (repostOf). Each post carries url, text (cut at 1,200 characters), postedAt, reactions and comments (null = the provider did not say, never 0), and reactable: false means the engage-before-invite step cannot use it (a group post, or another link li_ingest_prospects refuses as engagePostUrl; notReactable says why). No browser and no LinkedIn account is used; this reads a third-party provider. COST: 1 enrichment lookup per person not already read in the last 3 days, from the same shared pool as li_enrich_profiles, whether they have 2 posts or none; anyone read recently comes back free (cached: true), and refresh=true re-reads at full price. Per-person status: posts (found), no_posts (read: nothing of their own, or nothing inside sinceDays; charged; olderPosts counts what sinceDays hid), not_found (the provider says it is not a profile it can read; not charged), unreadable (the provider answered and we could not tell what it means; not charged, retry later), failed (the read broke; not charged), skipped_no_credits, skipped_plan_inactive. Never read unreadable or failed as "they do not post". Siblings: for who a person IS (headline, role, employer) use li_enrich_profiles; for what we already hold on a contact, including posts read earlier at no cost, use li_contact_dossier with fields ['posts']. Pass estimateOnly=true first for a long list: it reads nothing and charges nothing. Always relay message: it says what was charged and what is left.

NameRequiredDescriptionDefault
refreshNoIgnore posts read in the last 3 days and read again. Costs a lookup for every person.
maxPostsNoHow many of each person's latest posts to return, 1 to 2 (more is read as 2). Default 2. Costs the same either way: 1 lookup per person.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
sinceDaysNoOnly return posts from the last N days. A person whose latest posts are all older comes back no_posts with `olderPosts` saying how many were hidden. Default: no age limit.
profileUrlsYesLinkedIn profile URLs. One real read takes at most 50 people and a longer list is refused rather than cut short; estimateOnly answers for up to 500 and says how many calls to split it into.
estimateOnlyNoPreview instead of reading anything: how many were read recently (free), how many would be read, how many lookups that charges and what is left. Charges nothing and reserves nothing.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only flag readOnlyHint=false/openWorldHint=true; the description goes far beyond by disclosing the cost model (1 lookup per person, shared pool with li_enrich_profiles, free within 3 days, refresh=true at full price) and the full per-person status taxonomy with which statuses are charged. It also warns against misreading unreadable/failed as 'they do not post' and explains reactable/notReactable. No contradiction with annotations — the readOnlyHint=false is consistent with credit-charging reads.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and the actionable instruction are front-loaded, and virtually every sentence carries operational weight (cost, statuses, field semantics). It is nonetheless a very dense single block that would scan better broken into cost / output / status sections.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description compensates fully: it enumerates returned fields (url, text cut at 1,200 chars, postedAt, reactions/comments with null-means-unknown, reactable), the status enum, and the billing outcome. An agent has everything needed to call, interpret, and report cost correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so each parameter is already documented (refresh cost, maxPosts 1-2, sinceDays/olderPosts, estimateOnly, sessionId/sessionLabel). The description reinforces a few semantics — 'Up to 2 posts per person, newest first' and the olderPosts-charged interaction — but adds little the schema does not already say. Baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb+resource ('fetch each person's latest LinkedIn posts') plus the concrete downstream purpose (personalise the first touch, pick a post to react to). It also explicitly distinguishes itself from li_enrich_profiles (identity) and li_contact_dossier (held data), so an agent can route without opening sibling schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use: pick ONE post before importing, or choose the post for engage-before-invite. It names alternatives and their conditions ('for who a person IS use li_enrich_profiles', 'for what we already hold use li_contact_dossier with fields [posts]') and prescribes estimateOnly=true first for long lists.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_hold_sendingHold All Sending (operator only)AInspect

OPERATOR ONLY. The install-wide brake: hold ALL sending on EVERY account for a set time, release it early, or read it (omit both minutes and release). While held, every account's invites, sequence messages, replies, bulk sends, posts and invitation accepts/withdrawals wait — none are lost, they go out by themselves when the hold ends — and every read (inbox, acceptances, conversations) keeps running. A bulk batch already running stops before its next person and reports the rest as notAttempted. Every hold ends by itself (5 minutes to 24 hours); setting it again replaces the standing hold. It is also set AUTOMATICALLY for 12 hours after a possible misdelivery. Use it before a manual redeploy or restart, server maintenance, or when a send path looks wrong. Per-account hold is li_pause_account. The same control is on the dashboard's Activity page. While a hold stands, every tool reply carries sendingHeld.

NameRequiredDescriptionDefault
reasonNoWhy — required with `minutes`; shown on every held job
minutesNoHold for this many minutes from now (5-1440)
releaseNotrue = end the hold now

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations (readOnlyHint=false, destructiveHint=false, openWorldHint=true) establish the mutation/safety profile, and the description adds substantial context beyond them: what is held vs. what keeps running, that nothing is lost, that a running bulk batch stops before its next person and reports the rest as notAttempted, auto-expiry range, replacement semantics, and the auto-set 12-hour misdelivery hold. The only minor gap is no explicit auth/permission detail beyond 'OPERATOR ONLY'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but dense and front-loaded — the install-wide brake concept and OPERATOR ONLY warning lead, followed by effects, expiry, triggers, and the sibling pointer. Nearly every clause carries operational information, though a few parentheticals could be trimmed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a high-impact mutation tool with no output schema, the description covers what changes, what continues, side effects on running batches, expiry behavior, and even the sendingHeld reply flag. An agent has everything needed to call it correctly and predict the outcome.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds interaction semantics the schema does not: that omitting both minutes and release selects read mode, that reason is required with minutes, and that release=true ends the hold immediately. This meaningfully supplements the structured fields.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource with exact scope: an install-wide brake that holds ALL sending on EVERY account, with read/release modes. It explicitly distinguishes itself from the per-account sibling by naming li_pause_account, so an agent can tell them apart without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use triggers (before a redeploy or restart, server maintenance, or when a send path looks wrong) and names the alternative (li_pause_account for per-account). It also explains how to invoke the read mode (omit both minutes and release), leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_host_healthHost CPU / Memory (operator only)A
Read-only
Inspect

OPERATOR ONLY — refused for anyone but whoever runs this install, because these are the numbers for the machine itself rather than for any one account. It exists because "is this starvation or a bug?" cannot be answered from a job failure alone: a contended machine and a too-short timeout produce the IDENTICAL symptom, a navigation that runs out of time. If you are diagnosing an account that looks stuck or slow, call li_account_activity instead — it names what is running, what is queued, whether sending hours are open, and any cap that is holding work, all scoped to that account.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the safety profile (readOnlyHint, destructiveHint=false, openWorldHint=false), and the description adds the access-control behavior that annotations do not: it is refused for anyone but the install operator. It also explains why the data is host-scoped, which is real behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the OPERATOR ONLY constraint, which is the most important gating information. It is somewhat long for a no-parameter tool — the 'IDENTICAL symptom' narrative is a touch expansive — but each sentence still carries routing value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-argument, read-only, no-output-schema tool, the description covers access, purpose, and the alternative, so an agent can call it correctly. It does not describe what the returned numbers look like, but the title and read-only annotation make that gap minor.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Zero parameters, so the baseline is 4. There is nothing to explain and the description correctly spends no words on parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific resource and scope: host-level CPU/memory for the machine itself rather than any account. It explicitly distinguishes itself from the account-scoped sibling li_account_activity, so an agent can tell the two apart without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit precondition (operator only), an explicit use case (deciding 'starvation vs bug' when a job fails with a timeout symptom), and names the alternative (li_account_activity) with the condition that selects it. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_ignore_calendar_eventMark A Calendar Booking Not From LinkedInAInspect

Mark a calendar booking as NOT from LinkedIn — a client call, a friend, anything outside outreach — so li_match_contacts answers ignored for it on every later run instead of none. Pass eventId (the calendar's own event id: just that event) or email (the attendee's address: every booking by them, now and later), or both. Writes our database only; nothing is sent and no contact changes. Per team, like the do-not-contact list. remove: true takes the mark off again. Only on the user's say-so — never mark an ambiguous or none event yourself because it did not match.

NameRequiredDescriptionDefault
noteNoWhy, in a few words (shown back in `reason`)
emailNoThe attendee's email — ignores every booking they make
removeNotrue = take the mark off instead
eventIdNoThe calendar's own id for the event, exactly as passed to li_match_contacts

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial context beyond the annotations: it writes our database only, sends nothing, changes no contact, is scoped per team like the do-not-contact list, and is reversible via remove:true. This is consistent with readOnlyHint=false/destructiveHint=false, framing the write as safe and undoable.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but front-loaded — the core action and its downstream consequence come first. Multiple em-dash clauses and parentheticals make it slightly longer than necessary, but each sentence carries information that aids correct invocation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a reversible mutation with no output schema, the description covers the write scope, side effects (none sent, no contact change), reversibility, and the observable result (li_match_contacts returns 'ignored'). Nothing an agent needs in order to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so the baseline is 3, but the description adds real semantics: eventId marks just that event, email marks every booking by that attendee now and later, and both may be combined. The temporal/scope distinction between the two selectors goes beyond the schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource (mark a calendar booking as NOT from LinkedIn) and immediately names the downstream effect on li_match_contacts. It is clearly distinguishable from sibling mutation tools like li_mark_booked, li_mark_booking_cancelled and li_track_booking.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use ('a client call, a friend, anything outside outreach'), explicit when-not ('Only on the user's say-so — never mark an ambiguous or none event yourself'), and names the related tool whose output it changes.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_import_invitation_decisionsImport The Triage Ledger (one-time migration)A
Destructive
Inspect

One-time migration of an existing triage ledger into the memory — the JSON the routine kept in Google Drive, as-is: { "": { "account", "decision", "reason", "icp", "campaignId", "date" } }. account is matched like sessionLabel, among YOUR accounts. Each URL is resolved to the inviter's member id (our records first; the rest by a read-only LinkedIn lookup on that account, one browser session per account) — two URLs that turn out to be one person are merged, keeping the later decision. URLs that could not be resolved are imported against their slug and listed in unresolved (the next tray read re-keys them). accepted rows without an accept on record or a campaign on that account are refused, like li_mark_invitations. Always run with confirmImport=false first: it returns the full diff (create / update / unchanged / refused / merges) and writes nothing. Re-running is idempotent.

NameRequiredDescriptionDefault
ledgerYesThe ledger object, or the same as a JSON string
confirmImportYestrue writes; false returns the dry-run diff
resolveWithLinkedInNoLook up member ids we do not already hold on LinkedIn (read-only). Default true.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true and openWorldHint=true, but the description goes well beyond: merge semantics (later decision wins), refusal of accepted rows without accept/campaign, unresolved-URL handling and re-keying, LinkedIn lookups being read-only with one browser session per account. Rich disclosure of side effects and edge behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose and organized logically (format, matching, merging, refusal, dry-run, idempotency). It is a dense single block, but nearly every sentence carries operative information; only the inline JSON shape is a slight reading cost.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description steps in and describes the return shape (create/update/unchanged/refused/merges diff plus unresolved list) and the dry-run behavior. For a destructive, high-complexity migration tool, nothing an agent needs to call it safely is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds meaningful semantics: it documents the ledger's JSON shape keyed by profileUrl with fields account/decision/reason/icp/campaignId/date, and clarifies that confirmImport=false returns the full diff. This is value beyond the schema's terse parameter descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource (one-time migration of a triage ledger into the memory) and distinguishes itself from siblings by naming li_mark_invitations as the behavioral reference and contrasting with li_invitation_decisions. An agent can tell what this does without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs to run with confirmImport=false first and explains what that returns; also states that re-running is idempotent and describes the refusal rule. The when-to-use and the safe-operating procedure are both spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_ingest_prospectsIngest Prospects + Sequences (per account)A
Destructive
Inspect

Push prospects WITH their full prefilled sequence into our DB for a chosen sending account, deduped on profileUrl (re-ingesting updates rather than duplicates). campaignName SELECTS the campaign: a name that already exists on that account is added to, and a name that does not is created. The reply always says which one you got — result.campaignName, result.campaignLaunchedAt and result.campaignCreated — because a batch landing in a campaign you did not mean is otherwise invisible. Read result.warnings: the sharpest one says these prospects joined a campaign that is ALREADY LIVE, which means launch:false staged nothing. Each prospect carries connectionNote + firstMessage + followUp1..3/closer and optional metadata. startStage="connection" (default) enters the invite drip; startStage="message" (already a 1st-degree connection, e.g. an inbound invitation you accepted) NEVER sends an invite — and REQUIRES that the connection already exists. Nothing verifies it: if you are not 1st-degree when the first message comes due, the profile has no Message control, the send fails as "no message entry point", and that failure is PERMANENT — the prospect is retired having received nothing. A PENDING inbound invitation is NOT a connection, so accept it (li_accept_invitations, and check the reply says it was accepted) BEFORE staging anyone at the message stage — their first message is scheduled for connectionWaitHours (24h by default) after the campaign is launched, and with launch:false nothing is scheduled until you call li_launch_campaign. Someone already queued, invited or connected through this account's invite-only list (li_send_connection_now) is not invited twice: their invite is MOVED into this campaign with its state, the sequence attached, listed in result.adoptedFromInviteOnly. Replaces the skill's Airtable master sheet. Does not generate text — supply it.

Every confirmed ingest also returns collisions: who among these prospects had ALREADY been approached from the accounts you can see, measured just before the rows were written. A collision is NOT an error and NOTHING was refused — every prospect was imported. It is there because the cost lands later and invisibly: re-importing someone into the SAME campaign is idempotent, but importing them into a DIFFERENT campaign creates a second invite the drip attempts, and at send time LinkedIn answering "already invited" or "already connected" sends nothing while still spending a ~50s browser session the account then has to wait behind. Anything else sends a REAL second invite, charged against each account's rolling 7-day invite allowance this system enforces, set by the account's detected LinkedIn tier — 63-70 a week (5-10 a day) new or unverified, 90-100 a week (18-24 a day) established, 135-150 a week (25-30 a day) Premium, redrawn within the range each week. Our own policy, not a published LinkedIn limit; the daily band exists only to stop a burst. li_list_accounts gives each account's actual numbers. collisions.summary is one sentence safe to relay verbatim (it is folded into result.plan too), and the report is bounded by the accounts you can see and reports nothing about anything outside them (collisions.scopeNote says so), so a clean result means 'not in your pipeline', not 'nobody anywhere'. Do NOT call li_check_targets first to get it: this runs the same check, and it runs it knowing the campaign, so people already in that campaign read as an idempotent update here instead of as a clash elsewhere. That tool is for the list you have not decided to import yet.

ONE CALL, MANY CAMPAIGNS (up to 500 prospects): each prospect may name its own account (sessionLabel/sessionId), campaign (campaignId, or campaignName to find-or-create), strategy and ICP; a field it leaves out is inherited from the call. The batch is grouped by (account, campaign, strategy, ICP), each group is its own import with its own pacing sentence and collision report, and the reply's campaigns lists every campaign touched (created or existing, imported/updated/rejected) while routed gives each submitted row's campaign and outcome by position. A row naming a campaignId takes that campaign's account, strategy and ICP. suppressed and rejectedRows are lifted to the top level as one list. Everything is resolved before anything is written: an unknown account, campaign, strategy or ICP, or the same person routed to TWO places, refuses the whole call. launch:false stages every campaign it touches.

SHARED COPY: defaultSteps gives one text per field (connectionNote, firstMessage, followUp1..3, closer — the names a row's own copy uses) for every row that brings none. {first} is the only variable: the row's firstName, else the first word of name, filled in at import, so the stored text is what is sent. A row with no first name for a template using it is not imported and says why; copy reports per-step lengths and every row over a limit (a note over 300 is shortened, a message over the limit is refused).

The do-not-contact list is the one thing here that DOES refuse: a prospect on it (the team's own list or the install-wide one) is NOT imported, and comes back by name with the reason in result.suppressed (top-level suppressed when the batch was split). Relay that list — a person the user asked to reach who silently never entered the pipeline looks like a bug weeks later. Manage the list with li_suppress_profiles / li_list_suppressed / li_unsuppress_profile.

Engage-before-invite: a connection-stage prospect may carry engagePostUrl (one of their own posts, the one the note and first message mention). The sending account visits their profile and reacts to it first, and the invite follows about a day later, inside the window it goes out in (the person's own morning when the account times invites to each prospect, otherwise its sending hours; 18-30 hours with no window to aim at); about one in seven is only viewed. Reactions are capped at 15-25 a day per account (our policy, not LinkedIn's) and run in sending hours, so a large batch is engaged over days while everyone else keeps being invited. A post that is gone or not theirs releases the invite without a reaction. Message-stage prospects ignore engagePostUrl (result.warnings says so). result.engagement counts both, and the campaign contacts read shows each person's engagement.

NameRequiredDescriptionDefault
icpIdNoICP label id from li_list_icps (or use icpName); optional
launchNoDefault true — ingest immediately starts the drip (invites/first messages). Set false to STAGE the prospects for review WITHOUT sending anything; call li_launch_campaign later to start.
icpNameNoICP label by name, from li_list_icps; optional
prospectsYes1-500 prospects, each with profileUrl and the prefilled text for every strategy step (plus connectionNote for invites), or that text from defaultSteps. Each may name its own account, campaign, strategy and ICP.
sessionIdNoSending account id (or use sessionLabel)
campaignIdNoImport into THIS campaign, by id from li_list_campaigns. Authoritative: it names exactly one campaign, where a name can match two. Refused if it is not on the sending account or is completed — never quietly swapped for another campaign. Use it whenever you mean an existing campaign; use campaignName to make a new one.
strategyIdNoSequence template id from li_list_strategies (or use strategyName)
campaignNameNoSelects a campaign BY NAME on the sending account: an existing active campaign of that name is added to, and a name that does not exist yet is created. Stored and matched EXACTLY as you give it — nothing is appended, so calling again with the same name reaches the same campaign. Omit for the account's default managed campaign. Prefer campaignId when you mean a campaign that already exists. Two accounts may hold a campaign of the same name without clashing: the match is per account, and each campaign reports its own `accountLabel`.
defaultStepsNoShared copy: text per step (connectionNote, firstMessage, followUp1, followUp2, followUp3, closer) for every row that brings none of its own. {first} is the row's first name (firstName, else the first word of name), filled in at import; no other variable exists. A row with no first name for a template that uses {first} is not imported, and says why.
sessionLabelNoSending account label, e.g. "Deven" (defaults to the default account)
strategyNameNoSequence template; omit to use/create the default Day 1/3/7/14
confirmIngestYesMust be true to write; false returns a dry-run preview
idempotencyKeyNoAny unique string for THIS request (e.g. a UUID). Pass the same key when retrying after a timeout, 502 or 503: a call that already finished returns its stored answer (24h) instead of importing twice. Ignored on a dry run.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare a destructive, open-world write, and the description goes far beyond them: dedup on profileUrl, idempotency, permanent failure of message-stage sends when not 1st-degree, the exact semantics of launch:false staging, the ~50s browser session cost of a duplicate invite, the rolling 7-day invite allowance and its daily bands, and the do-not-contact refusal. This is exceptional disclosure for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core operation and its selection semantics are front-loaded, and the sectioned structure (ONE CALL MANY CAMPAIGNS, SHARED COPY, engage-before-invite) is navigable. The length is largely justified by the tool's genuine complexity, but several passages (invite-allowance tiers, engagement timing details) are more expansive than an agent needs for correct invocation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 13-parameter nested tool with 500-item batches, no output schema, and destructive behavior, the description covers the result contract explicitly (result.campaignName, result.campaignCreated, result.warnings, result.collisions, result.routed, result.suppressed, result.adoptedFromInviteOnly), including what each return field means. An agent has everything needed to invoke and interpret the call.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds cross-parameter semantics the schema alone does not convey: campaignName's find-or-create exact-match behavior versus campaignId's authoritative one-campaign semantics, startStage's divergent invite/message behavior, and engagePostUrl being ignored at message stage. Some prose overlaps the schema (e.g. {first} substitution, engagement caps) rather than adding new per-parameter meaning, keeping it at 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific verb and resource with scope: 'Push prospects WITH their full prefilled sequence into our DB for a chosen sending account, deduped on profileUrl.' It further distinguishes itself from siblings by naming li_check_targets, li_send_connection_now, li_accept_invitations and li_launch_campaign in the context of when this tool does and does not apply.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use guidance is present throughout: message-stage requires an existing 1st-degree connection and directs the agent to accept pending invitations via li_accept_invitations first; it explicitly says NOT to call li_check_targets first; and it routes suppression management to li_suppress_profiles/li_list_suppressed. Alternatives and preconditions are named concretely.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_invitation_decisionsList Inbound-Invitation Triage DecisionsA
Read-only
Inspect

Read the triage memory: who was judged, what was decided and why — for reporting ('everyone rejected as an IC this month') and to audit routing (decision=wrong_lane shows routeTo). Filter by account, decision, icp, decidedAt range, changedSinceDecision (their headline/company moved since the decision, as of the last tray read) and free text over name and reason. Newest first, paged with offset; total and truncated say how much more there is. Omitting the account lists every account you can see. Read-only, no browser.

NameRequiredDescriptionDefault
icpNoOnly decisions filed under this ICP label
limitNoDefault 50
sinceNoISO date — decided on or after
untilNoISO date — decided on or before
offsetNoRows to skip, for paging (default 0)
searchNoMatches name or reason
decisionNoOnly this decision: accepted, rejected, wrong_lane, failed or deferred
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see.
changedSinceDecisionNotrue = only people whose headline or company moved since they were judged

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, destructiveHint=false and openWorldHint=false, so safety is covered; the description reinforces with "Read-only, no browser" and adds genuinely new behavior: newest-first ordering, offset paging, and the total/truncated signals plus the changedSinceDecision "as of the last tray read" caveat.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single dense, front-loaded paragraph that leads with what the tool reads, then filters, then ordering/paging. No filler sentences; every clause carries information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With 10 optional parameters and no output schema, the definition covers filtering, default account scope, ordering, pagination, and the total/truncated return signals, so an agent has everything needed to call and interpret it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds real meaning: decision=wrong_lane surfaces routeTo, changedSinceDecision tracks headline/company drift since judgment, and omitting the account widens to every visible account. These go beyond the schema's per-parameter text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

"Read the triage memory" states a specific verb and resource and immediately enumerates its scope: who was judged, what was decided, and why. It is clearly distinguishable from the write-side sibling li_import_invitation_decisions and from generic listing tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives concrete use cases (monthly rejection reporting, auditing routing via decision=wrong_lane) so an agent knows when to reach for it. It does not explicitly name an alternative tool or state when-not to use it, which holds it below a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_job_resultCheck Queued Work — What Is Still Running, and What It DidA
Read-only
Inspect

Where your queued work has got to, and what it reported doing. This is the recovery path for a call that timed out or returned a job id: a timeout means the WAIT expired, NOT that the work failed — the job usually keeps running and finishes. Never resend on one; ask here instead.

CALL IT WITH NO jobId AFTER QUEUEING A BATCH. That lists every job of yours the queue is holding — still queued, running, and recently finished — across ALL your accounts in one call, each row carrying state and stillRunning. So six replies are one call, not six, and stillRunning: 0 is how you know the batch has landed. Naming an account narrows it; you do not need to.

Pass a jobId for one exact job, with its full result. A research jobId (research_…, from li_research_profiles) returns that run's merged rows instead, limit a page (default 50) from offset. onlyFailures=true shows the ones that finished while reporting they did NOT do their work — a send that delivered nothing 'completes' like any other job, so this is the view that catches it. A bulk send's result carries the per-recipient outcome, so after a timeout this tells you exactly who was already messaged.

WHERE IN THE QUEUE, AND WHEN. Every row carries firesAt — the moment a HELD job actually runs — and waitingBecause, the reason it was held (outside that account's sending hours, the daily invite cap, the weekly ceiling, a busy host). A row with neither is simply queued behind other work rather than deferred. On top of that, a reply carries eta for the LAST job still queued, whose finish is the batch's finish: read eta.checkAfterMs and sleep ONCE instead of polling. The eta is ABSENT rather than guessed when the queue cannot be read, the job cannot be placed, or nobody has timed that kind of work yet.

A job id we have no record of comes back as found: false, NOT as an error: records are kept about an hour, and their absence says nothing about whether the message went out. Settle that from the conversation (li_conversation_history), never by sending again.

⚠️ Jobs that dedupe on a fixed id — a manual inbox sweep, acceptance poll, invite reap or withdrawal — delete themselves the moment they finish, so they will not appear here. That is deliberate (it is what lets you trigger them again immediately). Read those from what they changed: li_replies_to_answer for a sweep, li_connections for an acceptance poll, li_withdraw_status for a withdrawal you asked for — and li_list_accounts (autoWithdraw.lastRun) for the DAILY invite reap, whose report is a different record from li_withdraw_status's.

NameRequiredDescriptionDefault
jobIdNoThe exact job to read — comes back in a timed-out tool's reply
limitNoHow many to list (default 20)
jobIdsNoExactly these jobs, up to 50 — the ids li_send_reply handed back. Every one is reported, including ones the queue no longer holds (found: false), so a batch is one call rather than one call per recipient.
offsetNoOnly with a research jobId (from li_research_profiles): the first row to return. Pass the previous reply's nextOffset.
waitMsNoWait up to this long (max 120000ms) for the jobs asked about to finish, instead of answering immediately and being asked again. Returns as soon as none are still running. Sleeping once here is cheaper and kinder than polling.
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
onlyFailuresNoOnly jobs that finished while reporting they did not do their work
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only supply readOnly/openWorld/destructive hints, but the description discloses far more: unknown ids return found:false rather than an error, records last about an hour, eta is absent rather than guessed when the queue is unreadable, and fixed-id dedupe jobs delete themselves on completion. It also explains the firesAt/waitingBecause hold semantics and that onlyFailures catches 'completed but did nothing' sends — rich operational context no annotation carries.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose and the headline usage rule are front-loaded in the first two paragraphs, and the long tail is genuinely load-bearing (dedupe caveat, eta absence, found:false). It is dense and occasionally chatty ('cheaper and kinder than polling'), so it falls short of a 5, but nothing is redundant with the schema or annotations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With 8 optional parameters and no output schema, the description carries the whole burden and does so: it names the returned fields (state, stillRunning, firesAt, waitingBecause, eta.checkAfterMs, found), explains paging via nextOffset, and covers the timeout/retry trap. An agent can act correctly without ever reading a return schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description genuinely adds meaning: it distinguishes the jobId (single job) path from the no-jobId (batch listing) path, explains that jobIds reports every id including missing ones, and clarifies that limit/offset paging applies only to a research jobId. Minor gaps remain (e.g. sessionId vs sessionLabel precedence), which keeps it short of a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource framing ('Where your queued work has got to, and what it reported doing') and immediately positions itself as the recovery path for a timed-out call or a returned job id. It is clearly separable from li_campaign_status, li_session_status, and the various check_* siblings because it is about job records, not account or campaign state.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use ('CALL IT WITH NO jobId AFTER QUEUEING A BATCH'), an explicit when-not ('Never resend on one; ask here instead'), and routes the agent to named alternatives for cases it cannot serve (li_conversation_history to settle delivery, li_replies_to_answer / li_connections / li_withdraw_status / li_list_accounts for self-deleting jobs). Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_launch_campaignLaunch LinkedIn CampaignA
Destructive
Inspect

Launch a created campaign. Two things happen: every already-connected prospect on it (startStage "message" — an inbound invitation you accepted, or anyone already 1st-degree) has their first message scheduled immediately — with NO check that the connection actually exists, so anyone whose invitation was not really accepted fails permanently at send time rather than waiting, and the first pending connection invite is enqueued. The worker drips later invites with humanized delays; the acceptance poller marks accepted invites, then post-acceptance messages auto-send sequentially with reply checks. THIS IS THE CONFIRM STEP for a batch ingested with launch:false — a campaign holding only already-connected people launches fine and returns messageStage.started, and no invite is ever sent to them.

MANY AT ONCE: pass campaignIds (up to 100) or namePrefix (+ sessionLabel to narrow it to one account) instead of campaignId. Without confirmLaunch it lists every matched campaign with its account, status, people and notStartedYet, and which would be refused; confirmed, each launches exactly as the single form, with its own ownership check and its own result line — one refusal does not stop the rest. A namePrefix launch also needs expectCount (the number the preview listed): without it the call only previews, and a different count refuses.

NameRequiredDescriptionDefault
sessionIdNoWith namePrefix only: match on this account only
campaignIdNoOne campaign id from li_list_campaigns. Or campaignIds / namePrefix.
namePrefixNoBulk: every campaign you can see whose name starts with this (case-insensitive, the same match as li_list_campaigns), at most 100; listed before anything is launched
campaignIdsNoBulk: up to 100 campaign ids, each launched as the single form would be
expectCountNoWith namePrefix only: the number of campaigns the preview listed. Required to confirm, so nothing is launched that the preview did not show.
sessionLabelNoWith namePrefix only: match on this account only
confirmLaunchYesMust be true to enqueue the first connection invite

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the destructive/openWorld/idempotent annotations: no check that a connection exists so unaccepted invites fail permanently at send, worker drips invites with humanized delays, acceptance poller then auto-sends post-acceptance messages with reply checks, and bulk mode isolates failures per campaign. These are real operational consequences an agent must know before invoking.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and the confirm-step framing, and nearly every sentence carries non-obvious information (failure modes, preview behavior, per-campaign isolation). It is long and dense with nested parentheticals and em-dash clauses, though, which slightly hurts scanability for a 7-param tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still tells the agent what comes back (preview listing account/status/people/notStartedYet/would-be-refused, and messageStage.started on a connected-only launch) and how partial refusals surface. For a tool this complex, nothing material is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, and the description adds interaction semantics the schema cannot express: campaignIds/namePrefix/sessionLabel mutual roles, the expectCount anti-drift guard, and confirmLaunch as the preview-vs-launch switch. Some of this (sessionLabel/namePrefix pairing) duplicates the schema wording, so not a full 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ("Launch a created campaign") and immediately frames itself as the confirm step for batches ingested with launch:false, which distinguishes it from li_create_campaign, li_ingest_prospects and li_pause_campaign/resume_campaign without needing the schema. It also covers the bulk form explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

States precisely when to use it ("THE CONFIRM STEP for a batch ingested with launch:false"), when a campaign with only connected prospects works, when to switch to campaignIds/namePrefix/sessionLabel, and that namePrefix requires expectCount matching the preview. Exclusions (what happens without confirmLaunch) are explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_list_accountsList Sending AccountsA
Read-only
Inspect

List the LinkedIn sending accounts (one per saved session). Each carries inviteNotes — personalized INVITE notes sent this month and whether LinkedIn says the monthly allowance is used up (when state is spent, write no connection notes until the date in guidance; invites still go out without them) — and inviteHold (new invites held after repeated identical failures). Each has a label (the name a person gave it) and a profileSlug (its own LinkedIn profile slug, linkedin.com/in/, once the tier read has seen it) — plus linkedinName and memberId (the permanent id, which never changes). sessionLabel on every tool accepts ANY of label, LinkedIn name, slug or member id; two accounts matching is refused, never guessed. Each account also carries its tier, warm-up state, status, whether it is the default account, and its autoWithdraw invite-hygiene setting plus the last cleanup run, its officeHours (enabled/start/end/timezone), its sendWindowMode (account | prospect | both) and its prospectWindow (prospect-local invite and message hours), and its booking settings (booking links, firstNudgeHours, closeAfterDays, maxNudges, cancelRecheckHours, prefillName, tool, nameParam — change them with li_set_booking_settings). Also each account's plan (the billing standing — entitled, in grace, lapsed or cancelled — with the dates and days left) and its enrichment usage (allowance, used, remaining, and the window it is measured in). Lookups are POOLED PER OWNER: enrichment is the pool the account draws from — every entitled account its owner holds adds its share (400 for the whole trial, 1,500 a month paid; thisAccountAdds), sharedAcrossAccounts says how many — so accounts of one owner show the SAME numbers and a lookup from any of them spends the same pool. A trial pool is for the WHOLE trial and never resets; a paid one is per calendar month; window.kind: mixed means paid and trial accounts together — the paid part resets next month, the trial part never does. Read enrichment.window.kind before promising anyone that credits come back next month. enrichment: null means the usage could not be read, never that nothing was spent. Use a label/id from here as sessionLabel/sessionId on the other tools to pick which account acts. This is CONFIGURATION — for what an account is doing right now (running/queued/scheduled work) call li_account_activity instead. Never exposes cookies.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly/non-destructive/closed-world, and the description adds substantial context beyond them: 'Never exposes cookies', 'enrichment: null means the usage could not be read, never that nothing was spent', pooled-per-owner credit semantics, and the ambiguity rule that a matching label/id 'is refused, never guessed'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded, but the body is a very long, densely parenthesized run-on block documenting dozens of return fields in em-dash clauses. Much of it is justified by the absence of an output schema, but the nesting makes it hard to scan and every clause is packed into one paragraph.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the full burden of explaining return values, and it does so exhaustively: inviteNotes/inviteHold behavior, plan standing, enrichment pooling and window reset rules, officeHours, sendWindowMode, prospectWindow, and booking settings. Nothing an agent needs to interpret the response is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the schema baseline is 4. The description still adds meaning about the sessionLabel/sessionId identifiers used by other tools (accepting label, LinkedIn name, slug or member id), which is useful context even though it isn't a parameter of this call.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific verb and resource with scope ('List the LinkedIn sending accounts (one per saved session)'), and the closing explicitly distinguishes it from the sibling li_account_activity ('This is CONFIGURATION — for what an account is doing right now ... call li_account_activity instead'). An agent can tell exactly what this returns versus the activity tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear downstream context ('Use a label/id from here as sessionLabel/sessionId on the other tools to pick which account acts') and names alternatives for adjacent needs (li_account_activity for live work, li_set_booking_settings for changing booking config). It lacks an explicit 'call this when...' trigger, but the routing to alternatives is present.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_list_campaignsList CampaignsA
Read-only
Inspect

Every campaign you can see, newest first — its id, name, status, sending account, strategy, ICP, contact count, priority (true = its invites go before the account's other campaigns) and state (whether it is really sending, and why not). START HERE: li_campaign_status and li_campaign_contacts both need a campaignId, and this is what hands you one.

FILTERS run over every campaign before the page is cut: status ("active", "paused", "completed"), namePrefix (case-insensitive), sessionLabel/sessionId (sending account), minContacts/maxContacts (people in it). PAGED: the reply carries totalMatching, truncated and nextOffset (pass it as offset); a filtered empty page says how many campaigns exist without the filters. fields keeps only the named columns (campaignId always). format: "csv" returns the rows as one CSV string (csv + columns) instead of campaigns. DB-backed and instant.

NameRequiredDescriptionDefault
limitNoDefault 50
fieldsNoOnly these columns per row (campaignId is always kept). Omit for every column.
formatNo"csv" = one header + rows string instead of the `campaigns` array
offsetNoSkip this many matching campaigns — pass the last reply's `nextOffset`
statusNoOnly campaigns in this status, e.g. "active"
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
namePrefixNoOnly campaigns whose name starts with this (case-insensitive)
maxContactsNoOnly campaigns holding at most this many people (0 = empty ones)
minContactsNoOnly campaigns holding at least this many people
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see.

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover safety (readOnly, non-destructive, closed-world), but the description adds real behavioral context: filters apply over the whole set before the page is cut, the reply carries totalMatching/truncated/nextOffset, an empty filtered page reports the unfiltered count, and the operation is 'DB-backed and instant'. This is well beyond annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but well organized into purpose, filters/paging, fields/format, and a closing performance note, with the most important routing advice ('START HERE') front-loaded. Every sentence carries information an agent needs; nothing is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description itself documents the return shape (campaigns rows with named columns, or csv+columns), pagination metadata, and the empty-page edge case. Combined with annotations and a fully described 10-param schema, an agent has everything needed to call and interpret this tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the description adds meaning the schema does not: filters run before pagination, nextOffset should be passed back as offset, fields always retains campaignId, and format=csv swaps the campaigns array for a csv+columns pair. Some items (case-insensitive namePrefix) merely restate the schema, keeping this just short of a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource ('Every campaign you can see, newest first') and enumerates the returned columns, so an agent knows exactly what this tool yields. It explicitly distinguishes itself from siblings by naming li_campaign_status and li_campaign_contacts as downstream consumers that need the campaignId this tool produces.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'START HERE' directive states when to use this tool relative to named alternatives, and explains that both sibling tools depend on a campaignId obtained here. It also describes the filter/pagination strategy so the agent knows how to narrow results rather than paginate blindly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_list_conversationsList Recent LinkedIn ConversationsA
Read-only
Inspect

List one account's most recent LinkedIn message threads, read live from LinkedIn in a browser session on the worker (it queues behind other browser work, so it can take a while). Each row has the other participant (participantName, participantProfileUrl, participantProfileId = their ACoAA… member id), lastActivityAt, unreadCount, the latest message text (lastMessageText), weOweReply (true when THEY spoke last), addressable (false for sponsored/InMail blasts, which carry no participant identity and cannot be replied to) and, when we track that person, ours (our status, outcome, campaign). It sends nothing and changes no contact, message or conversation record (the read only notes the account's own member id, so threads between your own accounts can be recognised).

FILTERS. onlyNeedsReply=true keeps threads where they spoke last; onlyAddressable=true drops the blasts; sinceDays and profileUrl narrow further. The walk pages by cursor until limit threads pass THOSE filters, the inbox ends, or a page ceiling is hit. With onlyNeedsReply the server then ALSO drops, and counts: threads somebody already decided about after their last message — handled, completed, outcome-marked or snoozed (alreadyDecidedCount; a newer message from them brings the thread back flagged reopened); threads with your own team's accounts (excludedInternalCount); LinkedIn itself — blasts and "The LinkedIn Team" (excludedSystemCount); and threads reviewed with li_mark_reviewed and unchanged since (alreadyReviewedCount). Those drops happen AFTER limit, so a page can hold fewer rows than asked. A lookup that fails filters NOTHING and says so (decidedLookupFailed, internalLookupFailed, reviewLookupFailed). Somebody we hold no record for is untracked, not decided, and stays listed until a mark records them (li_mark_outcome / li_mark_reply_handled / li_snooze_reply accept the participantProfileUrl and create a light record).

COVERAGE. Check reachedEnd and truncated before concluding anyone is absent: a walk that stopped early has not seen older threads. participantProfileId is the member id li_send_bulk_messages takes for its fast path. Pair with li_read_conversation for a full thread. Seen status is NOT here (it would cost one extra call per thread); stored reads carry it — see li_conversation_history (seenByThemAt).

NameRequiredDescriptionDefault
limitNoHow many threads to collect (default 20, max 200), counted after onlyNeedsReply / onlyAddressable / sinceDays / profileUrl but BEFORE the decided / internal / system / reviewed drops, so the reply can hold fewer. The inbox is walked by cursor, 20 threads per round trip, until this many match, the inbox runs out or the page ceiling is hit — a large number costs browser time. Check `reachedEnd`.
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
sinceDaysNoOnly threads active in the last N days. Check `truncated` and `reachedEnd`: the walk stops at the page ceiling or your `limit`, so a window reaching past `coversSince` is NOT answered here — raise `limit`, or name the people and use li_read_conversations_bulk.
profileUrlNoOnly the thread with this person (vanity /in/<slug> or ACoAA… URL)
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.
onlyNeedsReplyNoReturn only threads where the other person spoke last (we owe a reply)
onlyAddressableNoDrop threads that cannot be replied to — LinkedIn sponsored/InMail blasts, with or without a named sender, which still count as "they spoke last". Off by default so nothing is hidden; the response always reports how many were unaddressable.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and destructiveHint=false, yet the description adds substantial behavior: it queues behind browser work and can be slow, sends nothing and changes no record, notes the account's own member id, drops rows AFTER limit (so a page can hold fewer than requested), and reports lookup-failure flags rather than silently filtering. This is far beyond what annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is long, but well-structured with front-loaded purpose and clear FILTERS / COVERAGE sections, and the density is largely justified because there is no output schema to carry the field and count semantics. A few clauses about count flags could be tightened, but almost every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the full burden of explaining returns — participantName/participantProfileUrl/participantProfileId, lastActivityAt, unreadCount, lastMessageText, weOweReply, addressable, ours, and the alreadyDecidedCount/excludedInternalCount/excludedSystemCount/alreadyReviewedCount counters — plus coverage caveats (reachedEnd, truncated). Nothing an agent needs to call or interpret it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds genuine semantics: the interplay of onlyNeedsReply/onlyAddressable/sinceDays/profileUrl, the fact that filtering counts happen before the decided/internal/system/reviewed drops, and that participantProfileId is the member id li_send_bulk_messages consumes. It goes beyond restating the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+scope: 'List one account's most recent LinkedIn message threads, read live from LinkedIn in a browser session.' It immediately differentiates from siblings by naming li_read_conversation for a full thread and li_conversation_history for stored reads, so an agent can route without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use each filter (onlyNeedsReply keeps threads where they spoke last; onlyAddressable drops blasts) and names alternatives with their triggering conditions (li_read_conversations_bulk for named people, li_mark_outcome/li_mark_reply_handled/li_snooze_reply for untracked senders). It even warns which filters do NOT answer a query.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_list_icpsList ICPsA
Read-only
Inspect

List the ICP labels (ideal customer profiles) you can attach to campaigns, newest first: each with id, name, description, campaignCount (campaigns you can see using it), shared (an install-wide label every team sees) and editable. DB-backed, instant, unpaged. An ICP is only a label for comparing audiences: pass it as icpId/icpName to li_create_campaign or li_ingest_prospects, change a campaign's with li_edit_campaign, compare with li_outreach_analytics(groupBy="icp"). Create one with li_create_icp. Sequence templates are li_list_strategies; campaigns are li_list_campaigns.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish readOnly/openWorld=false/destructive=false, so the safety profile is covered. The description adds real behavioral context beyond them: 'DB-backed, instant, unpaged' tells the agent there is no pagination loop or eventual-consistency wait to handle, and the field list pre-empts output discovery.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose, scope, and return shape, then routing. Every clause carries information, though the long compound sentences in the second half are dense and slightly harder to scan than a bulleted alternative listing would be.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description substitutes by enumerating return fields with meaning (campaignCount = campaigns you can see, shared = install-wide). Together with the routing table for related ICP tools, an agent has everything needed to call and use this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline is 4. The description instead documents returned identifiers (id, name, description, campaignCount, shared, editable), which is the more useful semantic information for a parameterless list call.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('List the ICP labels') and immediately scopes it: labels attachable to campaigns, returned newest first. It explicitly distinguishes itself from li_list_strategies and li_list_campaigns, so an agent can route without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Goes well beyond context: it explains what an ICP *is* (a label for comparing audiences) and names the exact downstream tools/fields where the value is consumed (icpId/icpName to li_create_campaign / li_ingest_prospects, li_edit_campaign to change, li_outreach_analytics(groupBy="icp") to compare, li_create_icp to create). This is explicit when-and-how guidance with alternatives named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_list_strategiesList Sending StrategiesA
Read-only
Inspect

List the sequence templates (strategies) you can use, newest first: each with id, name, description, ordered steps (kind + delayHours), connectionWaitHours, campaignCount (campaigns you can see using it), shared (a built-in template every team sees) and editable. DB-backed, instant, unpaged. Read it before li_create_campaign or li_ingest_prospects to pick a strategyId/strategyName, or before li_create_strategy to avoid a name clash. Audience labels are li_list_icps; campaigns are li_list_campaigns.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, openWorldHint=false, and destructiveHint=false, so safety is covered. The description adds useful behavioral context beyond that: DB-backed, instant, unpaged, newest-first, and a detailed account of the returned strategy fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The first sentence front-loads the core action and return shape, while the second sentence cleanly states the routing guidance. Despite listing many fields, every included detail earns its place because there is no output schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, read-only list tool with no output schema, the description is highly complete: it explains the return fields, ordering, pagination behavior, shared/editable distinctions, and when to use it relative to sibling tools.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there are no parameter semantics to document. The empty schema and zero-parameter input are consistent with the description, which correctly implies this is an unfiltered list operation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: list the sequence templates (strategies). It also names distinguishing details like newest-first ordering and return fields, and explicitly routes the agent to sibling tools for campaigns and audience labels.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly says when to read it before li_create_campaign, li_ingest_prospects, and li_create_strategy, including the concrete purpose of picking a strategyId/strategyName or avoiding a name clash. It also names li_list_icps and li_list_campaigns as the alternatives for related data.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_list_suppressedRead The Do-Not-Contact ListA
Read-only
Inspect

Read-only: who is on the do-not-contact list this caller can see. The do-not-contact list is checked at IMPORT — a listed prospect is NOT imported and is reported by name in result.suppressed of li_ingest_prospects — and again at INVITE time, so a queued invite for someone added to the list after their import is retired with category suppressed instead of sent — and before every automated message step, and by the assistant send tools (li_send_reply, li_send_message, li_send_bulk_messages), which REFUSE a listed person. Only a message a person types themselves in LinkedIn or on the dashboard is not blocked. It is per TEAM: an entry made with a personal token applies to every LinkedIn account that team runs and to nobody else; entries on the INSTALL-WIDE list (made by the operator, i.e. the shared secret) apply to every team. With a personal token the rows are the team's own list PLUS the install-wide one (it applies to them, so hiding it would make a refusal unexplainable); the operator sees every list. Each row carries scope ('install' applies to everybody, 'owner' to one team) and removable — whether THIS caller may remove it with li_unsuppress_profile. truncated: true means the list is longer than limit and the rows shown are not all of it.

NameRequiredDescriptionDefault
limitNoMax rows to return (default 500, max 1000). A longer list reports truncated: true rather than silently trimming.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare it as a non-destructive read; the description goes far beyond that, disclosing the enforcement points (import, invite, automated messages, send tools that REFUSE listed people), the per-team vs install-wide scoping model, and the visibility rules for personal vs operator tokens. It also explains the returned fields scope, removable, and truncated.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core purpose ('Read-only: who is on the do-not-contact list') before the dense behavioral detail. It is long and information-heavy, but each clause explains a distinct enforcement or scoping fact that aids correct interpretation.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description compensates fully by describing the row fields (scope, removable), the truncated flag, and the referenced result.suppressed shape. An agent has everything needed to interpret the response and understand caller-specific visibility.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the single limit parameter is already documented with default and max. The description adds marginal meaning by tying truncation to limit, but does not go beyond what the schema states, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource immediately: 'Read-only: who is on the do-not-contact list this caller can see.' It scopes the resource ('this caller can see') and implicitly distinguishes itself from the mutating siblings li_suppress_profiles and li_unsuppress_profile by being the read side.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explains the operational role of the list (checked at import, invite, message steps; enforced by send tools) and names li_unsuppress_profile and li_ingest_prospects as related operations. It gives strong context for why/when the list matters but never states an explicit 'call this when you need to audit X' directive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_bookedRecord A Booked CallA
DestructiveIdempotent
Inspect

Record that a call is ON THE CALENDAR for this person: booking status booked, outcome meeting_booked (never won), remaining steps cancelled, bookedAt stamped. Valid from any status — someone may book straight off a sequence link without ever replying.

IDEMPOTENT ON calendarEventId: the same event again changes nothing (one timeline entry); the same event with a new meetingStart is a RESCHEDULE and only moves the time. The same event on a different person is refused. Identify the person by profileUrl (from li_match_contacts) or contactId.

NameRequiredDescriptionDefault
noteNoOptional note for the contact timeline
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
sessionIdNoThe account this conversation is on. Only used when the person is in NO campaign: then a light record is created on this account so the mark has something to act on. Needed only when you have more than one account.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
meetingStartYesWhen the call starts, ISO 8601
sessionLabelNoSame, by account label
calendarEventIdYesThe calendar's own id for the event
conversationUrnNoThe thread, straight from li_list_conversations (single-person form). Recorded on the conversation so the live inbox recognises it as decided. On its own it finds the person only when their thread is already stored — pass profileUrl (the participantProfileId / ACoAA… URL) too.
participantNameNoTheir name from li_list_conversations — kept on a newly recorded contact.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations give destructiveHint=true and idempotentHint=true, but the description goes well beyond them: same calendarEventId is a no-op, same event with a new meetingStart is a RESCHEDULE that only moves the time, and same event on a different person is refused. It also names the destructive side effects (cancelling remaining steps) that justify the destructive hint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the action and its effects, then idempotency semantics. Dense but nearly every sentence carries non-obvious information; a little tightening of the long sentences would reach the top mark.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 11-parameter mutation tool with no output schema, the description covers identity resolution, idempotency/reschedule behavior, and side effects. Only minor gaps remain, such as failure modes when the contact cannot be resolved.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: calendarEventId is the idempotency key, meetingStart drives reschedule behavior, campaignId is required when a contact sits in multiple campaigns, and sessionId only applies when the person is in no campaign. These disambiguate when each optional identity field should be passed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource (record a booked call) and enumerates the exact state changes: booking status `booked`, outcome `meeting_booked`, remaining steps cancelled, `bookedAt` stamped. This clearly separates it from siblings like li_mark_outcome, li_mark_stopped, li_cancel_steps and li_mark_booking_cancelled.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

"Valid from any status — someone may book straight off a sequence link without ever replying" tells the agent the trigger condition well. It also offers routes to identify the person (profileUrl from li_match_contacts, or contactId). It does not explicitly name when to prefer a sibling (e.g. li_track_booking or li_mark_booking_cancelled), so it stops short of the top mark.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_booking_cancelledRecord That A Booked Call Was CancelledA
Idempotent
Inspect

The calendar event is gone or cancelled. Finds the booking by calendarEventId on the accounts you can see and puts it back to agreed (outcome interested) with the next check after the account's cancelRecheckHours (24h by default). The nudge count is NOT reset: someone who was already nudged comes back with nextAction close, never a second nudge. Cancelling twice changes nothing. A MOVED event is not a cancellation — that is li_mark_booked with the same calendarEventId and the new meetingStart.

NameRequiredDescriptionDefault
noteNoOptional note for the contact timeline
calendarEventIdYesThe calendar event id recorded with li_mark_booked

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate idempotency and non-destructiveness, but the description adds substantial state semantics: the booking returns to `agreed`/`interested`, next check follows cancelRecheckHours, nudge count is not reset, previously nudged contacts get `close` instead of a second nudge, and double-cancelling is a no-op. These are meaningful behavioral details beyond the structured annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the trigger, then flows through outcome, recheck timing, nudge behavior, idempotency, and the moved-event distinction. Every sentence contributes operational guidance without waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a mutation tool with no output schema, the description covers the relevant state transition, timing behavior, idempotency guarantee, nudge exception, and the nearest alternative tool. Only the optional note parameter is left to the schema, which is acceptable given 100% schema description coverage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents both parameters. The description still adds useful meaning for calendarEventId by tying it to the id recorded with `li_mark_booked` and clarifying the moved-event alternative, though it does not discuss the optional note parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a precise action: find the booking by calendarEventId and restore it to `agreed` with outcome `interested`. It also distinguishes this from `li_mark_booked` for moved events, so an agent can separate it from siblings without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly covers the triggering condition (calendar event gone or cancelled) and the key exclusion: a moved event is not a cancellation and should use `li_mark_booked` with the same calendarEventId and new meetingStart. This gives clear when-to-use and when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_marked_conversationsPeople By Their Marked VerdictA
Read-only
Inspect

The people you marked with ONE outcome, across every account you can see. A general listing: li_marked_conversations(outcome="interested", dueOnly=true) is everyone marked interested whose check-again date (their li_snooze_reply date) has arrived. ⚠️ The booked-call follow-up does NOT run from here any more: people who agreed to a call are tracked with li_track_booking and worked from li_booking_due (nudge/close with limits), li_match_contacts + li_mark_booked (the calendar), li_bookings. DB-backed and instant; no browser.

Each row: name, company, profileUrl, accountId/accountLabel, campaignName, outcomeAt + outcomeNote, checkOn (the check-again date — it IS the snooze, set with li_snooze_reply), theirLastAt/ourLastAt (ends of the STORED thread; null = nothing stored, never "they did not write"), nudgesSent + lastNudgeAt (messages we sent after the verdict AND after their last word, i.e. nudges still unanswered — derived from the stored thread), repliedSinceMarked and suppressed. The system stores no email address for prospects: match a booking by name (and company).

dueOnly DROPS, and counts in excluded: anyone who wrote since the last decision or was reopened (they are owed a reply — li_replies_to_answer), anyone on a do-not-contact list, and anyone whose check date is still ahead. Without it every marked person is listed, flagged instead of dropped.

Nothing here sends anything, and a check date arriving sends nothing either. Record a booked call with li_mark_booked (or li_mark_outcome(outcome="meeting_booked") when there is no calendar event).

NameRequiredDescriptionDefault
limitNoMost rows to return (default 100, max 200)
dueOnlyNoOnly people actionable today: check date arrived or never set, not written since, not suppressed
outcomeYesWhich verdict to list — usually "interested" or "meeting_booked"
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
sessionLabelNoScope to one account label (omit for every account you can see)

TDQS

A4.1/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare read-only/safe, but the description adds substantial context beyond them: what dueOnly DROPS and counts in excluded, what null means in theirLastAt (never 'they did not write'), what nudgesSent means, that no email is stored, and that neither the tool nor a due date sends anything. This is exactly the kind of behavioral disclosure the annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Substantial and information-dense, but it front-loads a long sibling-routing narrative and a full row-field catalogue before the core call signature is clear. Several clauses (booking lifecycle, storage caveats) could be trimmed or moved since they concern other tools, hurting front-loading.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Rich enough to call correctly: row semantics, dueOnly behavior, storage caveats, and safety profile are all covered despite the lack of an output schema. Minor gap is that limit/sessionId/sessionLabel scoping is left implicit.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents all five parameters. The description's row-field explanation and its inline usage of outcome/dueOnly add situational clarity but no new syntax or format detail beyond the schema; baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource (listing people by their marked outcome) and scopes it (across every account you can see). Explicitly differentiates from the booked-call flow tracked by li_track_booking/li_booking_due, naming siblings an agent might otherwise confuse.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Example invocation with dueOnly=true clarifies the primary use case, and the note that booked-call follow-up no longer runs here actively redirects the agent to li_booking_due. Doesn't spell out an exhaustive when-not list, but the routing guidance is strong.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_invitationsRecord Triage Decisions On Inbound InvitationsAInspect

Record what you decided about people who invited this account, in OUR database only — it never clicks Accept, Ignore or Withdraw on LinkedIn and sends nothing. li_received_invitations(onlyUnreviewed) then leaves them out (counted under alreadyDecided) until their headline or company changes. Up to 200 per call; each is resolved to the inviter's member id (a renamed vanity URL stays the same person) and keyed per account — the same person inviting two of your accounts gets two independent decisions.

Decisions: rejected and wrong_lane need a reason (wrong_lane takes routeTo, the account that should reach them cold instead), deferred is 'look again later', failed is for a failed accept. accepted is REFUSED unless li_accept_invitations confirmed the accept or they are already in a campaign on this account — accept them with li_accept_invitations, which records accepted itself. Re-marking overwrites and keeps the old decision in the history. A rejection is NOT a do-not-contact: nobody is suppressed.

confirmMark=false is a dry run: every row says marked / updated / unchanged / refused (with why) and nothing is written. One refusal never blocks the rest.

NameRequiredDescriptionDefault
decisionsYes1-200 decisions, one per inviter; each is applied or refused on its own
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
confirmMarkYestrue writes; false previews every row
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnlyHint=false, destructiveHint=false, openWorldHint=false), it discloses the 200/call cap, per-account keying, member-id resolution, overwrite-plus-history behavior, that re-marking preserves the old decision, that a rejection is NOT a suppression, and the dry-run row outcomes (marked/updated/unchanged/refused) with per-row independence.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but front-loaded — the core purpose and non-side-effect guarantee lead, with decision rules and dry-run semantics following. Some sentences are long and pack multiple clauses, but essentially no filler for a tool of this complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex mutation tool with no output schema, the description covers everything an agent needs: side-effect scope, decision semantics, refusal handling, history behavior, suppression caveat, and dry-run output shape. Nothing material is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real semantics: the conditional requirement of `reason` for rejected/wrong_lane, the meaning of `routeTo` as the cold-reach account, the accepted-refusal rule keyed to a decision value, and the sessionId/sessionLabel multi-account requirement. It goes beyond restating the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (records decisions) and resource (inbound invitations on this account), and immediately scopes it as 'in OUR database only' with no LinkedIn-side action. This cleanly distinguishes it from li_accept_invitations and other invitation tools without the agent needing to open a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes the agent: 'accepted' is refused unless li_accept_invitations confirmed the accept, so accept via that tool instead. It also names the li_received_invitations interplay (rows leave results, counted under alreadyDecided) and defines when each decision applies.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_outcomeRecord How A Conversation WentA
Destructive
Inspect

Record the human verdict on a conversation: won, meeting_booked (a call is on the calendar — NOT won), interested (they want to talk, nothing booked yet), not_interested, or no_outcome (it ran its course and nothing came of it). A demo request or a booked call is never won. This is orthogonal to the message status, which only ever says where the AUTOMATION stopped — a deal and a dead end are both handed_off, and nothing in the pipeline can tell them apart, so a person marks it.

Mark up to the batch limit at once with profileUrls — after working a queue the verdict is usually the same for several people, and it was the one mark that still cost a call each.

Marking an outcome CLOSES the conversation: every SCHEDULED step is cancelled and an open sequence moves to handed_off, so no further automated message goes out. A newer message from them REOPENS it (verdict kept), so an interested person who writes back is owed a reply again and leaves li_marked_conversations(dueOnly) until marked again.

For someone who AGREED to a call, use li_track_booking instead of marking interested by hand: it marks interested AND schedules the booking check that li_booking_due works from. A booked call is li_mark_booked (outcome meeting_booked plus the calendar event). Marking not_interested/no_outcome closes an open booking; meeting_booked marks it booked.

⚠️ It does NOT reach a message already handed to the queue. A reply you queued with li_send_reply moments ago is a job on its way to a browser, and nothing consults the outcome before it types — so marking somebody not_interested does not stop the answer you just sent them. Check li_job_result before marking if that matters. Pass outcome=null to clear a mis-mark — that only unsets the verdict and revives nothing; use li_reset_contact to put a contact back into the sequence.

NameRequiredDescriptionDefault
noteNoWhy — stored with the verdict and in the timeline
outcomeYeswon | meeting_booked | interested | not_interested | no_outcome, or null to clear a mis-mark. Applies to every contact in this call.
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
sessionIdNoThe account this conversation is on. Only used when the person is in NO campaign: then a light record is created on this account so the mark has something to act on. Needed only when you have more than one account.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
profileUrlsNoSeveral contacts in ONE call, max 200. Each is resolved and marked independently: one refusal does not stop the rest, and the reply reports every one by URL.
sessionLabelNoSame, by account label
conversationUrnNoThe thread, straight from li_list_conversations (single-person form). Recorded on the conversation so the live inbox recognises it as decided. On its own it finds the person only when their thread is already stored — pass profileUrl (the participantProfileId / ACoAA… URL) too.
participantNameNoTheir name from li_list_conversations — kept on a newly recorded contact.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true; the description earns that by specifying exactly what gets destroyed — scheduled steps cancelled, open sequence moved to handed_off. It goes further with non-obvious traits: a newer message REOPENS the conversation with the verdict kept, and marking does NOT reach a message already handed to the queue. This is behavioral context well beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the verdict definitions, then layers orthogonality, batching, closing semantics, and routing. Dense but each paragraph carries information; the length is justified by the eleven parameters and the destructive/reopen semantics, though it is heavier than strictly necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, yet the description fully covers what the tool does to state (closes conversation, cancels scheduled steps, hands off, reopens on new message), the queue caveat, and every sibling handoff. Nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds rationale beyond the schema: why profileUrls batching exists (same verdict across a queue, one call saved) and confirms outcome=null only unsets the verdict and revives nothing. Adds genuine meaning over the structured fields.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (record the human verdict) plus the exact resource and enumerates all five verdicts with discriminating definitions (`meeting_booked` = call on the calendar, NOT won). An agent can distinguish this from siblings like li_mark_booked or li_track_booking without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes to alternatives: use li_track_booking for someone who agreed to a call, li_mark_booked for a booked call, li_reset_contact to revive a sequence, li_job_result to check a queued reply first. It also states when-not (a demo request or booked call is never `won`) and how to undo (outcome=null).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_repliedMark Contact RepliedA
Destructive
Inspect

Flag a contact as REPLIED — stops the auto follow-up sequence and surfaces them in li_replies_to_answer. Rarely needed: the inbox sweep detects replies by itself (force one with li_sweep_inbox); use this when you saw a reply it cannot, e.g. one sent by email. Writes our database only and sends nothing. Identify by profileUrl (vanity URL, the /in/ACoAA… URL li_list_conversations returns, or a bare member id — all resolve to the same person) or contactId. A person in NO campaign (an inbound or manual thread) gets a light conversation record on the account you name with sessionLabel, instead of a refusal — pass the participantProfileId URL (and conversationUrn + participantName) from li_list_conversations. After answering, close it with li_mark_reply_handled; record a verdict with li_mark_outcome.

NameRequiredDescriptionDefault
noteNoOptional note for the contact timeline
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
sessionIdNoThe account this conversation is on. Only used when the person is in NO campaign: then a light record is created on this account so the mark has something to act on. Needed only when you have more than one account.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
profileUrlsNoSeveral contacts in ONE call, max 200. Each is resolved and marked independently: one refusal does not stop the rest, and the reply reports every one by URL.
sessionLabelNoSame, by account label
conversationUrnNoThe thread, straight from li_list_conversations (single-person form). Recorded on the conversation so the live inbox recognises it as decided. On its own it finds the person only when their thread is already stored — pass profileUrl (the participantProfileId / ACoAA… URL) too.
participantNameNoTheir name from li_list_conversations — kept on a newly recorded contact.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true and openWorldHint=false, but the description adds crucial scope: 'Writes our database only and sends nothing,' which tells the agent the destructive hint is a local record mutation, not an outbound message. It also discloses partial-failure semantics ('one refusal does not stop the rest') and the light-record branch for no-campaign people.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with purpose, then when-to-use, then side effects, then identity mechanics — a sensible order. It is dense and long, but nearly every clause carries a distinct constraint rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 10-parameter, no-required, no-output-schema tool, the description covers identity resolution, the no-campaign branch, bulk partial-failure behaviour, and the follow-up sequence. It does not mention the note or messageId parameters, but those are documented in the schema and are minor.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% (baseline 3), but the description adds real meaning on top: profileUrl accepts vanity URLs, /in/ACoAA… URLs, or bare member ids; campaignId is needed whenever a contact may sit in two campaigns; conversationUrn alone only finds an already-stored thread. Cross-parameter guidance like pairing profileUrl with conversationUrn/participantName exceeds the schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+effect: 'Flag a contact as REPLIED — stops the auto follow-up sequence and surfaces them in li_replies_to_answer.' This distinguishes it from li_mark_reply_handled and li_mark_outcome, which are named as the follow-on steps.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when NOT to use it ('Rarely needed: the inbox sweep detects replies by itself (force one with li_sweep_inbox)') and the narrow condition that selects it ('use this when you saw a reply it cannot, e.g. one sent by email'). It also routes to the two follow-up siblings, so the agent knows the full sequence.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_reply_handledMark Reply HandledAInspect

Record that a person DEALT WITH this conversation. Call it after you answer a reply, or to close one you are not going to answer.

It is no longer what empties li_replies_to_answer — that queue is derived from the thread, so an answer sent with li_send_reply drops the row once the send lands. What this still does, and nothing else does: it writes the conversation down as handled, which is what makes the contact stop reading "needs reply" on their dossier and in the campaign, takes them out of the reply-rate numerator, and puts a dated line with your note on their timeline. It is also the only way out for a thread the queue cannot read — an answer typed by hand in LinkedIn (nothing re-reads that thread), a reply sent from a different account than the one that campaign uses, or a send that reported a failure after the message had in fact gone out.

It says NOTHING about how the conversation went — that is li_mark_outcome (won / meeting_booked / interested / not_interested / no_outcome), and marking an outcome does this as well, so use that one instead whenever you already know the verdict.

If they write AGAIN after you mark this, the conversation REOPENS on its own: the inbox sweep (about hourly) still watches a handled thread for new messages from them, puts it back in li_replies_to_answer with reopened, and li_list_conversations(onlyNeedsReply) lists it again. ⚠️ Still NOT for "not right now" — nothing brings a handled conversation back on a DATE. Use li_snooze_reply for that.

NameRequiredDescriptionDefault
noteNoOptional note for the contact timeline, e.g. what you answered
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
sessionIdNoThe account this conversation is on. Only used when the person is in NO campaign: then a light record is created on this account so the mark has something to act on. Needed only when you have more than one account.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
profileUrlsNoSeveral contacts in ONE call, max 200. Each is resolved and marked independently: one refusal does not stop the rest, and the reply reports every one by URL.
sessionLabelNoSame, by account label
conversationUrnNoThe thread, straight from li_list_conversations (single-person form). Recorded on the conversation so the live inbox recognises it as decided. On its own it finds the person only when their thread is already stored — pass profileUrl (the participantProfileId / ACoAA… URL) too.
participantNameNoTheir name from li_list_conversations — kept on a newly recorded contact.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only say non-read-only, non-destructive, closed-world; the description goes far beyond by disclosing the automatic REOPENING on a new inbound message (hourly sweep, `reopened` flag), the timeline write, the reply-rate numerator effect, and the failure/re-send edge cases. This is substantive behavioral context no annotation conveys.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four dense paragraphs, front-loaded with the core action and then the queue/side-effect mechanics, with bolded warnings. Slightly verbose and warning-heavy, but nearly every sentence carries non-redundant information rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 10-param, no-output-schema mutation tool, the description covers the action, side effects, reopening lifecycle, alternatives, and edge cases a caller would otherwise get wrong. Nothing material for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the prose adds real meaning: it frames why campaignId scoping matters (multiple memberships), why conversationUrn alone may not find the person, and when sessionId is needed. It stops short of walking every one of the 10 params, but it enriches the semantically tricky ones.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (record/mark) and resource (a reply/conversation as handled) and immediately contrasts it with li_mark_outcome, li_snooze_reply, li_send_reply, and li_replies_to_answer. An agent can distinguish it from every sibling without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use ('after you answer a reply, or to close one you are not going to answer'), explicit when-not ('NOT for not right now... Use li_snooze_reply'), and names the preferred alternative when a verdict is known (li_mark_outcome). It even enumerates the queue-unreadable edge cases it uniquely handles.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_reviewedRecord That A Thread Was ReviewedA
Destructive
Inspect

The routine ledger: record that you LOOKED at a thread and decided something about it — "vendor pitch, skip", "nothing to answer", "answered by hand" — without claiming an outcome about the person. li_list_conversations(onlyNeedsReply) then skips that thread (counted in alreadyReviewedCount) until it MOVES: a message newer than lastMessageAt brings it back by itself. Keyed on the thread and account, so it works for anyone, tracked or not; a new review of the same thread replaces the old one. Pass lastMessageAt exactly as li_list_conversations gave it (lastActivityAt). It sends nothing, changes no status and records no outcome — use li_mark_outcome for a verdict on the person, li_mark_reply_handled to close a tracked conversation.

NameRequiredDescriptionDefault
noteNoWhy, in a sentence
decisionYesShort verdict, e.g. skip, vendor_pitch, no_action, answered_by_hand, spam
sessionIdNoThe account the thread is on
sessionLabelNoSame, by account label
lastMessageAtYesThe thread's newest activity you reviewed — `lastActivityAt` from li_list_conversations (epoch ms, or an ISO date).
conversationUrnYesThe thread, from li_list_conversations

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations give readOnlyHint=false and destructiveHint=true; the description justifies the destructive flag by disclosing replacement semantics ('a new review of the same thread replaces the old one') and bounds the blast radius ('sends nothing, changes no status and records no outcome', counted in alreadyReviewedCount). It does not cover permission/auth requirements or how to undo a review, so it stops short of a 5, but it adds substantial behavior beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose before branching into side effects and alternatives, and every clause carries information. It is dense with quoted examples and parenthetical asides, which slightly slows parsing, but nothing is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 6-parameter mutation with no output schema and no annotations about permissions, the description covers purpose, effect, reversal, replacement and sibling routing. The remaining gap is the return value/confirmation (e.g. whether alreadyReviewedCount is returned), which an agent would have to discover empirically.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds a real cross-tool constraint: pass lastMessageAt 'exactly as li_list_conversations gave it (lastActivityAt)', mapping a confusingly named field between two tools. It also explains the thread+account keying that makes sessionId/sessionLabel interchangeable, which the schema only hints at.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb and resource — recording that a thread was *looked at*, explicitly not an outcome about the person — and distinguishes itself from the near-identical siblings li_mark_outcome and li_mark_reply_handled by name. The scope ('without claiming an outcome about the person') is exactly the axis an agent needs to separate this from the other mark_* tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says when to use it (after looking at a thread and deciding something) and when not to ('use li_mark_outcome for a verdict on the person, li_mark_reply_handled to close a tracked conversation'). It also explains the downstream interaction with li_list_conversations(onlyNeedsReply) and the exact reversal condition ('a message newer than lastMessageAt brings it back'), so an agent knows both the trigger and the effect.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_mark_stoppedStop A ContactA
Destructive
Inspect

Stop a contact entirely (not a fit / do-not-contact) — cancels every pending step so nothing else goes out. It records NOTHING about how the conversation went; if you know, say so with li_mark_outcome instead (it stops the sequence too).

🚨 THIS IS TERMINAL, AND IT IS NOT A HOLD. It sets the message to handed_off, which closes the person out everywhere: no further automated step ever goes out, and only a new message from them reopens the conversation (the inbox sweep (about hourly) still watches the thread); the contact reads as closed on every surface; and acceptance can no longer revive them. If what you mean is "do not send this message, but keep the sequence alive", that is li_cancel_steps — it holds only the steps you name and touches no message status, no contact status and no outcome. A note saying "holding" changes nothing: the row still reads finished. Only reach for this tool when you mean the person is finished with; li_reset_contact can undo it, but only once whatever blocked the send is genuinely resolved.

NameRequiredDescriptionDefault
reasonNoWhy they are finished with, stored on the timeline (e.g. "not a fit")
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
profileUrlsNoSeveral contacts in ONE call, max 200. Each is resolved and marked independently: one refusal does not stop the rest, and the reply reports every one by URL.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare destructiveHint=true; the description goes far beyond by disclosing the exact state change (message set to handed_off), the full blast radius ('closes the person out everywhere', contact reads closed on every surface), the sole reopening condition (a new inbound message, with the hourly inbox sweep), and that acceptance can no longer revive them. This is exactly the behavioral context an agent needs before an irreversible action.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core meaning is front-loaded in sentence one and the critical warning is bolded. The hold-versus-stop distinction is restated twice (once as 'NOT A HOLD', once as the quoted 'keep the sequence alive' case), which is mild redundancy, and the third paragraph is a long compounded sentence — defensible given the stakes, but not maximally tight.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive, high-consequence tool with no output schema and no required parameters, the description covers what changes, what persists, who can be affected across campaigns, and how to reverse it. Nothing needed to invoke it safely is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents contactId, campaignId, profileUrl, profileUrls, messageId, and reason with rich per-field guidance. The description adds no parameter-level syntax or format detail, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource ('Stop a contact entirely') and immediately scopes the meaning with '(not a fit / do-not-contact)'. It explicitly distinguishes itself from li_mark_outcome, li_cancel_steps, and li_reset_contact, so an agent can route correctly without opening any sibling schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use ('Only reach for this tool when you mean the person is finished with'), names the exact wrong-intent alternative for the common confusion ('do not send this message, but keep the sequence alive' -> li_cancel_steps), routes outcome-recording elsewhere, and discloses the undo path via li_reset_contact with its precondition.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_match_contactsMatch Calendar Attendees To ProspectsA
Read-only
Inspect

Match booking attendees you read from the user's calendar (Google Calendar, Calendly, Cal.com, or booking-confirmation emails) to prospects — deterministic, read-only, no LLM. Pass what the event shows: name, email if any, the calendar's eventId, startsAt, and ALSO createdAt (when the event was created/booked) and the raw description whenever the event has them — the timing and time-zone signals are read from those two.

The pool: people with a booking in play (link sent, agreed, cancelled), everyone in an active campaign, anyone messaged in the last 60 days, and anyone our booking link went to in the last 30 days — only on accounts you can see. Names are normalised (case, accents, punctuation, titles like Dr/PhD, middle names and initials, "Last, First", a small nickname table: Bob=Robert, Bill=William…); a prospect's name is cleaned the same way as the name prefilled into the booking link.

Rules, in order: (1) exact full name among people sent the booking link in the last 30 days → match (full_name_recent_link); two such namesakes → the one who booked within 15 minutes of their own LinkedIn message, else ambiguous. (2) email's company domain equals the prospect's companyDomain AND first or last name matches → strong; full name matches exactly → strong; first and last each within one typo → WEAK; exactly one strong → match. Personal email domains (gmail, outlook, hotmail, yahoo, icloud, proton…) never count as a company domain. (3) Scoring (score): first_name +2 (or first_name_typo +1), last_name_in_email +3 (prospect's surname in the email's local part), recent_link +2, booked_after_message +3 (event created 0–15 min after their inbound LinkedIn message), timezone_fits +1 (a zone in the description fits theirs). A conflicting surname excludes a prospect. Match only with a first-name signal, a surname (on the booking or in the email), score ≥ 7 and a lead ≥ 3.

Per candidate: match (safe to li_mark_booked), ambiguous (NEVER mark it — list the candidates in others, best first, for a human to pick), none, or ignored (marked not from LinkedIn with li_ignore_calendar_event — skip it, never mark it). Each carries the rule, the chosen prospect's signals/score, a one-sentence reason, and the other candidates.

NameRequiredDescriptionDefault
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
candidatesYes1-200 calendar bookings to match, one per event, as read from the calendar
sessionLabelNoMatch only against one account

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint/destructiveHint=false, and the description is consistent, adding 'deterministic, read-only, no LLM' plus the candidate pool it searches and the strong constraint that ambiguous results must never be auto-marked. This is useful operational context beyond the annotations, though it stops short of stating any limits or failure behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the input guidance well, but the middle section is a dense wall of internal scoring rules (first_name +2, last_name_in_email +3, score ≥ 7 and lead ≥ 3) that the agent cannot act on, since the output already carries signals/score. Much of that text does not earn its place from the caller's perspective.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the return-value burden and does so: it enumerates the four outcomes (match, ambiguous, none, ignored) and the per-candidate payload (rule, signals, score, reason, others). Coverage of inputs, outputs, and follow-up routing is solid for this complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: it explains that createdAt and description are required to derive the timing and time-zone signals, and clarifies that name/email/eventId/startsAt should reflect what the event shows. This goes beyond the schema's field-level docs.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Match booking attendees you read from the user's calendar ... to prospects') and immediately qualifies it as deterministic, read-only, no LLM. It is clearly distinguishable from siblings like li_mark_booked and li_ignore_calendar_event, which it explicitly feeds into.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Tells the agent what to pass and why (createdAt and description carry timing/time-zone signals), describes the candidate pool, and routes the agent to the right follow-up tools per outcome ('safe to li_mark_booked', 'NEVER mark it', 'marked not from LinkedIn with li_ignore_calendar_event'). It does not explicitly frame when to prefer this over any competing tool, but the workflow context is unambiguous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_move_contactMove A Contact Between CampaignsA
Destructive
Inspect

Move people's membership from one campaign to another, keeping their invite and message history — nothing is re-created, so the sequence continues where it was. This is how you correct a batch that landed in the wrong campaign; before it existed the only option was to stop every person and re-import them, which loses their history. Both campaigns must send from the SAME account: a campaign is bound to one LinkedIn login, so moving across accounts would change who is approaching this person mid-sequence. Refused if the target already holds them (one membership per campaign) or is completed. Their scheduled steps belong to the target campaign afterwards. ⚠️ Moving people into a PAUSED campaign holds further INVITES but does NOT hold a message step that is already scheduled — nothing checks the campaign at send time for those. Three forms: one person, a list of profileUrls, or allInCampaign for everyone still in the source, which is the shape a mis-filed import actually has. A list refuses outright if some cannot move rather than splitting the group across two campaigns; allInCampaign previews first and needs the count echoed back before it writes.

MOVE AND RE-COPY in one call: resnapshot=true (or stepTexts) first re-copies each mover's step kinds and delays from the TARGET campaign's strategy, keeping their own text, then moves them. Only for people nothing has happened to yet (invite not sent, no step sent or scheduled); anyone else, or anyone with no text for a target step kind, is named and nothing is moved. Calling again after a failure is safe.

idempotencyKey: pass the same key when retrying after a timeout, 502 or 503; a call that already finished returns its stored answer instead of running twice (24h).

NameRequiredDescriptionDefault
dryRunNoPreview the move (and the re-copy) without writing.
partialNoMove the ones that can move even though some cannot. Default false.
contactIdNoOne contact by id, instead of profileUrl
stepTextsNoPer-person text for target step kinds a mover has no text for (or to replace theirs). Implies resnapshot.
profileUrlNoOne person
resnapshotNoRe-copy each mover's steps from the target campaign's strategy before moving (kinds and delays; their own text kept). Untouched people only. Not with contactId.
expectCountNoHow many you expect to move, from the preview. The call is refused if the campaign holds a different number — which is what stops a mistyped fromCampaignId moving a live campaign wholesale.
profileUrlsNoSeveral people in ONE call, max 500. Every one is checked BEFORE anything is written: if some cannot move the whole call is refused, because moving the rest would split the group across two campaigns — which is the problem you are fixing, made harder to see. Pass partial=true to move the ones that can.
toCampaignIdYesWhere they should be instead
allInCampaignNoMove EVERYONE still in the source campaign. The shape a mis-filed import actually has, since nobody holds a list of them. Returns a PREVIEW first and writes nothing until you call again with expectCount set to the number it showed.
fromCampaignIdYesThe campaign they are in now
idempotencyKeyNoAny unique string for THIS request (e.g. a UUID). Reuse it only to retry the same call.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true, but the description adds far more than the safety flag: history is preserved rather than re-created, cross-account moves are refused because a campaign is bound to one login, the paused-campaign caveat that invites are held but already-scheduled message steps are not, partial-vs-atomic list semantics, and the idempotency key's 24h replay window. This is genuinely non-obvious behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose is front-loaded and every paragraph carries decision-relevant information (eligibility, atomicity, preview flow, idempotency). It is long and some content overlaps the schema descriptions, but there is little that is purely decorative; the emoji warning marks a real trap.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 12-parameter destructive tool with no output schema, the description covers what an agent needs: which parameter shape to choose, what gets previewed vs written, what causes refusal, and how retries are deduplicated. Nothing essential to calling it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds interaction-level meaning the schema does not: how resnapshot/stepTexts pair with the move, why expectCount exists (guarding a mistyped fromCampaignId), and why a list refuses wholesale while allInCampaign previews first. It does not add syntax detail beyond the schema, keeping it from a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource — moving membership between campaigns — and immediately scopes the behavior ('keeping their invite and message history — nothing is re-created'). It contrasts itself against the historical alternative ('stop every person and re-import them'), so an agent can distinguish it from siblings like li_reset_contact or li_import_prospects without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use ('how you correct a batch that landed in the wrong campaign'), plus hard preconditions (both campaigns must send from the SAME account) and refusal conditions (target already holds them, or is completed). It also disambiguates the three invocation shapes and tells the agent when to use each, which is exactly the routing information a caller needs.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_new_connectionsNew Connections Not In Our DBA
Read-only
Inspect

Read the account's LinkedIn connections list in the worker browser and return only the people NOT yet in a campaign you can see — people who connected outside our sequences (inbound accepts, manual adds) — so you can start outreach on them with li_ingest_prospects (startStage "message"). Read-only on LinkedIn; one browser session on the account lane, deeper and slower as scrollPasses rises, at most limit people. Defaults to your default account. Pending requests TO the account are li_received_invitations; acceptances of our own invites are li_poll_acceptances.

NameRequiredDescriptionDefault
limitNoMost new people to return (1-500)
sessionIdNoAccount id (or use sessionLabel)
scrollPassesNoHow many lazy-load scroll passes (more = more connections, slower). Default 6.
sessionLabelNoAccount label, e.g. "Deven"

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly/openWorld/non-destructive, and the description adds real context beyond them: it runs a browser session on the account lane, gets deeper and slower as scrollPasses rises, caps output at `limit`, and defaults to the default account. It stops short of describing failure modes or pagination/dedup behavior, so 4 rather than 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single dense, front-loaded paragraph where the scope restriction comes first, followed by behavioral notes and sibling routing. Every clause earns its place with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description still defines what is returned (only people not in a campaign, inbound accepts/manual adds) plus the follow-up tool and sibling boundaries. For a moderately complex, four-parameter read tool this is complete enough for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, and the description reinforces rather than extends it ('deeper and slower as scrollPasses rises', 'at most `limit` people'). The schema already documents these meanings, so the description adds only marginal value; baseline 3 for high coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Read the account's LinkedIn connections list') and precisely narrows scope to people NOT yet in a campaign, which distinguishes it from li_connections and the invitation-related siblings. An agent can identify it without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes the agent: pending requests are li_received_invitations, acceptances of our own invites are li_poll_acceptances, and the downstream action is li_ingest_prospects (startStage "message"). It names alternatives and the condition that selects each.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_outcome_summaryHow Conversations Actually WentA
Read-only
Inspect

Counts of the human verdicts across the conversations you can see: won, meetingBooked, interested, notInterested, noOutcome, and undecided — the ones nobody has judged yet. Plus bookings per account: agreed → booked rate, booked without vs after a nudge, median link → booking hours, closed after a nudge, booked off the sequence link with no reply. Scope it to one account with sessionLabel and/or to one campaign with campaignId. This is the only measure of RESULT in the system: every other count says what the automation did, not what came of it. DB-backed and instant.

NameRequiredDescriptionDefault
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
campaignIdNoScope to one campaign, from li_list_campaigns
sessionLabelNoScope to one account label (omit for every account you can see)

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, destructiveHint=false and openWorldHint=false, so the safety profile is covered. The description adds context beyond that: it is 'DB-backed and instant,' and it explains what `undecided` means ('the ones nobody has judged yet'), which is genuine semantic disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is front-loaded with the primary content (the verdict counts) before layering on booking metrics and scoping. It is dense and slightly run-on, but every clause carries information rather than padding.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the burden of explaining return values, and it does so thoroughly by naming the count categories and booking metrics. All params are optional and the scoping behavior is covered, leaving only minor ambiguity around what 'you can see' precisely means.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all three params (sessionId, campaignId, sessionLabel) are already documented in the schema, making 3 the baseline. The description echoes the sessionLabel/campaignId scoping but does not add syntax or format detail beyond the schema, and omits sessionId entirely.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb-and-resource: it enumerates the exact human verdict counts (won, meetingBooked, interested, notInterested, noOutcome, undecided) and the per-account booking metrics it returns. It also explicitly distinguishes itself from siblings by stating it is 'the only measure of RESULT in the system: every other count says what the automation did.'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear context for when to reach for this tool ('the only measure of RESULT') and how to narrow it (sessionLabel and/or campaignId), which implicitly contrasts it with activity-count tools like li_outreach_analytics. It stops short of naming those siblings explicitly or stating exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_outreach_analyticsLinkedIn Outreach AnalyticsA
Read-only
Inspect

Compare outreach performance grouped by strategy, ICP, campaign or account: invite/acceptance rates, reply rates, per-step sends, reply attribution by step, handed-off/failed/completed counts. Also localHours: acceptance and reply rates by PROSPECT-LOCAL send hour, six buckets; sessionLabel and sendWindowMode filter it; sends with no known zone are unbucketed*.

⚠️ READ replyRate AS WHAT IT COUNTS: replies still AWAITING an answer ÷ first messages sent. It FALLS as conversations are marked handled, not as they are answered — so a well-worked strategy scores lower than a neglected one, and handedOff on the same row is the rest of those replies. Judge a strategy with li_outcome_summary, which reads the human verdict, and never quote replyRate on its own as 'how many people replied'.

Also bookings, grouped the same way plus a total: booking links sent, how many agreed to a call, how many booked (agreedToBookedRate), agreedBooked (the numerator of agreedToBookedRate; booked also counts calls booked by people who never agreed), booked without vs after a nudge, median hours from link to booking, closed after a nudge, and bookedFromSequenceLinkNoReply — calls booked straight off the sequence's own link by someone who never replied, which only the calendar can reveal. Rows whose names collide (strategies, ICPs and campaigns are copied per team) carry ' · ', or ' · 2', in label, and rows and bookings.rows name the same key the same way.

groupBy "arm" + experimentId (from li_experiment_assign): per arm × ICP and per arm, n and acceptance (accepted ÷ invited), reply (ever replied ÷ first messages sent) and booking (booked ÷ n) rates, each with k, n and a Wilson 95% interval; people put in the control arm for having no post are reported apart (forcedToControl). Without experimentId it lists your experiments.

NameRequiredDescriptionDefault
groupByYesRow grouping: strategy, icp, campaign, account, or arm (an experiment's arms; needs experimentId)
sessionIdNoLimit to one account by id (li_list_accounts). Omit to cover every account you can see.
experimentIdNogroupBy "arm" only: the experiment (omit to list yours)
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see.
sendWindowModeNoFilter `localHours` to accounts in this send-window mode (account | prospect | both); rows are not filtered

TDQS

A4.1/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover only the safety profile (readOnlyHint=true), but the description adds substantive behavioural semantics: replyRate counts still-awaiting replies over first messages and falls as conversations are marked handled, sends with no known zone fall into unbucketed*, control-arm members forced for lack of a post are surfaced via forcedToControl, and colliding labels get suffixed with account names. These are exactly the non-obvious behaviours an agent needs and are not derivable from the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The text is a dense run-on wall of prose with no headings, bullets, or line breaks, packing metric definitions, caveats and grouping rules into long compound sentences. Much of the booking-metric enumeration is justified only because no output schema exists, but the formatting makes it hard to scan and the warning about replyRate is buried behind a large paragraph.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description correctly carries the return-value burden and documents the metric groups, the arm experiment output, label collision handling and unbucketed rows. It is effectively complete for calling the tool, though it says nothing about result size, row limits, or pagination.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already defines every parameter. The description adds only marginal meaning — that sessionLabel/sendWindowMode scope localHours, that groupBy 'arm' needs experimentId — which the schema largely states itself. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific verb and resource: 'Compare outreach performance grouped by strategy, ICP, campaign or account', and enumerates the concrete metrics returned. It also names sibling tools (li_outcome_summary, li_experiment_assign) so an agent can place it relative to neighbours without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly routes the agent elsewhere for the authoritative verdict ('Judge a strategy with li_outcome_summary... never quote replyRate on its own') and explains the arm/experimentId pairing for the arm grouping. It does not, however, define when to prefer this over other analytics-ish siblings (e.g. li_account_activity, li_bookings), so guidance is strong but not exhaustive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_pause_accountPause or Resume an AccountAInspect

Put one LinkedIn account on hold (paused:true) or resume it (paused:false). While paused it does NO LinkedIn work: scheduled invites and messages are held (not lost — they resume where they stopped), the inbox check, acceptance check and other recurring reads are skipped, its end-of-day report is skipped, and nothing alerts about any of it. Anything asked for directly on it (a message, an invite) is refused with the reason. Signing in (Reconnect) and diagnosis still work. Use it for an outage already known about — e.g. the account's proxy is down — so it stops failing and alerting; it is NOT the fix for a blocked account (that needs Reconnect). Current state is in li_list_accounts (paused).

NameRequiredDescriptionDefault
pausedYestrue = put on hold, false = resume
reasonNoWhy, in a few words — shown next to the Paused badge
sessionIdNoAccount id (or use sessionLabel)
sessionLabelNoAccount label, e.g. "Deven"

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only say this is not read-only and not destructive; the description goes far beyond, spelling out exactly what is suspended (scheduled invites/messages held and resumed in place, inbox/acceptance checks and recurring reads skipped, end-of-day report skipped, no alerts) and what still works (Reconnect, diagnosis). It is also clear that direct requests are refused rather than queued, so the agent knows this is a reversible hold, not a deletion.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the action and the paused parameter, and every sentence carries distinct information (effects, exceptions, routing advice). It is a single dense paragraph of roughly 120 words, so mildly heavier than necessary but without filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description covers the mutating effect, the scope of what changes, the exceptions, the alternative tool for blocked accounts, and where current state is visible. Nothing an agent needs to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, and the schema already explains paused and reason. The description still adds operational meaning for the paused flag beyond 'true = put on hold' by describing the resulting account behavior, which helps the agent choose the right value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (pause/resume) on a specific resource (one LinkedIn account) and encodes the two modes as paused:true/false in the first clause. It is clearly distinguishable from li_pause_campaign and li_hold_sending, which operate on campaigns and sending rather than a whole account.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use case ('an outage already known about — e.g. the account's proxy is down') and an explicit when-NOT-to-use case ('NOT the fix for a blocked account (that needs Reconnect)'), naming the alternative. It also points at li_list_accounts as the way to read current state.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_pause_campaignPause a CampaignA
Idempotent
Inspect

Stop a campaign sending, reversibly. No further invites go out from it, and an invite job already queued for it stops rather than passing the baton to the next person — so a drip that is running is genuinely held, not merely slowed. Reach for this the moment prospects are in a campaign you did not mean, instead of stopping people one at a time. The reply says HOW MANY it held, because a pause that reports only success reads the same whether it caught a hundred people or nobody. Two things it does NOT do: a message step already scheduled still sends (hold those with li_cancel_steps, which leaves the sequence alive — NOT li_mark_stopped, which hands the contact off permanently), and acceptance polling stops for this campaign, so somebody who accepts while it is paused is not noticed until it resumes. Reverse it with li_resume_campaign.

MANY AT ONCE: campaignIds (up to 100) or namePrefix (+ sessionLabel for one account) instead of campaignId. Without confirm:true it only lists what matched, with counts; confirmed, each campaign is paused with its own ownership check and result line.

NameRequiredDescriptionDefault
confirmNoBulk forms only: true to act. Without it the call lists what matched.
sessionIdNoWith namePrefix only: match on this account only
campaignIdNoOne campaign id from li_list_campaigns. Or campaignIds / namePrefix.
namePrefixNoBulk: every campaign you can see whose name starts with this (case-insensitive, the same match as li_list_campaigns), at most 100; listed before anything is paused
campaignIdsNoBulk: up to 100 campaign ids, each paused as the single form would be
sessionLabelNoWith namePrefix only: match on this account only

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=false, destructiveHint=false, idempotentHint=true, so the safety profile is covered. The description still adds non-obvious behavior: a queued invite job is halted mid-sequence, an already-scheduled message step still sends, and acceptance polling stops for the campaign until resume. It doesn't cover auth/permission requirements or rate limits, which keeps it short of a 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but front-loaded and well partitioned, with the MANY AT ONCE block separating the bulk form. Some phrasing is stylistic overhead ('passing the baton to the next person'), which is the only thing keeping it from a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema, but the description covers the return shape ('the reply says HOW MANY it held') and the bulk listing/acting distinction. Combined with the behavior caveats and the reversal path, an agent has everything needed to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3; the description earns above that by explaining the mutually exclusive addressing modes (single campaignId vs campaignIds up to 100 vs namePrefix + sessionLabel scoped to one account) and that confirm:true is what turns a listing into an action. That is meaning the schema alone does not convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Stop a campaign sending, reversibly') and immediately scopes it against siblings: li_cancel_steps for scheduled message steps, li_mark_stopped as the permanent alternative, li_resume_campaign as the inverse. An agent can pick this apart from li_pause_account and li_hold_sending without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit trigger ('the moment prospects are in a campaign you did not mean, instead of stopping people one at a time') plus two explicit exclusions with named alternatives. The bulk section adds a second usage mode (campaignIds/namePrefix) with the confirm:true gating rule spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_pending_invitesList Pending Sent InvitationsA
Read-only
Inspect

Read the connection invites this account has sent that are still pending, newest-age-last, each with how many days ago it was sent. Opens a browser session on the account. Read-only — pair it with li_withdraw_invites to clean up; the invites this system itself tracks are li_connections (DB, instant).

READ reach BEFORE CONCLUDING ANYTHING: 'complete' = every pending invitation was read; 'partial' = the walk stopped early (limitedBy says why), so somebody absent may simply be further down; 'failed' = the list could not be read, which is NOT an empty backlog. An account with nothing pending comes back 'complete' with pendingOnLinkedIn 0 (LinkedIn's own "No sent invitations" page).

pendingOnLinkedIn is LinkedIn's OWN published total and is null when unreadable, never 0 — do not substitute a count of the rows below. trackedPendingInDb shows how many this system itself is tracking, which is lower for any account with manual outreach.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
maxInvitesNoStop after this many, newest first — e.g. 100 for a quick look. Omit to read the WHOLE backlog, which is what you want before bulk-enriching or deciding in one pass, but costs minutes of browser time (~10 invitations per second of scrolling) and holds the browser session for that account throughout, so nothing else on it runs until it finishes.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations (readOnlyHint true, openWorldHint true, destructiveHint false) are consistent with 'Read-only', and the description goes well beyond them: it discloses that it opens a browser session, that the session is held for the account throughout, and it defines the semantics of the `reach` field ('complete'/'partial' with limitedBy/'failed' ≠ empty backlog). That is exactly the kind of behavioral context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the operation and its scope, then the result-interpretation guidance. Every sentence is load-bearing (reach semantics, null handling, DB vs LinkedIn counts), though the all-caps warning and the density of field references make it heavier than it needs to be.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description compensates by explaining the return semantics it would otherwise lack: reach/limitedBy, pendingOnLinkedIn being null-not-0, and trackedPendingInDb tracking fewer than LinkedIn's total for manual-outreach accounts. An agent has what it needs to call this and interpret the result correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the schema descriptions themselves already carry the cost/blocking rationale for maxInvites and the sessionId/sessionLabel fallback behavior, so the description adds essentially nothing parameter-specific (it never mentions sessionId/sessionLabel semantics). Baseline 3 applies when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb+resource+scope: 'Read the connection invites this account has sent that are still pending, newest-age-last, each with how many days ago it was sent.' It also explicitly separates itself from two siblings, li_withdraw_invites (cleanup) and li_connections (DB-tracked, instant), so an agent can pick correctly without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Names the pairing action ('pair it with li_withdraw_invites to clean up') and the distinguishing alternative ('the invites this system itself tracks are li_connections (DB, instant)'), plus context on when to set maxInvites (quick look vs whole backlog before bulk-enriching). No explicit when-not-to-call or prerequisite statements, hence not a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_poll_acceptancesCheck Acceptances + Start Day-1 Follow-upsA
DestructiveIdempotent
Inspect

Trigger one acceptance check on the server-side worker: it scrapes your sent-invites + connections (your network area), marks newly-accepted connections, and AUTO-SCHEDULES each accepted contact's Day-1 (first) message. Use this from a daily cloud routine to drive the post-acceptance flow. Returns immediately (the worker does the single-session browser work) with current invited/accepted counts; re-check li_connections(status="accepted") shortly after to see results. Requires the API/worker to be running.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare non-readOnly, destructive, idempotent and open-world, but the description adds context the annotations cannot convey: the work is performed asynchronously by a server-side browser worker, the call returns immediately with invited/accepted counts, it mutates state by scheduling Day-1 messages, and it depends on the worker being up. That is exactly the behavioral context an agent needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core action and its side effect are front-loaded in the first sentence, followed by usage, async-return behavior, and prerequisites. Four sentences with parentheticals is slightly dense, but each sentence carries information an agent needs and none is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still explains what comes back (current invited/accepted counts) and warns that the real results must be read later via li_connections. Prerequisites, async semantics, and side effects are all covered, so an agent can invoke this correctly without further context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters (schema coverage 100% is vacuous), so per the rubric the baseline is 4. The description correctly implies no argument is needed and instead names a separate tool for retrieving results, adding no confusion about parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a specific verb and mechanism: it triggers an acceptance check on the server-side worker, scrapes sent-invites/connections, marks newly-accepted contacts, and auto-schedules Day-1 messages. This is far more than a restatement of the name. However, it never distinguishes itself from the near-identically-named sibling li_check_acceptance, so an agent cannot confidently route between the two from the description alone.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives an explicit usage scenario ('Use this from a daily cloud routine to drive the post-acceptance flow') plus a prerequisite ('Requires the API/worker to be running') and a follow-up step (re-check li_connections(status="accepted")). There is no when-not guidance and no exclusion relative to li_check_acceptance, which is the main ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_proxiesEgress Proxies (operator only)A
Read-only
Inspect

OPERATOR ONLY. Lists the egress proxies an account can be assigned — label, host:port, the country and type the vendor SOLD it as, the last check (real egress IP, ASN, and its type: isp passes, hosting is a datacenter), and which accounts use each — plus installDefault, the env proxy every account WITHOUT its own proxy uses. Never returns a username or password. Adding a proxy or changing its credentials is done on the dashboard (Proxies page), never through a tool, so no credential passes through a conversation. Two accounts on one proxy share an IP — the safety rule is one per account.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already carry readOnly/non-destructive/non-open-world, and the description goes well beyond them: it discloses that credentials are never returned, explains the egress-IP/ASN type semantics (isp passes, hosting is a datacenter), names installDefault and the no-proxy fallback, and states the one-account-per-proxy sharing risk.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the operator-only warning and structured as one dense inventory sentence followed by security and safety notes. Every clause carries information, though the long first sentence packs several distinct ideas together.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description fully carries the return-value burden: it enumerates the returned fields, explains their meaning, and closes the obvious ambiguity (no credentials returned). Nothing needed to call or interpret this tool is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes no parameters, so per the baseline this is a 4. The description instead spends its effort on output semantics (field meanings, ISP vs hosting classification), which is the right place given an empty input schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb and resource ("Lists the egress proxies an account can be assigned") with concrete detail on what each row contains — label, host:port, vendor-sold country/type, last-check egress IP and ASN classification, consumers, plus installDefault. It distinguishes itself from one-off siblings like li_check_proxy by scope, but never names a sibling explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

"OPERATOR ONLY" sets an access precondition, and it routes the agent away from tools for mutation work (adding a proxy or changing credentials happens on the dashboard, not via a tool). It does not explicitly say when to prefer this over li_check_proxy or how it relates to li_set_account_proxy assignment.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_react_to_postReact to a LinkedIn PostA
Idempotent
Inspect

React to ONE LinkedIn post from a chosen account, now: Like by default, or another of LinkedIn's reactions. The worker's browser opens the post, reads it like a person would, refuses if the page shows more than one post or the post already carries this account's reaction (a reaction is a toggle, so it is never clicked twice), clicks once, and confirms the reaction shows. Returns outcome (reacted, already_reacted, post_unavailable, not_confirmed, failed), the post's author as the page names them, and reactionsToday. Dry-run unless confirmReact=true: confirm the post and the account with the user first, since a reaction is public.

Capped at 15-25 reactions a day per account (our policy; LinkedIn publishes no limit), shared with campaign engagement, and spaced a few minutes apart, so a call may wait for its turn. Not held to sending hours (a person asked now). A timeout is NOT a failure: the job is still queued, so never call again for the same post; read li_job_result with the returned jobId.

To warm up an INVITE, do not call this: give the prospect engagePostUrl in li_ingest_prospects or li_create_campaign and the campaign reacts about a day before the invite, never less than 16 hours, checking the post is really theirs. Reading someone's recent posts to pick one is a separate lookup; this tool only reacts.

NameRequiredDescriptionDefault
postUrlYesThe post: https://www.linkedin.com/feed/update/urn:li:activity:<id>/ (also share/ugcPost urns) or a https://www.linkedin.com/posts/<...>/ copy-link. Anything else is refused before a browser opens.
reactionNoLIKE (default), PRAISE (Celebrate), EMPATHY (Love), INTEREST (Insightful), APPRECIATION (Support) or ENTERTAINMENT (Funny); the on-screen name works too.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
confirmReactYesMust be true to react; false returns a dry-run preview and touches nothing
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations: describes the browser opening the post, refusal when multiple posts or a prior reaction exist, toggle semantics preventing double-click, daily cap of 15-25 shared with campaign engagement, spacing that may cause wait, not being held to sending hours, and that a timeout is not failure – read li_job_result with the returned jobId. Annotations (non-readOnly, idempotent) are consistent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and default; then behavior, caps, failure handling, and the invite-warmup exclusion. Dense but every paragraph earns its place for a multi-behavior tool. Slightly long, but the exclusions are necessary for correct routing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema, so the description explains return values (outcome enum, author, reactionsToday) and failure semantics (timeout not failure, use li_job_result). Caps, preconditions, and account-selection rules are all covered for a 5-parameter mutating tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and descriptions are strong, but the prose adds value: the default reaction, that confirmReact=false means dry-run touching nothing, that sessionId/sessionLabel are never guessed and required with multiple accounts, and the postUrl refusal behavior. Mostly restates schema though, so not 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (react) and resource (ONE LinkedIn post from a chosen account), and explicitly disambiguates from sibling behaviors: warming up an invite must go through li_ingest_prospects/li_create_campaign, and picking a post is a separate lookup. An agent can distinguish this from li_fetch_posts and campaign tools without opening anything.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use and when-NOT-to-use: 'To warm up an INVITE, do not call this' and routes to li_ingest_prospects/li_create_campaign instead; also says reading posts to pick one is a separate lookup. Names the dry-run default and the confirm precondition with the user.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_read_conversationRead a LinkedIn ConversationAInspect

Read one thread end to end (oldest → newest), each message tagged with senderIsUs and a real ISO timestamp. Identify the thread by conversationUrn (from li_list_conversations, most reliable) and/or profileUrl. Opens a browser session, so it takes a while — check li_conversation_history first (by profileUrl or conversationUrn), which serves the stored copy instantly. Every 1:1 thread read is STORED, including one with somebody in no campaign (kept on a light contact found by their member id), so the next draft reads it from li_conversation_history instead; stored says how many rows were new. noThread: true (with ok: true) means we looked and there is no conversation with that person; an error means the lookup or read did not work, which is NOT the same. Sends nothing and changes nothing on LinkedIn — use the thread to draft a contextual reply, then send it with li_send_message.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
profileUrlNoContact profile URL to locate the thread when conversationUrn is omitted. A vanity /in/<slug> or an fsd-id /in/ACoAA... URL both resolve.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.
conversationUrnNoConversation urn from li_list_conversations (most reliable)

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial context beyond the annotations: 'Opens a browser session, so it takes a while' discloses latency, and the STORED side effect (every 1:1 read is persisted, light-contact retention) explains why readOnlyHint is false despite a read operation. It also disambiguates noThread: true (ok: true, no conversation exists) from an error (lookup/read failed), which annotations cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and structured logically (purpose → identification → latency + alternative → storage → noThread semantics → side-effect note). It is information-dense with several parentheticals, but each sentence carries distinct guidance rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the return semantics itself (senderIsUs, ISO timestamps, stored row count, noThread vs error) and the safety profile (sends and changes nothing). Given four optional params and an open-world browser operation, nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so all four parameters are already documented in the schema, including conversationUrn being 'most reliable'. The description adds only marginal routing detail (urn and/or profileUrl, profileUrl as fallback when urn omitted), so the baseline 3 for full schema coverage applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource with exact scope: 'Read one thread end to end (oldest → newest)', including the return shape (senderIsUs, ISO timestamp). It distinguishes itself from the sibling li_conversation_history (stored copy) and li_read_conversations_bulk, so an agent can route without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use and when-to-prefer-alternative: check li_conversation_history first (instant stored copy), and only call this when a live read is needed. Also names the downstream step (draft the reply, send with li_send_message) and the identification inputs (conversationUrn from li_list_conversations, and/or profileUrl).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_read_conversations_bulkRead Many Threads At Once (past the 20-thread cap)A
Read-only
Inspect

Read the last N messages of the threads with MANY named people (up to 100) in ONE browser session. Use it when you know WHO: li_list_conversations walks the inbox by recency instead, li_read_conversation is the one-person form, and li_conversation_history serves already-stored threads instantly. Each result carries weOweReply, so this answers 'which of these 100 people am I yet to reply to'; the reply also counts oweReply, noThread and failedReads. Per person: failed means the read did not work (NOT an empty thread), noThread means we looked and there is no conversation. Read-only on LinkedIn. One browser session however many people you name, so prefer one big call over several small ones.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
profileUrlsYesProfile URLs to read, max 100 per call
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.
perThreadLimitNoMost recent messages to return per person (default 10, max 50)

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the readOnly/destructive/openWorld annotations by disclosing that all reads share one browser session, that results carry weOweReply, that the reply includes oweReply/noThread/failedReads counters, and the exact distinction between `failed` (read did not work) and `noThread` (no conversation exists). That error-semantics detail is genuinely useful and not derivable from structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core action and scope, then routes to alternatives, then covers result/error semantics in a tight sequence with no filler. Dense but every sentence earns its place; the only minor cost is the cluster of backticked result-field names that reads slightly heavily.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 4-parameter read tool with no output schema, the description supplies what is otherwise missing: return-field meaning (weOweReply and the counters), per-person failure semantics, single-session behavior, and account defaults are covered by the schema. An agent has everything needed to call and interpret it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents profileUrls, perThreadLimit, sessionId and sessionLabel. The description echoes the 'last N messages' and 'up to 100' limits but adds no syntax or format detail beyond what the schema provides, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource (read the last N messages of threads for many named people, up to 100) in the first sentence, and explicitly contrasts itself with three siblings: li_list_conversations, li_read_conversation, and li_conversation_history. An agent can select it without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit selection condition ('use it when you know WHO') and then names each alternative with the behavior that distinguishes it (inbox by recency, one-person form, already-stored threads). It also states a preference rule: one big call over several small ones.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_received_invitationsInbound (Received) Connection RequestsAInspect

List the connection requests people sent to this account that are awaiting a decision, each with name, HEADLINE (role + company), mutual connections, profile URL, the inviter's permanent memberId (ACoAA…) and a decision block from the triage memory. Read-only on LinkedIn.

RETURNS A JOB ID AND DOES NOT WAIT (by default). A long tray takes minutes to walk — a 57-invite tray at scrollPasses 40 outlived the client's 180s wait while the job finished fine. Read the result with li_job_result({ jobIds: [jobId], waitMs: 120000 }) — waitMs is honoured with jobIds, not jobId. Pass wait=true only for a small tray.

TRIAGE MEMORY. Every row carries decision: null if this account never judged them, else { decision (accepted | rejected | wrong_lane | failed | deferred), reason, icp, campaignId, routeTo, decidedAt, changedSinceDecision } — and changes (headlineBefore/Now, companyBefore/Now) when their headline or company moved since they were judged (case, whitespace and emoji ignored). People are matched on the member id, so a renamed vanity URL is still recognised. onlyUnreviewed=true returns ONLY the rows that need judging: never judged, a failed accept to retry, or changed since judged — and counts still covers the whole tray: { total, unreviewed, alreadyDecided: { accepted, rejected, wrong_lane, deferred }, failedRetry, changed }, so you can say '56 already checked' without listing them. Record verdicts with li_mark_invitations; li_accept_invitations records accepted/failed by itself. Each read also updates first/last-seen on every person it saw.

CONNECTION REQUESTS ONLY, by default. The same page carries newsletter-subscription and page-follow invitations listed against the inviter's own profile; accepting 'them' subscribes the account to a newsletter. filteredOut counts them; includeNonConnection=true shows them (with decision: null — they are never a connection decision).

🚨 READ pageState.reachedEnd BEFORE CONCLUDING ANYTHING FROM AN ABSENCE. When false, countsAreFloor is true and a reachNote says so: somebody missing may simply be below where the walk stopped. Nothing is ever marked gone from a read. Raise scrollPasses and read again.

NameRequiredDescriptionDefault
waitNoBlock until the read finishes and return the list directly. Default false (returns a jobId — read it with li_job_result). Only for a small tray.
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
scrollPassesNoA CEILING on scroll passes, not a fixed number — the walk stops as soon as the card count settles and reports `pageState.reachedEnd`. About ten invitations load per pass. Default 12; raise it for a big backlog.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.
onlyUnreviewedNoReturn only people never judged, failed accepts to retry, and people whose headline/company changed since they were judged. Counts still cover the whole tray. Default false.
includeNonConnectionNoAlso list newsletter, page-follow and event invitations. Default false — they are not requests to connect.

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With annotations present, the bar is lower, yet the description adds substantial behavioral context beyond them: the async job pattern (returns a jobId, must be read with li_job_result, waitMs honoured with jobIds not jobId), a concrete timing anecdote (57-invite tray at scrollPasses 40 outlived a 180s wait), the side effect that each read updates first/last-seen, and the reachEnd/countsAreFloor caveat. The 'Read-only on LinkedIn' claim sits alongside readOnlyHint=false, but the description itself resolves this by disclosing the local first/last-seen mutation, so there is no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is long but front-loaded with bold headers and almost every block earns its place (async contract, triage memory shape, newsletter trap, reachEnd warning). The one anecdotal sentence about the 57-invite tray is slightly verbose but supports the timing claim.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description carries the full burden of describing returns, and it does: the decision block fields, the changes object, counts breakdown, filteredOut, and pageState.reachedEnd/countsAreFloor/reachNote. For a 6-param tool with no output schema this is complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds meaning beyond the schema: it explains why onlyUnreviewed exists (never judged, failed retry, changed since judged), that counts still cover the whole tray, and what includeNonConnection surfaces (newsletter/page-follow invitations with decision: null). This goes past restating the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('List the connection requests people sent to this account that are awaiting a decision') and enumerates the returned fields (name, headline, mutual connections, memberId, decision block). It is clearly distinguishable from siblings like li_pending_invites (outbound) and li_new_connections.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes the agent to alternatives: 'Record verdicts with li_mark_invitations; li_accept_invitations records accepted/failed by itself.' It also gives when-to-use conditions for onlyUnreviewed, includeNonConnection, and wait (small tray only), so nothing about selection is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_rename_accountRename an AccountAInspect

Give one LinkedIn account a name of your choosing, or pass an empty name to let LinkedIn name it (the person's own name, kept up to date). A typed name is never overwritten by LinkedIn's. The account still answers to its LinkedIn name, slug and member id too. Changes only our label — nothing on LinkedIn. Current names are in li_list_accounts.

NameRequiredDescriptionDefault
nameYesThe new name; empty = use the LinkedIn name
sessionIdNoAccount id (or use sessionLabel)
sessionLabelNoCurrent name, LinkedIn name, slug or member id

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag readOnlyHint=false and destructiveHint=false, and the description adds genuinely new behavior: a typed name is never overwritten by LinkedIn, and the change is confined to our local label. That clarifies reversibility and blast radius well beyond the annotation flags.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four short sentences, each carrying distinct information (naming rule, empty-name fallback, persistence, alias resolution, pointer to sibling). Front-loaded on the core action; no filler, though it is denser than strictly needed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema, but the tool is a simple local relabeling mutation whose scope, fallback behavior and naming persistence are all spelled out. An agent has enough to call it correctly; only the return payload is unspecified, which is minor here.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline 3, but the description goes further: it explains the empty-string semantics of name, the persistence guarantee for typed names, and that the account also answers to its LinkedIn name, slug and member id (mapping to sessionLabel/sessionId).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (rename/give name) and resource (one LinkedIn account), and immediately bounds the effect: it changes only our label, nothing on LinkedIn. This distinguishes it cleanly from siblings like li_list_accounts or li_set_account_proxy.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives a concrete usage rule (pass an empty name to let LinkedIn name the account) and points to li_list_accounts for current names. It lacks an explicit when-not-to-use statement, but the operating context is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_replies_to_answerReplies Needing An AnswerA
Read-only
Inspect

Contacts who REPLIED and are awaiting a human/Claude answer (auto follow-ups already stopped). DB-backed and instant.

WORK THE WHOLE QUEUE IN THREE CALLS: ask for it with includeThread=true and each row arrives with the end of its stored conversation already attached, so there is no per-person read at all; draft every answer; send them together with li_send_reply's replies array. Without includeThread this is the queue alone and each person costs a second call to li_conversation_history.

THE QUEUE IS DERIVED FROM THE CONVERSATION, so a person leaves it by being answered — not by being marked. It drops anyone whose newest message from us is newer than their last word, which means a reply sent with li_send_reply clears the row BY ITSELF, but only once the send actually lands (it is queued, so that is minutes, not seconds) and only when it went out from the account on the row's own accountId — which li_send_reply now routes to for you, so you can hand this queue straight to it without grouping. Until then they are still here: read li_job_result, never re-send.

⚠️ THE ROW LEAVING THIS QUEUE IS NOT THE SAME AS THE CONVERSATION BEING CLOSED. Answering writes no status anywhere, so the contact still reads "needs reply" on their dossier and on the campaign, and the reply rate still counts them. li_mark_reply_handled is what records that a person dealt with it — call it after you answer, and call it for a conversation you are simply not going to answer.

⚠️ DO NOT use it to put somebody down for a while — it closes the conversation with no date to come back on. li_snooze_reply is that. Either way, a NEW message from them after the close or the snooze reopens it: the row comes back here carrying reopened (with what it had been marked, which is kept).

An answer somebody typed BY HAND in LinkedIn needs none of that: the inbox sweep (about hourly) notices a thread we already owed that now ends on our own message, stores it, and the row leaves this queue by itself. It runs only about hourly, so it can lag — force it with li_sweep_inbox (no selector sweeps every account) rather than reading threads one by one to check.

Each row carries WHO THEY ARE — name, headline, role, company and location, filled from research already bought for that person, so drafting needs no li_contact_dossier call. Plus campaignName (which sequence they are in, so you answer in the right offer's voice) and the two ends of the stored thread, theirLastAt and ourLastAt — their last word four days ago with nothing from us since is somebody genuinely waiting. Both null means this thread has never been read; theirLastAt alone can be the moment their reply was DETECTED when the thread itself was never stored. A row never shows our last message newer than theirs.

⚠️ An attached thread is the STORED one, so it can lag and it can be empty — empty means nobody has read that thread yet, NEVER that they did not write. For those, li_read_conversation fetches it live (a browser session, minutes) and stores it for next time. Record how it went afterwards with li_mark_outcome.

A row flagged bookingPending: true is somebody the booking link went to or who agreed to a call, and who has WRITTEN BACK since — answer what they said; they are deliberately NOT in li_booking_due, and a booking nudge on top of their message is refused. 🚨 Rows with bookingCheckRequired: true (our link is out, or a call is booked) need a CALENDAR SEARCH for that person before you write anything: people book within minutes of saying yes, and this server reads no calendar. Booked → send nothing, li_mark_booked. Not booked → answer, and li_send_reply with calendarChecked: true (refused without it). bookingHint says which. booking carries the state when one is tracked, and companyDomain their employer's web domain.

NameRequiredDescriptionDefault
limitNoMost rows to return (default 100); `truncated` says when there are more
sessionIdNoLimit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account.
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account.
includeThreadNoAttach the end of each stored conversation to its row, so drafting needs no second call per person. Off by default because it is the difference between a queue and a queue plus every thread in it; threads are attached to at most the first 25 rows and the reply says when it stopped.
sessionLabelsNoSeveral accounts in ONE call, by label. Leaving every selector off already spans all your accounts, which is usually what you want; this is for "these three of my six". Each label must match exactly one of your accounts — a name shared by two is refused rather than guessed.
threadMessagesNoHow many of the most recent messages to attach per thread (default 6). Still returned oldest-first. Message text is never cut — a truncated question is the one thing that would make the answer wrong.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare readOnly/non-destructive, but the description discloses much more: the queue is derived from the conversation, rows clear only once a queued send actually lands (minutes, not seconds) and only when sent from the row's own accountId, and a `reopened` flag returns with prior marker state. It also covers the booking edge cases (bookingPending, bookingCheckRequired, calendarChecked requirement) and thread-staleness caveats. This is unusually rich behavioral context beyond the structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

It is long, but the first sentence front-loads the core identity and the three-call workflow follows immediately, so structure is sound. Every paragraph carries actionable instruction rather than filler, though the density of repeated ⚠️ warnings means a reader must parse carefully to extract the ordered steps.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description must carry the return contract, and it does: it enumerates row fields (name, headline, role, company, location, campaignName, theirLastAt/ourLastAt, bookingPending, bookingCheckRequired, bookingHint, booking, companyDomain, reopened). Combined with the workflow guidance, an agent has everything needed to call and consume the result correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: it explains why includeThread trades cost for eliminating per-person calls (and the 25-row thread cap), what threadMessages attaches, and how sessionLabels selects a subset of accounts with an exact-match rule. It stops short of fully documenting every selector's default behavior, but goes well past the schema's own text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening line states a precise verb+resource and scope: contacts who REPLIED and are awaiting a human answer, with the clarifying parenthetical that auto follow-ups already stopped. It cleanly distinguishes itself from the nearest siblings (li_booking_due, li_due_messages, li_marked_conversations) by explaining what membership in the queue means. An agent can identify the tool's output without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit operational routing: use includeThread to avoid per-person reads, send via li_send_reply's `replies` array, never use this to park someone (use li_snooze_reply), and force refresh with li_sweep_inbox instead of reading threads one by one. It also names the follow-up call li_mark_reply_handled and the pre-write calendar search for bookingCheckRequired rows. Alternatives and exclusions are spelled out rather than inferred.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_research_profilesResearch a List of People in One CallAInspect

Everything needed to judge and personalise a list of up to 500 people, merged into ONE compact row per person in your order: pipeline (every campaign they are already in on the accounts you can see, the same memberships li_check_targets reports: account, campaign, campaign status, connection and message status, outcome, hasInbound), profile (name, headline, role, company, location, timezone), company (their current employer from its LinkedIn company page: name, domain, industry, size) and posts (latest posts: url, postedAt, isRepost, reactable, a 160-character textPreview). Default include: pipeline, profile, company; posts are only read when asked for. COST: the same lookups as li_enrich_profiles and li_fetch_posts, from the same shared pool: 1 per profile bought (the employer comes free with it), 1 per person whose posts are read. FREE: a stored profile of ANY age for someone already in your pipeline, one under 30 days for anyone else, and posts read in the last 3 days; refresh=true buys everything again. Each row's profile.status says bought or reused, with fetchedAt, and credits totals what was charged. reactable: false marks a post the engage-before-invite step cannot use (li_ingest_prospects would refuse it as engagePostUrl). LONG LISTS: the call waits about 20 seconds; a run still going comes back as a jobId, read with li_job_result({ jobId, waitMs, offset }), 50 rows a page with nextOffset. A run cut off by a server restart reads interrupted: run it again, and what was already bought comes back free. pipeline.unreadable, and a status of failed or pending, mean NOT KNOWN, never "none". A URL that is not a profile (invalid_url) or is only a colleague's prospect (refused) is skipped and costs nothing. estimateOnly=true previews the charge for free. Opens no browser.

NameRequiredDescriptionDefault
includeNoWhich sections to return: pipeline (free), profile, company (bought together: 1 lookup per person not stored), posts (1 more lookup per person not read recently). Default: pipeline, profile, company.
refreshNoBuy every profile and read every person's posts again, ignoring what is stored. Costs a lookup per person per paid section.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
profileUrlsYesLinkedIn profile URLs, up to 500. Duplicates (two spellings of one person) are researched once. Only these URLs reach the data provider.
estimateOnlyNoPreview: how many profiles are stored (free), how many would be bought, how many posts would be read, and what that charges. Reads your pipeline, looks nothing up, charges nothing.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
postsSinceDaysNoWith posts: only posts from the last N days. Costs the same; a person with nothing that recent reads no_posts. Default: no age limit.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (readOnlyHint=false, openWorldHint=true): it discloses the credit model (1 per profile bought, 1 per posts read), free reuse windows (any age in-pipeline, 30 days otherwise, 3 days for posts), the bought/reused status and fetchedAt fields, long-run jobId/interrupted behaviour, and error semantics (unreadable=NOT KNOWN, invalid_url/refused skipped free). This is unusually rich behavioural disclosure consistent with readOnlyHint=false.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the outcome (one compact row per person) before cost and edge cases, and each sentence carries operational weight. It is dense and long for a single paragraph, with some overlap between the cost sentences and the schema's own coverage, which keeps it from a perfect score.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description fully compensates: it describes each row section, the status/fetchedAt/credits fields, jobId pagination via li_job_result, and every documented failure mode. An agent has everything needed to call and interpret results correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3, but the prose adds cross-parameter meaning the schema lacks: the coupling of include/refresh/postsSinceDays to distinct charges, and the fact that posts are read only on request. Most individual parameter mechanics are duplicated from the schema, so it stops short of a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource+scope: 'Everything needed to judge and personalise a list of up to 500 people, merged into ONE compact row per person.' It enumerates the exact sections returned and explicitly names the sibling tools it mirrors (li_enrich_profiles, li_fetch_posts), so an agent can distinguish it from them without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear context: default include is pipeline/profile/company, posts only when asked, estimateOnly previews for free, and refresh matters only when you want to re-buy. However, it never states the routing rule explicitly (e.g. 'use this instead of li_enrich_profiles when you also need pipeline/posts'), leaving the agent to infer selection from the cost comparison.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_reset_contactReset A Dead Sequence (recover failed/handed-off)A
Destructive
Inspect

Put a contact whose automation died back into the sequence. A failed/handed_off message on an accepted connection revives at the first unsent step (reply-check still guards follow-ups); a failed invite resets to not_sent and is queued again (caps and sending hours re-apply). Refuses invited/declined/withdrawn connections. Use after an outage killed sequences, or to undo a mistaken li_mark_stopped / li_mark_outcome — only once whatever blocked the send is fixed, or it fails again. Pass campaignId when the person is in more than one campaign. Writes our database only; the drip does the sending. Moving someone between campaigns is li_move_contact.

NameRequiredDescriptionDefault
contactIdNoThe contact id from li_contact_dossier or li_campaign_contacts
campaignIdNoWhich campaign membership to reset. Load-bearing here: this re-queues a REAL invite or send, from the account that row's campaign belongs to, so a contact in two campaigns must not be resolved by whichever was touched last.
profileUrlNoThe contact, by LinkedIn profile URL (or use contactId)

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag destructive/open-world writes, but the description adds substantive behavior beyond them: refusal for invited/declined/withdrawn connections, per-state reset semantics (revive at first unsent step vs. reset to not_sent and re-queue), caps and sending hours re-applying, and the fact it only writes the database.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action and scope, and every clause carries information (refusal cases, prerequisites, param hint, sibling pointer). Dense with parentheticals, but no sentence is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, yet the description covers preconditions, failure modes, refusal states, scoping, and side effects — everything an agent needs to invoke this destructive mutation safely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real guidance the schema does not: pass campaignId when the contact is in multiple campaigns. That closes the ambiguity for the highest-risk parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource with scope: reviving a dead automation for a contact. It explicitly separates itself from siblings by naming li_move_contact and li_mark_stopped, so an agent can pick it without opening another schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use triggers (after an outage killed sequences, to undo a mistaken li_mark_stopped/li_mark_outcome) plus a precondition (only once the blocking send issue is fixed) and a routing alternative (li_move_contact for campaign changes).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_resolve_profile_urlResolve LinkedIn Profile URLsAInspect

Turn any spelling of a person's LinkedIn address into their public profile URL plus both ids. LinkedIn names one person three ways: the public vanity URL (linkedin.com/in/), the member id (/in/ACoAA…) and the search id li_search_leads returns (/in/ACwAA…); both ids are case-sensitive. Returns, per input: publicProfileUrl, memberId, searchId, status (resolved | unresolved | invalid) and source (input = already public, cache = known, free; provider = a lookup from the shared enrichment allowance, 1 per person; browser = the account opened the profile and read where LinkedIn redirected it). A vanity URL is never looked up. Read-only: it writes no contact and sends nothing. You rarely need it: every tool that takes a profile URL resolves search ids itself (reported as searchIds). Use it to see who a search id is before deciding, or to clean a list. error on an unresolved row says which step failed and what to do. Always relay message.

NameRequiredDescriptionDefault
sessionIdNoOnly with several accounts: whose allowance and browser are used. Account id from li_list_accounts.
useBrowserNoOpen what the provider cannot resolve on the account's browser (default true). Opening a profile is visible to that person as a profile view.
profileUrlsYesProfile addresses in any form: vanity URL, /in/ACoAA… or /in/ACwAA…
sessionLabelNoSame as sessionId, by account label, LinkedIn name, profile slug or member id.

TDQS

A4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explicitly declares 'Read-only: it writes no contact and sends nothing', but the annotations set readOnlyHint=false. The tool is in fact not purely read-only: the browser step opens the target's profile (visible to that person as a profile view) and provider lookups spend the shared enrichment allowance, so the flat 'Read-only' claim contradicts the hint. It does otherwise disclose a lot (cost per lookup, source vocabulary, status values, error/message semantics), which keeps this from being a total misdirection.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core purpose and then layers id forms, sources, cost, and usage routing. It is long and parenthetically dense, but nearly every clause carries operational information rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description enumerates the returned fields (publicProfileUrl, memberId, searchId, status, source), explains each status and source value, and tells the agent how to read error/message on unresolved rows. An agent has everything needed to call and interpret it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the four parameters are already documented and the baseline is 3. The description adds a little meaning (accepted input forms, that both ids are case-sensitive, that a vanity URL is never looked up) but says nothing new about sessionId/sessionLabel/useBrowser beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource (turn any spelling of a person's LinkedIn address into the public profile URL plus both ids) and explains the three id forms it reconciles. It also names the sibling it interacts with (li_search_leads, whose search ids it resolves) and why other tools generally don't need it, so an agent can place it precisely.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-not-to-use ('You rarely need it: every tool that takes a profile URL resolves search ids itself') followed by the two cases where it IS appropriate ('see who a search id is before deciding, or to clean a list'). The alternative behavior is stated rather than left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_resume_campaignResume a Paused CampaignA
DestructiveIdempotent
Inspect

Start a paused campaign sending again. ⚠️ THIS IS THE LOADED ONE, not launching: a campaign that was launched before restarts its drip on its own, with nobody pressing anything — so resume only after whatever caused the pause is actually sorted out, and read the count of people still waiting for an invite before you do. A campaign that has never been launched becomes active but sends nothing until li_launch_campaign is called.

MANY AT ONCE: campaignIds (up to 100) or namePrefix (+ sessionLabel for one account) instead of campaignId. Without confirm:true it only lists what matched, with each one's people still waiting (notStartedYet); confirmed, each is resumed with its own ownership check and result line. A namePrefix resume also needs expectCount (the number the preview listed): without it the call only previews, and a different count refuses.

NameRequiredDescriptionDefault
confirmNoBulk forms only: true to act. Without it the call lists what matched.
sessionIdNoWith namePrefix only: match on this account only
campaignIdNoOne campaign id from li_list_campaigns. Or campaignIds / namePrefix.
namePrefixNoBulk: every campaign you can see whose name starts with this (case-insensitive, the same match as li_list_campaigns), at most 100; listed before anything is resumed
campaignIdsNoBulk: up to 100 campaign ids, each resumed as the single form would be
expectCountNoWith namePrefix only: the number of campaigns the preview listed. Required to confirm, so nothing is resumed that the preview did not show.
sessionLabelNoWith namePrefix only: match on this account only

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover write/destructive/idempotent/openWorld, but the description adds substantial context beyond them: a resumed unlaunched campaign becomes active yet silent, resume restarts an existing drip unaided, each resumed campaign gets its own ownership check, and namePrefix resume requires expectCount or it only previews. This is exactly the kind of behavioral disclosure the annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The warning is front-loaded and the description is split into a single-campaign section and a clearly labelled MANY AT ONCE bulk section. Dense but nearly every clause carries a distinct operational fact; only slight redundancy in the repeated 'instead of campaignId' phrasing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 7-parameter mutation tool with no output schema, the description covers triggering conditions, the bulk preview/confirm protocol, ownership checking, and the launch/resume distinction. An agent has everything needed to invoke it safely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds the rationale behind parameters rather than restating them: confirm gates action vs preview, expectCount prevents resuming anything the preview did not show, and sessionLabel/sessionId scope namePrefix to one account. It meaningfully enriches the parameter model beyond the schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Start a paused campaign sending again') and explicitly distinguishes itself from li_launch_campaign ('A campaign that has never been launched becomes active but sends nothing until li_launch_campaign is called'). An agent can tell it apart from li_pause_campaign and li_launch_campaign without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit precondition for use ('resume only after whatever caused the pause is actually sorted out'), names the alternative (li_launch_campaign) and its condition, and explains the bulk preview-then-confirm workflow including the expectCount refusal guard. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_scheduled_postsList Scheduled PostsA
Read-only
Inspect

List scheduled posts — queued and recently published — latest scheduled time first, each with its id, account, text, scheduledAt and status (scheduled | posted | failed | cancelled). A failed one carries the reason — a scheduled post that did not go out is otherwise invisible. DB-backed and instant; up to limit rows (default 50). Cancel one with li_cancel_scheduled_post; queue one with li_schedule_post; publish immediately with li_create_post.

NameRequiredDescriptionDefault
limitNoMost posts to return, newest scheduled time first (default 50, max 200)
sessionIdNoLimit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account.
sessionLabelNoLimit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish readOnlyHint=true, destructiveHint=false, and openWorldHint=false, so safety is covered. The description adds genuinely useful context beyond that: DB-backed and instant (latency), the status enum including failed, and the rationale that a failed post would otherwise be invisible. It stops short of describing pagination or result shape in detail.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded in the first clause, and every subsequent sentence carries load: ordering, returned fields, status values, failure visibility, latency, row cap, and sibling routing. No filler or repetition of the title.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema present, the description compensates by enumerating the returned fields and status vocabulary, and by disclosing ordering and result limits. An agent has everything needed to decide when to call it and how to interpret the response.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the schema already documents limit semantics (default 50, max 200) and the sessionId/sessionLabel account-scoping requirement. The description restates the limit and the returned 'account' field but adds no syntax or format detail beyond the schema, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (List) and resource (scheduled posts), plus the scope (queued and recently published), ordering (latest scheduled time first), and the returned fields (id, account, text, scheduledAt, status). It is readily distinguishable from li_cancel_scheduled_post, li_schedule_post, and li_create_post, which are named explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Directly routes the agent: cancel with li_cancel_scheduled_post, queue with li_schedule_post, publish immediately with li_create_post. Each alternative is paired with the action it performs, so the when-to-use decision requires no inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_scheduled_stepsWhat Is About To Be Sent, To Whom, And WhenA
Read-only
Inspect

Every message step that has a send time on it, soonest first — the queue of what is actually about to go out.

No other read answers this. li_due_messages is what is already LATE, li_accepted_pending_message is an acceptance with no opener yet, and li_job_result lists JOBS — so it can say a send is queued without saying who it is to. This lists PEOPLE with times: stepId, kind, position, the full text, scheduledAt, originalDueAt, deferredSendAt, prospectTimezone, prospectLocalOriginalDue, prospectLocalDeferredSend, the campaign and the account that will really send it.

⚠️ IT DOES NOT FILTER ON CAMPAIGN STATUS, deliberately, and that is the most useful thing about it: PAUSING A CAMPAIGN DOES NOT STOP A STEP THAT IS ALREADY SCHEDULED. The worker never looks at the campaign when it sends a step, so a paused campaign's queued messages still leave. Each row carries campaignStatus so a paused one shows up as context rather than as an absence — and to actually stop those messages, hold them with li_cancel_steps.

A step with NO send time is not here: a later step's time is set when the one before it goes out, so those are sequence positions rather than messages waiting — thousands of them, and they would bury the sixty that are real.

Pass withinHours for a window (withinHours=24 is "what goes out today"), and campaignId to see one campaign's queue on its own — that filter goes into the QUERY, so the soonest steps it returns are the soonest steps OF THAT CAMPAIGN rather than whatever survived a page cut somewhere else. Both narrow on top of the account scope, never instead of it.

requeue is the last time this message was put back in the queue, or null when nothing has. Take the stepId straight to li_cancel_steps or li_edit_step.

NameRequiredDescriptionDefault
limitNoHow many steps to return, soonest first (default 50, max 200).
sessionIdNoScope to one sending account id
campaignIdNoOne campaign only — "what is this campaign about to send". Narrows the QUERY on top of the account scope, so the page is the soonest steps OF THIS CAMPAIGN. From li_list_campaigns.
withinHoursNoOnly steps due within this many hours — "what is going out today" is withinHours=24. Omitted returns the soonest ones whenever they are due.
sessionLabelNoScope to one sending account by label

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover safety (readOnlyHint=true, destructiveHint=false, openWorldHint=false), and the description adds non-obvious operational behavior: the worker ignores campaign status when sending, so pausing a campaign does not stop already-scheduled steps, and each row carries campaignStatus as context. It also names the remedy (li_cancel_steps) and enumerates returned fields, which matters since there is no output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core definition and the sibling disambiguation before the caveats. It is dense and every block carries information, but the campaign-status caveat is stated twice and the parenthetical asides plus ALL-CAPS/emoji add length that could be trimmed without losing signal.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description correctly carries the return contract, listing stepId, kind, position, full text, scheduledAt, originalDueAt, deferredSendAt, prospectTimezone, prospectLocalOriginalDue, prospectLocalDeferredSend, campaign, account and campaignStatus. Combined with the filter semantics and the deliberate non-filtering on campaign status, an agent has everything needed to call and interpret it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3 and the parameter docs largely restate the schema. The description still adds relational meaning the per-parameter text does not: both filters narrow on top of the account scope rather than replacing it, and the campaignId filter is applied in the QUERY so paging cannot distort which steps come back.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource plus scope and ordering: 'Every message step that has a send time on it, soonest first — the queue of what is actually about to go out.' It then explicitly differentiates from three siblings (li_due_messages = LATE, li_accepted_pending_message = acceptance with no opener, li_job_result = JOBS), so an agent can route correctly without opening other schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use (withinHours=24 = 'what goes out today'; campaignId = one campaign's queue), when-not (does NOT filter on campaign status, by design), what is excluded (steps with no send time are sequence positions, not pending messages), and a concrete next action (take stepId to li_cancel_steps or li_edit_step). Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_schedule_postSchedule Feed PostAInspect

Queue a post for a specific future time, visible and cancellable until it goes out. Use this when a HUMAN has chosen the moment ("publish this Thursday 09:00"); for "post something today if there is something worth saying", use li_create_post at the moment you decide. The text is stored exactly as given and never regenerated at send time. scheduledAt is stored as provided and is NEVER silently moved into the account's activity window — a time outside it returns a warning instead, since a human may be deliberately targeting another timezone. When the account cannot post RIGHT NOW (paused, signed out or blocked by LinkedIn, plan ended) the post is still queued but the reply carries accountHold { kind, message } with the link that fixes it: tell the user, because the post fails at its time unless that is fixed first. Never move it to another account.

NameRequiredDescriptionDefault
textYesExact post text (max 3000 chars)
imageUrlNoPublic http(s) image URL to attach
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
scheduledAtYesISO-8601 with offset, e.g. 2026-09-03T09:00:00+05:30. Must be in the future.
sessionLabelNoWhich account posts. Required when more than one account is active.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (which only say readOnly=false, destructive=false, openWorld=true) by disclosing that text is stored exactly and never regenerated at send time, that scheduledAt is never silently normalized into the activity window and instead returns a warning, and that a paused/signed-out/blocked/expired account still queues the post but returns `accountHold { kind, message }` that must be surfaced because the post will fail at its scheduled time.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The core action and its cancellation window are front-loaded in the opening clause, and each subsequent sentence carries a distinct operational fact. It is on the long side for a five-parameter tool, but there is little filler to cut other than mild repetition around account handling.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description usefully characterizes the reply (the `accountHold` object and its kind/message shape) and the deferred-failure behavior, and it covers the account-selection constraint. It does not mention whether the response returns an identifier for later cancellation via li_cancel_scheduled_post, which is the one thing an agent would still need to infer.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is already 100%, so the baseline is 3, but the description adds real semantics the schema lacks: text is stored verbatim with no regeneration, and scheduledAt is honored exactly even outside the activity window rather than being adjusted. It also implies sessionId/sessionLabel should target the same account rather than being moved.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope: 'Queue a post for a specific future time', and immediately adds the lifecycle boundary ('visible and cancellable until it goes out'). This distinguishes it from li_create_post (publish now) and li_cancel_scheduled_post without needing to open any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use rule with a quoted trigger ('publish this Thursday 09:00' when a HUMAN has chosen the moment) and names the alternative with its own condition ('post something today if there is something worth saying' → li_create_post at the moment you decide). It also states the counterfactual constraint 'Never move it to another account.'

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_search_leadsFind LinkedIn Leads That Fit an ICPAInspect

Find prospects who fit an ICP: give LinkedIn search filters (job titles, seniority, function, location, industry, company size, companies, schools, keywords and their exclusions) and get back a list of people with profile URL, name, location, current title and company, a short "about", premium and Open Profile flags. No browser and no LinkedIn account is used; this reads public search results from a third-party provider. The results are scraped public data, not a verdict: deciding who actually fits, and why, is still your job.

COST: 25 enrichment lookups per search PAGE that returns anyone (pages hold up to 25 people; a page of 3 costs the same as a page of 25), from the same shared pool as li_enrich_profiles. A search that finds nobody costs nothing. maxResults is rounded UP to whole pages and the whole window is reserved first. Pass estimateOnly=true first for a free preview of the most it would charge and what is left. At most 100 per call; continue with startPage (nextStartPage in the reply). LinkedIn shows at most 100 pages (2500 people) for one search, so narrow the filters to reach past that.

BATCH: put every company, title or place you want in ONE call: each list field matches ANY of its entries. Never loop one company per call: each call is charged a whole page however few people it finds, so per-company calls spend several times the lookups for the same people.

Each person is marked against YOUR team: alreadyContact (in your contacts, with status and campaigns) and doNotContact (on your team's or the install's do-not-contact list). Each is an object (yes), false (checked, not found) or null (could not check). A false is only "not under this URL": search results name people by LinkedIn's search id (/in/ACwAA…, case-sensitive), not their public profile URL, so someone you hold under their vanity URL can still read false here. truncated true means more people match than this call returned. unreadable counts rows the provider sent that could not be read; they are left out and never charged. Always relay message.

Each lead carries publicProfileUrl when the person was looked up before (free), else null. Pass the search-id profileUrl to any tool as it is: every tool that takes a profile URL resolves it to the public profile URL first (cache, then a provider lookup from the same pool, then the account's browser) and reports how in searchIds; imports store the vanity URL with the member id. li_resolve_profile_url does the same resolution on its own, read-only.

Siblings: li_enrich_profiles for the full profile of the people you pick (headline, experience, employer page; each row also carries publicProfileUrl, memberId and searchId); li_fetch_posts for their latest posts to write a personal opener; li_check_targets to see who is already in your pipeline; li_ingest_prospects to import the ones you choose with their prefilled copy.

NameRequiredDescriptionDefault
schoolsNoSchools they attended, by name, e.g. ["Stanford University"]. Up to 50.
keywordsNoFree text LinkedIn matches anywhere on the profile, e.g. "fintech" or "B2B SaaS". LinkedIn search operators work (AND, OR, NOT, quotes).
functionsNoLinkedIn job functions: accounting, administrative, arts_and_design, business_development, community_and_social_services, consulting, education, engineering, entrepreneurship, finance, healthcare_services, human_resources, information_technology, legal, marketing, media_and_communication, military_and_protective_services, operations, product_management, program_and_project_management, purchasing, quality_assurance, real_estate, research, sales, customer_success_and_support.
locationsNoWhere the person lives, as place names LinkedIn resolves itself, e.g. ["Pune", "United Kingdom", "San Francisco Bay Area"]. Spell countries out: LinkedIn reads "UK" as Ukraine. Up to 70.
seniorityNoLinkedIn seniority levels: in_training, entry_level, senior, strategic, entry_level_manager, experienced_manager, director, vice_president, cxo, owner_partner. cxo is C-level; owner_partner is founders, owners and partners.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
startPageNoThe LinkedIn results page to start on, default 1. To continue a search, pass the previous reply's nextStartPage with the SAME filters. LinkedIn stops at page 100.
maxResultsNoHow many people to return, default 25, at most 100. Rounded UP to whole pages of 25; each page that returns anyone is charged whole (25 lookups), and a search that finds nobody costs nothing.
industryIdsNoLinkedIn industry IDS (numbers as strings), not names. Common ones: 4 Software Development, 6 Technology, Information and Internet, 11 Business Consulting and Services, 14 Hospitals and Health Care, 25 Manufacturing, 27 Retail, 43 Financial Services, 44 Real Estate, 80 Advertising Services, 96 IT Services and IT Consulting, 104 Staffing and Recruiting, 1862 Marketing Services. Full list: https://github.com/HarvestAPI/linkedin-industry-codes-v2/blob/main/linkedin_industry_code_v2_all_eng_with_header.csv. Up to 50.
estimateOnlyNoPreview instead of searching: how many pages, the most lookups it could charge and what is left. Searches nothing, charges nothing and reserves nothing.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
pastCompaniesNoPeople who USED to work at these companies (LinkedIn company page URLs). Up to 50.
pastJobTitlesNoPast job titles, as text. Up to 50.
excludeSchoolsNoLeave out people who attended these schools (by name). Up to 50.
companyHeadcountNoEmployee count of their CURRENT employer: self-employed, 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+.
currentCompaniesNoPeople who work at these companies NOW, as LinkedIn company page URLs (https://www.linkedin.com/company/<name>/). A company name alone is refused. Up to 50, matched as ANY of them: put every target company in this one call, never one call per company.
currentJobTitlesNoCurrent job titles, as text LinkedIn matches, e.g. ["Founder", "Head of Growth"]. Up to 50.
excludeFunctionsNoLeave out these job functions (same values as functions).
excludeLocationsNoLeave out people living in these places (names, as for locations). Up to 70.
excludeSeniorityNoLeave out these seniority levels (same values as seniority).
profileLanguagesNoLanguage of their LinkedIn profile: Arabic, English, Spanish, Portuguese, Chinese, French, Italian, Russian, German, Dutch, Turkish, Tagalog, Polish, Korean, Japanese, Malay, Norwegian, Danish, Romanian, Swedish, Bahasa Indonesia, Czech.
yearsOfExperienceNoTotal years of experience: under_1, 1_to_2, 3_to_5, 6_to_10, over_10.
companyHqLocationsNoWhere the person's CURRENT employer is headquartered, as place names (same rules as locations). Up to 70.
excludeIndustryIdsNoLeave out these LinkedIn industry ids (numbers as strings, as for industryIds).
recentlyChangedJobsNoOnly people who changed jobs in the last 90 days (a common buying moment).
excludePastCompaniesNoLeave out people who worked at these companies (LinkedIn company page URLs). Up to 50.
excludePastJobTitlesNoLeave out people with any of these PAST titles. Up to 50.
yearsAtCurrentCompanyNoYears at their current employer: under_1, 1_to_2, 3_to_5, 6_to_10, over_10.
excludeCurrentCompaniesNoLeave out people working at these companies now (LinkedIn company page URLs), e.g. your own firm or existing customers. Up to 50.
excludeCurrentJobTitlesNoLeave out people whose CURRENT title matches any of these. Up to 50.
recentlyPostedOnLinkedInNoOnly people who posted on LinkedIn in the last 30 days (active, and li_fetch_posts will find something to open with).
excludeCompanyHqLocationsNoLeave out people whose employer is headquartered in these places. Up to 70.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only say non-read-only, non-destructive, open-world; the description adds the essentials the annotations cannot: no browser or LinkedIn account is used, data is scraped from a third-party provider, 25 lookups are charged per page even if partially filled, windows are reserved up front, empty searches are free, and LinkedIn caps at page 100. It also discloses the tri-state alreadyContact/doNotContact semantics with the URL-mismatch caveat for false, plus truncated and unreadable counters and the requirement to relay `message`.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose, return shape and cost lead, and the section headers (COST, BATCH, Siblings) make it scannable despite its length. Some material is restated — the whole-page charge appears in both COST and BATCH — and the profileUrl resolution paragraph is only tangentially about searching, but nothing is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 32-parameter, credit-spending, open-world search with no output schema, every decision an agent must make is covered: cost and reservation model, pagination continuation, batching, empty-result behavior, the shape and caveats of the contact flags, and where to go next. Nothing needed to call it correctly or safely is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3 and the schema already documents each of the 32 fields. The description still adds real meaning on the non-obvious parameters: maxResults is rounded UP and the whole window reserved, startPage must reuse identical filters, estimateOnly reserves and charges nothing, and list fields match ANY entry so they belong in one call.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence gives a specific verb+resource ('Find prospects who fit an ICP'), then enumerates the filter families accepted and the fields returned (profile URL, name, location, title, company, about, premium flags). It is immediately distinguishable from li_enrich_profiles, li_fetch_posts and li_ingest_prospects, which are explicitly routed to later in the text.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It prescribes concrete sequencing: run estimateOnly=true first for a free preview, then continue with startPage/nextStartPage using the SAME filters. It explicitly says when not to loop (per-company calls) and lists which sibling to use for each follow-on need (enrich the picked people, fetch posts for an opener, check the pipeline, import).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_selector_canaryCheck LinkedIn DOM Drift (operator only)A
Read-only
Inspect

OPERATOR ONLY — refused for anyone but whoever runs this install: it opens a live browser on the account for a few minutes and reports on our own selectors, which nobody but the operator can act on. If sends or reads on your account look broken, call li_account_activity (what is running, queued or held) or li_session_status with verifyLive (whether the LinkedIn sign-in still works). Read-only check that the controls this system depends on still exist AND are still clickable: the feed, the messaging composer and send button, a real contact profile's Message control and its href, a post's reaction control, and the post composer (opened and closed again, nothing typed or posted). Also checks the Voyager conversation read, because selectors are only half the exposure — LinkedIn rotates the messaging queryId and a failed read reports as an EMPTY INBOX rather than an error, which would leave the reply routine answering nobody while the account still looked healthy. It sends nothing. Runs daily on its own; use this to check immediately after a suspected change, or when sends start failing for no clear reason. "found but not hittable" is the important failure — a control covered by the sticky nav accepts no real click, which presents as the action silently doing nothing.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
profileUrlNoA real contact to check the profile controls on. Defaults to a recent accepted connection; without one those checks are SKIPPED and the run reports itself incomplete.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnly/destructive=false, and the description is consistent while adding substantial beyond-annotations context: it opens a live browser for minutes, sends nothing, creates and closes a post composer, and explains the two silent-failure modes (sticky-nav 'found but not hittable', and a rotated messaging queryId surfacing as an EMPTY INBOX). This is unusually rich disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The operator-only restriction and the read-only nature are front-loaded, and the failure-mode explanation is placed before the when-to-use guidance. It is dense but occasionally repetitive (read-only/sends-nothing stated more than once), pushing it below 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the reporting burden and does explain what is checked and that an incomplete run self-reports. It stops short of describing the actual report shape or pagination, but is otherwise complete for an operator diagnostic.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the schema already documents the default-account and default-connection behavior for sessionId/sessionLabel/profileUrl. The description does not add syntax or format meaning beyond that, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource (checks that LinkedIn DOM selectors still exist and are clickable) and enumerates exactly which controls are probed. It also distinguishes itself from li_account_activity and li_session_status by pointing to them as the alternatives for account/session health.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use ('immediately after a suspected change, or when sends start failing for no clear reason'), when-not (operator only, refused for anyone else), and named alternatives with their conditions. Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_send_bulk_messagesSend Bulk Messages (one account)A
Destructive
Inspect

Send many messages from one account in a single run, one at a time in a single browser session, and wait for the run to finish. Supply exact text per recipient — never generated. Dry-run unless confirmSend=true; anyone on the do-not-contact list is refused. Returns a per-recipient outcome. To answer replies use li_send_reply instead: it returns at once and sends each from the account whose conversation it is.

FAST PATH: pass conversationUrn + participantMemberId (both come straight out of li_list_conversations / li_read_conversations_bulk) and the send addresses that thread directly — one navigation instead of three, ~30s per recipient cheaper, and the recipient is verified against the thread's own markup BEFORE typing. Replies should always carry them.

WITHOUT THE PAIR the route is decided at send time, not by you: where this install already holds the person's member id AND a stored conversation with them on this account, the send opens that conversation anyway; otherwise it opens their profile, which is the only route that can start a NEW conversation. The dry run marks those rows thread-fast-path-or-profile rather than guessing — both routes verify the recipient before typing, and which one runs can change between the preview and the send.

Budget ~60s per recipient without the fast path and ~30s with it, against a 30-minute limit; prefer batches of 15 or fewer, because a run that times out cannot tell you who was already messaged.

NameRequiredDescriptionDefault
itemsYesRecipients and their exact text, one per person; batches of 15 or fewer are safest
sessionIdNoSending account id (or use sessionLabel)
confirmSendYesMust be true to send; false returns a dry-run preview
sessionLabelNoSending account label
viaMessagingNoUse the /messaging search flow instead of the per-profile composer. Default FALSE. /messaging finds people by typing a NAME into a typeahead that exposes no slug or URN, so the recipient cannot be verified before sending; the profile composer navigates to /in/<slug>/. Opt in only when the profile composer is blocked.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true and openWorldHint=true, but the description goes well beyond them: dry-run semantics, the do-not-contact refusal, the fact that route selection happens at send time and may differ between preview and send, the 'thread-fast-path-or-profile' dry-run marker, and the warning that a timed-out run cannot report who was already messaged. That is exactly the behavioral disclosure a destructive bulk operation needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but front-loaded and organized: purpose first, then FAST PATH and WITHOUT THE PAIR as scannable headers, then budget guidance. Nearly every sentence carries operational information, though the block is denser than a strict conciseness standard would demand.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive, long-running bulk tool with no output schema, the description covers prerequisites, confirmation, safety refusals, per-recipient timing, the 30-minute ceiling, and partial-failure risk. It only briefly says it 'returns a per-recipient outcome' without describing that payload, which is the one remaining gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds real meaning beyond the schema by explaining the conversationUrn+participantMemberId pairing as a route selector, the consequence of omitting the pair (profile composer, the only route that can start a new conversation), and the rationale for viaMessaging (typeahead exposes no slug, so verification is impossible). It elaborates rather than replaces the schema, so a 4 rather than 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence names a specific verb (send), resource (messages), and scope (many, one account, one browser session, sequential), and explicitly contrasts itself with li_send_reply. An agent can distinguish it from li_send_message and li_send_reply without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing rules: 'To answer replies use li_send_reply instead', dry-run unless confirmSend=true, do-not-contact refusal, and a whole FAST PATH / WITHOUT THE PAIR section explaining when each route is selected. It also gives operational guidance (batches of 15 or fewer, time budget) rather than leaving timing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_send_connection_nowQueue LinkedIn Connection Invites NowA
Destructive
Inspect

Queue LinkedIn connection invites now to the profiles you name, from one of your accounts; the drip then sends them ("now" in the name means queued now, not sent during this call). Returns IMMEDIATELY — it does not hold the call open while the browser works, and nothing is sent during the call itself. The invites join the account's invite-only campaign and the drip sends them during office hours only, under each account's rolling 7-day invite allowance this system enforces, set by the account's detected LinkedIn tier — 63-70 a week (5-10 a day) new or unverified, 90-100 a week (18-24 a day) established, 135-150 a week (25-30 a day) Premium, redrawn within the range each week. Our own policy, not a published LinkedIn limit; the daily band exists only to stop a burst. li_list_accounts gives each account's actual numbers. The reply's plan says in plain words when they will actually go out; relay that to the user rather than implying they have already been sent. Pass profileUrls for several people in one call. No follow-up messages are created — this is invites only; use li_ingest_prospects when the person should also get a sequence. Importing someone later into a campaign with a sequence (same account) MOVES this invite into that campaign instead of inviting again (adoptedFromInviteOnly in that reply). Use only after the user confirms the exact profiles and notes.

NameRequiredDescriptionDefault
noteNoOptional personalized note, used for every profile in this call (clamped to the account tier limit; omit to send note-less)
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
profileUrlNoOne recipient LinkedIn profile URL (or use profileUrls for several). A search id from li_search_leads (/in/ACwAA…) is resolved to the public profile URL first.
confirmSendYesMust be true to queue; false returns a dry-run preview
profileUrlsNoSeveral recipients in one call — far cheaper than one call each
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (readOnlyHint=false, destructiveHint=true, idempotentHint=false): discloses that the call returns immediately without holding the browser, that nothing is sent during the call, that invites join an invite-only campaign drip during office hours, and the exact rolling 7-day/tier allowance bands, noting this is an internal policy rather than a published LinkedIn limit. It also explains the reply's `plan` field and the adoption-not-reinvite behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the key semantics — the "now" meaning and the immediate return — before the tier and adoption details. It is on the long side and restates the not-sent-during-call point several ways, which is mild redundancy, but each sentence carries operational value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a write tool with no output schema and a 100%-documented schema, the description covers auth/account selection, the queuing semantics, tier-based invite limits, and the `plan` reply field an agent must relay. Nothing needed to call it correctly appears to be missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all six parameters including the note clamp and bulk profileUrls efficiency. The description reinforces bulk use ("Pass profileUrls for several people in one call") and the tier clamp on notes, but adds little syntax or format detail beyond what the schema provides. Baseline 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence states a specific verb+resource+scope ("Queue LinkedIn connection invites now to the profiles you name, from one of your accounts") and immediately disambiguates the misleading "now" (queued now, not sent during the call). It also names the sibling it is not (li_ingest_prospects) for the sequence case, so an agent can distinguish it without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use ("Use only after the user confirms the exact profiles and notes"), when-not ("No follow-up messages are created — this is invites only"), and the alternative to prefer when a sequence is also wanted (li_ingest_prospects). It also directs to li_list_accounts for account numbers and explains the adoption behavior when someone is later imported.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_send_messageSend a LinkedIn MessageA
Destructive
Inspect

Send ONE message to one 1st-degree connection (or open profile) and WAIT for the result: the call stays open while the browser opens their profile, types and sends, which can take minutes when other work is queued. To answer replies use li_send_reply instead (returns at once with job ids, takes many people, sends each from the right account); for many prepared messages from one account use li_send_bulk_messages. Dry-run unless confirmSend=true — confirm the exact recipient and text with the user first. Refused for anyone on the do-not-contact list. Returns ok and the worker's result. Never resend after a timeout: check li_job_result or li_conversation_history first. Pair with li_read_conversation to draft a contextual, non-deceptive message.

NameRequiredDescriptionDefault
messageYesExact message text to send (supplied, never generated at send time)
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
profileUrlYesRecipient profile URL (https://www.linkedin.com/in/<slug>); the fsd-id URL from li_list_conversations also resolves
confirmSendYesMust be true to send; false returns a dry-run preview
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
debugArtifactsDirNoIgnored; accepted only so older callers do not fail validation.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the destructive/openWorld/non-idempotent annotations: the call blocks for minutes while the browser types and sends, returns `ok` plus the worker's `result`, is refused for suppressed contacts, and must not be retried after a timeout. This is exactly the operational context annotations cannot convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but front-loaded: the blocking-wait surprise leads, followed by routing, safety preconditions, return shape, and a retry warning. Every sentence carries routing, safety, or failure-handling information; nothing is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still covers the return values (`ok`, worker `result`), timeout ambiguity, refusal cases, and the recommended companion tool (li_read_conversation). Nothing an agent needs to invoke this mutation safely is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3; the schema already documents confirmSend's dry-run behavior and session selection. The description adds only minor extra meaning (recipient eligibility, supplied-not-generated text), so it stays at baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource (send ONE message to one connection) plus scope constraints (1st-degree connection or open profile). It explicitly distinguishes itself from li_send_reply and li_send_bulk_messages, so an agent can route without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Names the alternatives and the exact conditions that select them (replies -> li_send_reply; prepared batch from one account -> li_send_bulk_messages). It also gives prerequisites (confirm recipient/text with user, dry-run unless confirmSend=true) and a hard exclusion (do-not-contact list).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_send_replySend Replies (queued — returns job ids, does not wait)A
Destructive
Inspect

Answer ONE person with profileUrl + message, or up to 25 in a single call with replies. Either way it returns IMMEDIATELY with a job id per recipient instead of holding the call open. This is the tool for answering replies; li_send_message stays the blocking one for when you genuinely need the outcome inline.

Why it returns early: LinkedIn work runs one browser session at a time, so a perfectly healthy send can sit queued for many minutes before it even starts — that is normal, not a fault. A client that gives up waiting reports an error for a message that is about to be delivered, the caller sends again, and the person receives it twice.

A returned job id is NOT a failure and NOT a timeout: the message is queued and the worker will send it. NEVER call this again for the same recipient to "make sure".

⚠️ DO NOT VERIFY BY DEFAULT. Queued means it is going out; polling spends time on news that was never in doubt. Say it is queued and finish. Verify only when somebody ASKS whether it landed, or when something later suggests it did not: the surest sign is a person still in li_replies_to_answer an hour on, because a delivered reply leaves that queue by itself. Then it is ONE call — li_job_result with jobIds set to the ids this returned, plus waitMs if you genuinely need to wait inside that call. One call covers the whole batch however many accounts it spans; stillRunning: 0 is how you know they have all landed.

The array form queues ONE ORDINARY SEND PER PERSON rather than a single bulk run, which is why it cannot time out and why each reply is recoverable on its own. Each reply is delivered in its own browser session addressed to one profile URL, so no part of a batch can reach the wrong person. Every recipient is checked BEFORE anything is queued: if one is refused, nothing at all is queued, so you can drop that row and call again safely.

✅ A BATCH MAY SPAN ACCOUNTS. Leave the account unnamed and every reply is sent from the account whose conversation it actually is — so the queue from li_replies_to_answer can be answered exactly as it came back, in one call, however many accounts it covers. Each queued row reports the accountId that sent it, and the reply carries an accounts summary.

Naming an account with sessionLabel still pins the WHOLE call to it, and any row whose conversation belongs elsewhere is then refused — use that when you mean it. Either way a row that cannot be placed (untracked, or on two of your own accounts) is refused BY NAME and nothing at all is queued, so you can drop it and call again safely. Answering someone from an account they never heard from is worse than answering them late.

NameRequiredDescriptionDefault
messageNoExact message text to send (supplied, never generated at send time)
repliesNoSeveral replies in ONE call, max 25. Each becomes its own queued send with its own job id. Two rows naming the same person are REFUSED — that is two answers to one conversation, and it would arrive as two messages seconds apart.
sessionIdNoSending account id (or use sessionLabel)
profileUrlNoOne recipient, by profile URL (https://www.linkedin.com/in/<slug>); the fsd-id URL from li_list_conversations also resolves. Use `replies` instead for several.
confirmSendYesMust be true to queue the send; false returns a dry-run preview
bookingCloseNoThese messages CLOSE the booking thread for people li_booking_due listed with nextAction "close". Same refusals, plus: only after the nudges are used. On queueing, the booking becomes closed, the verdict no_outcome and any remaining steps are cancelled. Applies to every reply in the call; never together with bookingNudge.
bookingNudgeNoThese messages are BOOKING NUDGES for people li_booking_due listed with nextAction "nudge". Each is REFUSED unless that person agreed (li_track_booking), their check date has arrived, they have nudges left, no nudge went out in the last 20 hours, they have not written since the link/agreement, they are not on the do-not-contact list and the account is not paused. The booking state moves in the same step as the queueing (nudge count +1, next check = send time + the account's close-after days), and outside the account's office hours the send is queued for when they open. With no account named, each person is sent from the account their agreed booking is on (li_booking_due's accountId), even when they are in campaigns on more than one of your accounts. Applies to every reply in the call.
sessionLabelNoSending account label
calendarCheckedNoREQUIRED (true) for anyone with our booking link out (bookingPending / bookingCheckRequired in li_replies_to_answer) or a call already booked: it says you searched the calendar for THIS person just before this call. This server reads no calendar, so a booking made minutes ago is invisible here until li_mark_booked records it. If they booked: send NOTHING (no "grab a slot", no "see you then") and record it with li_mark_booked. Without it such a row is refused and nothing is queued. Implied by bookingNudge / bookingClose (their routine sweeps the calendar first), but search again right before those too. Applies to every reply in the call.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare the safety profile (destructive, non-idempotent, open-world) but say nothing about queueing semantics, atomic batch refusal, account pinning, or double-send risk. The description supplies all of that: why it returns early, that a job id is not a failure, that nothing is queued if any row is refused, and that an unnamed account sends each reply from its own conversation's account.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the decisive fact (returns immediately, job id per recipient) and organized with emphasis markers, but it runs long and repeats itself — the 'a row that cannot be placed is refused BY NAME and nothing at all is queued, so you can drop it and call again safely' idea appears twice in near-identical wording.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, and the description compensates fully: it explains what comes back (a job id per recipient, per-row accountId, an `accounts` summary), how to poll for completion, and the edge cases (refusals, account mismatch, untracked recipients). Nothing an agent needs to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is already 100%, so the baseline is 3, but the description adds real meaning beyond the schema: sessionLabel pins the whole call (and rows belonging elsewhere are refused), bookingNudge/bookingClose/calendarChecked apply to every reply in the call, and the replies array is one ordinary send per person rather than a bulk run.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (answer/send replies), the two input shapes (one person via profileUrl+message, up to 25 via `replies`), and the defining behavior (returns immediately with a job id per recipient). It explicitly distinguishes itself from the blocking sibling li_send_message, so an agent can route without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use versus li_send_message (blocking vs queued), explicit when-NOT-to-verify ('DO NOT VERIFY BY DEFAULT', verify only when asked or when someone lingers in li_replies_to_answer), and the exact recovery path (one li_job_result call with jobIds, stillRunning:0). Alternatives and exclusions are all named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_session_statusLinkedIn Session StatusA
Read-only
Inspect

List your saved LinkedIn sessions (label, status, account tier, warm-up state) and, with verifyLive=true, check one account's sign-in in the worker browser — that queues behind other browser work and comes back as liveCheck; verifyLive=false is instant and DB-only. Use li_list_accounts for each account's full configuration (sending hours, plan, enrichment pool), li_account_activity for what it is doing now, and li_verify_account to confirm a reconnect (it records the result and clears the checkpoint breaker). Never exposes li_at/JSESSIONID cookies or storage state.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
verifyLiveYesOpen a browser and verify that the selected session reaches the LinkedIn feed
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnlyHint, destructiveHint=false), but the description adds substantial behavior beyond them: the live check queues behind other browser work, returns a `liveCheck` field, and the false path is instant and DB-only. It also discloses a security boundary (never exposes li_at/JSESSIONID cookies or storage state).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core listing behavior, then the mode tradeoff, then routing, then the security note. Every clause carries information, though the parenthetical field list and multiple sibling callouts make it dense.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, yet the description names the returned field (`liveCheck`) and describes latency/queueing behavior, plus alternatives and the security boundary. An agent has everything needed to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real semantics: it explains that verifyLive=true performs a browser sign-in check that queues and returns `liveCheck`, while false is DB-only, and it clarifies the default-account fallback for sessionId/sessionLabel.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('List your saved LinkedIn sessions') and enumerates the returned fields (label, status, account tier, warm-up state), then describes the conditional verifyLive behavior. An agent can distinguish it from every sibling without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes to alternatives with conditions: li_list_accounts for full config, li_account_activity for current activity, li_verify_account to confirm a reconnect. It also states when to use each mode (verifyLive=true vs false) and the tradeoff between them.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_account_complimentaryGive an Account Complimentary Access (operator only)AInspect

OPERATOR ONLY. Grants (complimentary: true) or ends (false) COMPLIMENTARY access on ONE LinkedIn account — the same control as the "Complimentary" plan on the account's Plan tab. A complimentary account has every paid ability with no payment and no end date: it is always entitled, carries the paid lookup allowance, is never billed (the subscription's paid seats and the seat minimum count only the other accounts) but still COUNTS toward the team's account limit, and is never sent a trial-end, renewal or subscribe notice; the Razorpay webhook never changes it. To make a whole customer free use li_set_team_complimentary; an account of a free team cannot be ended here until the team's Free is turned off. reason is stored as the plan note (who/why) — give one. Ending it moves the account to what its dates say: still paid through a future date → active, a trial not yet over → trial, otherwise past due from now (new invites stop, follow-ups finish in the 14-day grace, and the team is asked to subscribe). Name the account explicitly by sessionLabel or sessionId; it never falls back to a default, and a name matching two accounts is refused. Current state is plan.complimentary in li_list_accounts.

NameRequiredDescriptionDefault
reasonNoWhy — stored on the account as the plan note, e.g. "internal account"
sessionIdNoAccount id (or use sessionLabel)
sessionLabelNoAccount label, LinkedIn name or profile slug, e.g. "Deven"
complimentaryYestrue = grant complimentary access, false = end it

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only supply readOnly=false, openWorld=false, destructive=false; the description goes far beyond by disclosing the full entitlement model on grant (always entitled, paid lookup allowance, never billed, seats counted only for other accounts, still counts toward team limit, no trial-end/renewal notices, Razorpay webhook never mutates it) and the exact state transition on end (active / trial / past due with 14-day grace and new invites stopping). This is exactly the behavioral context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with OPERATOR ONLY and the toggle semantics, then the entitlement consequences, then the alternative tool, then the naming rules — a sensible priority order. It is a dense single block (~150 words) and could be broken into bullets, but nearly every clause carries non-obvious operational meaning rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 4-parameter mutation tool with no output schema, the description covers granting, revoking, side effects, prerequisites, discovery of current state (plan.complimentary in li_list_accounts), and the sibling alternative route. An agent has everything needed to call it correctly and predict the outcome.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds semantics the schema does not: `reason` is stored as the plan note and should always be supplied, `complimentary` is a grant/end toggle, and account naming via sessionLabel/sessionId never falls back to a default and is refused if a name matches two accounts. That naming-ambiguity rule is genuinely beyond the schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (grants/ends), a specific resource (COMPLIMENTARY access), and the exact scope (ONE LinkedIn account), then anchors it to a concrete UI equivalent (the 'Complimentary' plan on the account's Plan tab). It explicitly contrasts with li_set_team_complimentary, so an agent can distinguish it from the nearest sibling without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Names when to use it, when not to (use li_set_team_complimentary to make a whole customer free), and a concrete precondition ('an account of a free team cannot be ended here until the team's Free is turned off'). The OPERATOR ONLY constraint is front-loaded as the first words, so the agent knows the access requirement before deciding.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_account_proxyAssign an Account’s Proxy (operator only)A
Destructive
Inspect

OPERATOR ONLY. Points ONE account at a proxy from li_proxies, or back at the install default with proxyId: null. ⚠️ This changes the IP LinkedIn sees the account on — a fingerprint jump that can trigger a security check or a logout — so a change that moves the IP is REFUSED unless confirmEgressChange: true; the refusal names the old and new address. Confirm only when the person has agreed to the move. Check the proxy with li_check_proxy first. The account's next browser job launches through the new proxy; a warm browser on the old one is closed, not reused. Name the account explicitly — it never falls back to a default.

NameRequiredDescriptionDefault
proxyIdYesA proxy id from li_proxies, or null for the install default
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
confirmEgressChangeNoRequired when the account's egress IP changes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag destructiveHint=true, and the description goes well beyond that: it discloses the egress-IP refusal gate (confirmEgressChange), the security-check/logout risk of a fingerprint jump, that the refusal names old and new addresses, and that the warm browser on the old proxy is closed rather than reused.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but front-loaded: the OPERATOR ONLY gate, then the egress-change rule, then the follow-up behavior. Every sentence carries operational weight, though the length borders on long for a 4-parameter call.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, yet the description covers the outcome (refusal message contents), side effects (browser closed, next job uses new proxy), and the confirmation requirement, leaving nothing an agent needs to call it safely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3, but the description adds real meaning: it explains proxyId null resolves to the install default, that confirmEgressChange is a hard gate tied to IP movement, and that the account must be named explicitly with no default fallback.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource (set an account's proxy) and disambiguates from siblings by scoping it to ONE account and pointing at li_proxies / the install default, while the sibling li_set_user_default_proxy handles the user-level default. An agent can tell them apart without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs to check the proxy with li_check_proxy first, says to confirm only when the person agreed to the move, and states the operator-only precondition. This is when-to-use plus prerequisites plus a named companion tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_auto_withdrawConfigure Auto-Withdraw of Stale InvitesA
Destructive
Inspect

Turn the daily automatic withdrawal of an account's still-pending invites on or off, and set the age at which an invite is withdrawn (7-180 days, default 60). It is OFF for a new account and is turned on deliberately: LinkedIn names invitations that are "ignored, left pending, or marked as spam" as a restriction trigger, so once an account has been sending for a while the pending pile is the risk and this cleanup is the protection — but on day one there is no pile, and the only invitations a sweep could reach are ones the person sent by hand before they ever used this product. Withdrawing does NOT notify the recipient, but you cannot re-invite the same person for ~3 weeks afterwards. Separately, untracked decides whether the sweep also retires invites this system never sent — outreach the person did by hand — which is OFF for new accounts and should only be turned on if they ask. Read the current settings and the last run with li_list_accounts.

NameRequiredDescriptionDefault
enabledNoTurn the daily cleanup on or off
maxPerDayNoCap on how many invites ONE daily sweep withdraws (default 50, max 100), and the SHARED daily budget a manual li_withdraw_invites draws from too. The sweep runs once per 24h, so this is also the per-day rate: a 900-invite backlog takes ~18 days at 50/day. Bounded because every withdrawal is a real click in a browser session, so ~50 is roughly 15 minutes of browser time during which this account cannot send. For a large existing backlog prefer li_withdraw_invites, which drains on demand from the same budget.
sessionIdNoSending account id (or use sessionLabel)
untrackedNoAlso withdraw pending invites this system never sent — the ones found on LinkedIn's own sent-invitations list, i.e. the person's own manual outreach from before they used this product. OFF for new accounts on purpose: measured on a live account, one pass retired 50 invites of which NONE were ours. Only turn this on when the account holder has asked for their existing backlog to be cleaned.
sessionLabelNoSending account label, e.g. "Deven"
olderThanDaysNoWithdraw invites once they are older than this many days (default 60)

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the destructiveHint/openWorldHint annotations: withdrawal does not notify the recipient, re-inviting the same person is blocked for ~3 weeks, and every withdrawal is a real browser click (~50 ≈ 15 minutes during which the account cannot send). These are exactly the side-effect facts an agent needs before enabling a destructive, shared-budget sweep.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the primary action and its defaults; every sentence carries operational weight. The middle paragraph is a single long em-dash-heavy run-on that could be split for scanability, which is the only reason it isn't a 5.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a destructive, multi-param configuration tool with no output schema, the description supplies defaults, prerequisites, side effects, and cross-tool routing. An agent has everything needed to decide whether and how to call it.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is already 100% and the schema itself is verbose, so much of the parameter prose (maxPerDay cap, untracked measurement) is duplicated. Still, the description adds framing the schema lacks: defaults (off for new accounts, 60 days), the decision rule for `untracked`, and the shared-budget interaction with li_withdraw_invites, which raises it above the baseline 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a precise verb+resource: toggling the daily automatic withdrawal of an account's pending invites and setting the withdrawal age. The manual alternative (li_withdraw_invites) and the read path (li_list_accounts) are named, so the agent can place this tool relative to its siblings without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use reasoning: OFF by default for new accounts, turned on deliberately once the pending pile becomes the restriction risk, and `untracked` only when the account holder asks. It also names li_withdraw_invites as the better path for a large backlog, giving a clear routing rule.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_booking_settingsBooking Follow-Through SettingsA
Destructive
Inspect

Set one account's booking follow-through settings — the same ones as the dashboard's account page. Writes our database only; fields you omit keep their values, numbers are clamped to their bounds (the reply lists clamped), and the reply returns the account's full booking block. Current values are in li_list_accounts. Name the account when you hold several; it is never guessed.

  • bookingUrls: the booking links this account sends (cal.com / Calendly …). ANY outgoing message containing one — a sequence step, li_send_reply, or a message typed by hand in LinkedIn that the inbox sweep stores — marks the booking link as sent. Matched ignoring case, scheme, www and a trailing slash; a deeper path of a listed link counts too.

  • firstNudgeHours, closeAfterDays, maxNudges, cancelRecheckHours: when li_booking_due lists someone and whether its next action is a nudge or a close.

  • prefillName (default true): every outgoing message gets the recipient's cleaned name added to the booking link as a query parameter (cal.com name; Calendly name + first_name + last_name), so the booking comes back under the name li_match_contacts expects. The link stays the real link — no redirect, no shortener; links that are not this account's booking links are never touched; a link already carrying the name is left alone. Write the plain link in a message; PoliteReach adds the name.

  • bookingTool (default auto) and nameParam (default none): only for a self-hosted or unknown booking page, which is left unchanged unless nameParam is set. The defaults work for cal.com and Calendly.

NameRequiredDescriptionDefault
maxNudgesNoNudges before a close (default 1, 0-3)
nameParamNoOverride the query parameter the name goes in; '' clears it.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
bookingToolNoWhich booking tool the links are; auto detects it from the host.
bookingUrlsNoREPLACES the list. [] clears it.
prefillNameNoAdd the recipient's name to the booking link (default true).
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
closeAfterDaysNoDays after a nudge before the next action becomes a close (default 3, 1-30)
firstNudgeHoursNoHours after the booking link before the first check (default 48, 1-336)
cancelRecheckHoursNoHours after a cancelled booking before it is due again (default 24, 1-336)

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial behavior beyond the annotations: writes only the database, omitted fields keep their values, numbers are clamped with the reply listing 'clamped', bookingUrls replaces the list ([] clears it), and prefillName/bookingTool/nameParam have detailed conditional effects. This fully addresses the destructiveHint=true profile.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Long but well-structured: the purpose is front-loaded, followed by key caveats and a bulleted parameter breakdown. Every major sentence earns its place, though the overall length is on the verbose side even for a 10-parameter tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 10 optional parameters, no output schema, and destructive annotations, the description is complete: it covers account identification, default-omission behavior, clamping, replacement semantics, and return-block content. An agent has what it needs to invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema coverage is 100%, the description adds deep meaning beyond the schema, such as booking-link matching rules (case, scheme, www, trailing slash, deeper paths), what firstNudgeHours/etc. control, prefillName's exact query-parameter behavior, and the self-hosted-only condition for bookingTool/nameParam.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Set one account's booking follow-through settings — the same ones as the dashboard's account page.' It clearly distinguishes this settings-writer from read-oriented siblings like li_list_accounts and li_booking_due.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear context: current values are in li_list_accounts, and the account must be named when several exist ('it is never guessed'). It also relates parameters to li_booking_due and li_match_contacts, but gives no explicit when-not conditions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_default_pricing_tierSet the Pricing Tier New Customers Start On (operator only)AInspect

OPERATOR ONLY. Reads or sets the install-wide default price tier — 'founding' or 'standard' — that every NEW team (customer) is created on; the same control as the Default pricing selector above the Teams table on Users & access. Stored in the database, so it takes effect at once with no deploy. Existing teams keep their own tier (change one with li_set_team_billing) and no live subscription is touched. Omit tier to read it.

NameRequiredDescriptionDefault
tierNo'founding' or 'standard'; omit to read the current default

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only give readOnlyHint=false, destructiveHint=false, openWorldHint=false; the description goes well beyond them by disclosing that the value is DB-stored and takes effect immediately with no deploy, that existing teams and live subscriptions are untouched, and that the tool is operator-gated. That is precisely the mutation-impact context an agent needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads 'OPERATOR ONLY' and the core verb+resource, then layers scope constraints in a compact run of sentences. Slightly dense and a touch long, but each sentence (blast radius, read mode, sibling pointer) earns its place; nothing is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a one-parameter tool with no output schema, the description supplies everything needed: the operator gate, the read/write duality, the immediate-effect storage model, and the explicit scope of what is not affected. Nothing material is left to inference.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the single enum parameter already carries its own description, including the 'omit to read' behavior. The description restates the enum values and the omit-for-read semantics rather than adding new syntax or format detail, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope: reads or sets the install-wide default price tier that new teams are created on. It explicitly disambiguates from the related sibling li_set_team_billing and from the per-team control, so an agent can place it correctly without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Names the operator-only precondition, the exact call pattern ('Omit `tier` to read it'), the effect boundary (existing teams keep their own tier; use li_set_team_billing for those), and the non-effect on live subscriptions. When-to-use and when-not-to-use are both covered with an explicit alternative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_office_hoursSet an Account's Sending WindowAInspect

The hours during which this account does its own outgoing work. THREE things are gated by it: the invite drip, the scheduled Day-1/3/7/14 message drip (since 2026-09-08 — a canned follow-up landing in a live conversation at 3am is a worse signal than an overnight invite, not a better one), and the daily invite cleanup sweep. NOT gated, because a person pressing send at 23:40 means it: an ad-hoc message, a bulk send, and a reply sent from the inbox. So an invite or a scheduled follow-up due at 18:02 outside the window waits for the next opening, while a reply you send by hand goes immediately. Per account rather than global, so accounts in different geos behave correctly. end is EXCLUSIVE: 20 permits 19:59 and refuses 20:00. An end at or before the start is coerced to start+1 rather than stored, because hour >= start && hour < end is false for EVERY hour when end <= start, which would pause the account forever with nothing in the logs to say why. Read the current window with li_list_accounts.

PROSPECT-LOCAL WINDOWS (opt-in, per account): sendWindowMode account (default) is exactly the behaviour above. prospect gates the invite drip and scheduled steps by each CONTACT's timezone instead — invites inside prospectWindow.inviteStart–inviteEnd, steps inside messageStart–messageEnd, both local to the prospect, end exclusive, end <= start coerced the same way. both requires the account window AND the prospect window. A contact with no known timezone uses the account window in every mode, the daily cleanup always uses the account window, and ad-hoc/bulk/inbox sends stay ungated. An invite held longer than maxDeferHours (default 72) falls back to the account window so nobody waits for ever. Caps, pacing and every send check are unchanged. Recommended start: prospect mode, invites 8–11, messages 8–18, weekdaysOnly false. The reply carries a warning when both leaves many queued contacts with no overlapping hour.

NameRequiredDescriptionDefault
endNoLocal end hour, EXCLUSIVE
startNoLocal start hour, inclusive
enabledNoTurn the window on or off
timezoneNoIANA zone, e.g. Asia/Kolkata. Null/omitted falls back to the account timezone.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
prospectWindowNoProspect-local hours used by the prospect and both modes; omitted fields keep their stored values
sendWindowModeNoaccount (default): the account window gates everything; prospect: each contact's local window gates invites and scheduled steps; both: both must be open

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With only readOnlyHint=false, destructiveHint=false and openWorldHint=false available, the description carries the full behavioral burden and does so richly: it discloses the exclusivity rule, the silent-failure guard for end <= start (and why it exists), the maxDeferHours fallback, and the `warning` field returned when `both` mode strands contacts. This is exactly the mutation-safety context an agent needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The definition is long but justified by an 8-parameter nested tool, and it is structured with a clear gating model first, then the prospect-local windows section with explicit defaults and a recommendation. A few clauses editorialize (the 3am-follow-up aside), which is charming but not strictly load-bearing; still, almost every sentence conveys a rule or default.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a write tool with a nested object, an enum, and no output schema, the description covers the full behavioral contract, provides defaults for every prospectWindow field, states the read path (li_list_accounts), and notes the returned warning. Nothing an agent needs to invoke it correctly appears to be missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, and the description genuinely adds meaning: it explains that `end` is exclusive with a worked example (20 permits 19:59, refuses 20:00), and that end <= start is coerced to start+1 rather than stored. It also elaborates on how `sendWindowMode` values interact and how `maxDeferHours`/no-timezone contacts fall back, going beyond the schema's terse per-field text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The definition names a specific resource (the account's sending window) and precisely scopes what it controls, but it opens by defining the resource rather than stating the action ('set/update the window'), so the verb is inferred from the title and parameters. An agent can still tell this apart from siblings like li_hold_sending or li_pause_account, which affect sending state rather than the hours during which sends are permitted.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear operational context — which queues are gated (invite drip, scheduled drips, cleanup sweep) and which are not (ad-hoc, bulk, inbox replies) — and even offers a concrete recommended configuration for prospect mode. It stops short of explicit when-to-use/when-not routing against alternative tools, but the conditions that select each mode are spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_team_billingSet a Customer’s Pricing Tier, Currency and Price (operator only)A
Destructive
Inspect

OPERATOR ONLY. Reads or sets how a CUSTOMER (team) is billed — the same control as the Billing column on the dashboard's Users & access page. One subscription per team, priced per LinkedIn account (seat). Name the team by teamId or the email of anyone in it. pricingTier 'founding' or 'standard' and billingCurrency 'INR' (default) or 'USD' pick the plan the team checks out on — INR through Razorpay, USD through PayPal once PayPal is set up; a tier/currency whose plan is not set up is refused at checkout, never charged another price or currency. The currency cannot change while the team's subscription has not ended (cancel it first). razorpayPlanId sets a negotiated plan that wins over the tier's plan — a Razorpay plan_… or a PayPal P-… (its prefix picks the processor); null clears it. The id is checked with that processor when it is set up. Changes apply to the NEXT checkout only — a live subscription keeps its price. Omit all three to read the current settings. The tier NEW teams start on is li_set_default_pricing_tier.

NameRequiredDescriptionDefault
emailNoThe sign-in email of anyone in the team
teamIdNoTeam id (or use email)
pricingTierNo'founding' or 'standard' — the plan the team checks out on next
razorpayPlanIdNoA Razorpay (plan_…) or PayPal (P-…) plan id, or null to use the currency’s standard plan
billingCurrencyNo'INR' (Razorpay) or 'USD' (PayPal); cannot change while a subscription is live

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Well beyond the destructiveHint/openWorldHint annotations, it discloses that changes apply only to the NEXT checkout, that a live subscription keeps its price, that currency can't change while a subscription is live, that an unconfigured plan is refused at checkout, and that the plan id is validated against its processor. These are exactly the behavioral facts an agent needs for a billing mutation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads OPERATOR ONLY and the core purpose, then layers constraints efficiently. It is dense and sentence-heavy, but nearly every clause carries a distinct constraint (checkout timing, currency lock, processor validation); a minor trim would not lose information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 5-param, zero-required mutation tool with no output schema, it covers the read mode, safety caveats, precedence rules, and dependencies (PayPal setup, live-subscription lock) needed to invoke correctly. Nothing material is left for the agent to infer.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% so the baseline is 3, but the description adds cross-parameter meaning: razorpayPlanId overrides the tier's plan, its prefix selects the processor, null clears it, and INR is the default the schema only lists as an enum. That is genuine value above the schema, though the individual field semantics largely duplicate it.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource: reads or sets how a customer (team) is billed, priced per seat. It even maps the abstraction to a concrete UI location (the Billing column) and distinguishes the case from li_set_default_pricing_tier, so an agent can tell it apart from siblings without opening a schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit conditions: OPERATOR ONLY, target a team via teamId or any member's email, and omit all three writable params to read current settings. It names the alternative that governs NEW teams (li_set_default_pricing_tier) and states the checkout-time restriction, so the when-to-use picture is complete.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_team_complimentaryMake a Customer Free (operator only)A
Destructive
Inspect

OPERATOR ONLY. Reads or sets whether a CUSTOMER (team) is FREE — the same control as the Free switch beside Seats in the Teams table on the dashboard's Users & access page. Name the team by teamId or the email of anyone in it. complimentary: true puts EVERY LinkedIn account the team holds on complimentary access (full paid abilities, never billed, no trial, no subscribe link) and makes every account it connects later start complimentary. complimentary: false only stops new accounts being free — the accounts it already holds STAY complimentary (end one with li_set_account_complimentary), so nobody loses access by this switch. Free never changes the seat limit: complimentary accounts count toward it, so a free team with a limit of 1 holding one account is full (raise the limit on the Users page). reason is stored on each account's plan note. Omit complimentary to read the current state and seats (used / limit, complimentary count).

NameRequiredDescriptionDefault
emailNoThe sign-in email of anyone in the team
reasonNoWhy — stored on each account as the plan note, e.g. "founding partner"
teamIdNoTeam id (or use the email of anyone in it)
complimentaryNotrue = the team is free, false = new accounts are no longer free

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only provide the generic safety profile (destructiveHint=true, readOnlyHint=false); the description goes far beyond that by spelling out the asymmetric semantics of true vs false, that existing accounts keep access after setting false, that future accounts inherit the setting, and that seat limits are unaffected. This is exactly the state-change detail an agent needs before invoking a destructive mutation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Dense but essentially fluff-free; the operator-only warning is front-loaded and each sentence carries operative detail. It is a single unbroken block rather than scannable segments, which slightly hurts readability for a description this long.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description carries the return-value burden and does so by describing the read mode's output (state plus used/limit seats and complimentary count). Combined with the mutation semantics and prerequisites, nothing an agent needs to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning: `teamId` and `email` are presented as interchangeable team identifiers, and `complimentary` is explained in terms of downstream account/seat effects rather than merely its type. It edges above baseline without being fully exhaustive about the read-mode return shape.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb+resource combination ('reads or sets whether a CUSTOMER (team) is FREE') and anchors it to a concrete UI location (the Free switch beside Seats). It explicitly distinguishes itself from the nearest sibling, li_set_account_complimentary, so an agent can route correctly without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use for both modes (set with `complimentary`, read by omitting it) and names the alternative tool for the per-account case. It also states the operator-only restriction up front, which is a hard gate on usage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_team_discountGive a Customer a Discount on Their First Months (operator only)A
Destructive
Inspect

OPERATOR ONLY. Reads, gives or ends a CUSTOMER's (team's) discount on a NEW subscription: the same discount the dashboard's Users & access page shows per team. Name the team ONE way: teamId, the email of anyone in it, or account (an account id, label, LinkedIn name or slug; refused when it matches accounts of two teams). Omit kind and end to read the discount running now (or null), with adminSentence saying what it does. Give one with kind: 'percent' (value 1-99), 'amount' (value = a fixed amount off each month in the team's billing currency, major units, e.g. 500 for ₹500) or 'free' (no value), for months first months (1-12, default 1). The team's next checkout charges those months up front at the discounted price, then the usual monthly price starts by itself, on Razorpay (INR) and PayPal (USD) alike. validUntil (a date, YYYY-MM-DD, good to the end of that day UTC; at most a year ahead) makes the offer expire: a checkout after it pays the usual price. note is internal (never shown to the customer). Giving one emails the customer a special offer with the price, the deadline and a Subscribe link, once; the reply's customerEmailed says whether it really went out ('emailed', or 'not emailed: '). Refused: a team that already pays monthly (a discount applies to a new subscription only), a free or complimentary team, and a team with a discount still running (end it first with end: true, which emails the customer that it ended). The automatic offer after a trial is set with li_set_trial_end_offer.

NameRequiredDescriptionDefault
endNotrue = end the running discount now (the next checkout pays the usual price)
kindNo'percent', 'amount' (fixed off each month) or 'free'; omit to read
noteNoInternal note, e.g. "asked on a call"; never shown to the customer
emailNoThe sign-in email of anyone in the team
valueNoPercent off (1-99), or the amount off each month in major units; not for free
monthsNoHow many first months it covers, 1-12 (default 1)
teamIdNoTeam id (or use email or account)
accountNoAn account id, label, LinkedIn name or slug; its owner’s team is used
validUntilNoOffer valid until this day, YYYY-MM-DD (UTC); omit for no deadline

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only say readOnly=false, destructive=true, openWorld=true; the description adds the real behavioral burden: giving a discount sends a one-time customer email, ending one emails the customer that it ended, `customerEmailed` reports whether it truly went out, `note` is never customer-visible, and the checkout/billing consequences (months charged up front, then normal monthly price resumes, Razorpay INR and PayPal USD). This is well beyond what the annotations declare.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the OPERATOR ONLY constraint and scoped in the first clause, with no filler sentences. However, it is a single dense block of long sentences that mixes modes, refusals and return fields together, so it is harder to scan than a shorter or segmented version would be.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 9-parameter, zero-required, mutation-heavy tool with no output schema, the description covers naming the team, all three discount kinds, the deadline, refusals, the email side effects, and even the returned `adminSentence` and `customerEmailed` fields. Nothing an agent needs to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description genuinely adds meaning: `amount` is a fixed per-month figure in the team's billing currency in major units (with a ₹500 example), `validUntil` is good to end of day UTC and capped at a year ahead (a limit absent from the schema), and the three team-naming options plus the refusal when `account` is ambiguous. Some of this does restate schema enum text, keeping it below a 5.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb set (read/give/end) on a specific resource (a customer team's discount on a new subscription) and immediately scopes it as operator-only. It also disambiguates from siblings by name: li_set_trial_end_offer is called out as the tool for the post-trial automatic offer, and the billing/complimentary siblings are implicitly separated by the 'new subscription only' constraint.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use for each mode: omit kind/end to read, supply kind to give, use end:true to terminate. Alternatives and refusals are enumerated (already-paying teams, free/complimentary teams, teams with a running discount), and the reader is routed to li_set_trial_end_offer for the different scenario.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_trial_end_offerSet the Automatic Last Offer After a Trial (operator only)A
Destructive
Inspect

OPERATOR ONLY. Reads or sets the install-wide AUTOMATIC last offer: a discount on the first months of a new subscription, given once to a customer (team) whose trial ended without subscribing, and emailed to them as a personal last offer (marketing mail: the winback opt-out stops it). Hourly, a team qualifies when billing is set up for its currency, it is not free or complimentary, every account it pays for finished its trial afterDays or more days ago with none paid, and it has never subscribed and never had a discount. The offer is percent off (1-99; 0 turns the automatic offer OFF) for months first months (1-12), and it expires after validDays (1-30). afterDays is 8-60 (the trial-ended reminder goes at 7 days, so the last offer always comes after it). Give any of the four; the rest stay. Stored in the database, so it takes effect at the next hourly pass with no deploy; teams already offered keep their offer. Omit all four to read the current settings (defaults: 40% off 1 month, 10 days after the trial, valid 7 days). A one-off discount for one team is li_set_team_discount.

NameRequiredDescriptionDefault
monthsNoHow many first months the offer covers, 1-12
percentNoPercent off, 1-99; 0 turns the automatic offer off
afterDaysNoDays after the team’s latest trial end before it is offered, 8-60
validDaysNoHow many days the offer stays valid, 1-30

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (readOnlyHint=false, destructiveHint=true, openWorldHint=true) by disclosing the qualification criteria evaluated hourly, DB persistence with effect at the next hourly pass and no deploy, that already-offered teams keep their offer, and that percent=0 is the off switch. It also surfaces the operator permission requirement and the email delivery path, which annotations do not carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the critical constraint (OPERATOR ONLY) and organized as purpose, mechanics, parameters, persistence, read behavior, alternative. It is a long single block, and the qualification-criteria sentence is dense, but nearly every clause carries decision-relevant information rather than filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description covers the read path and lists current defaults, so an agent knows what a parameterless call yields. It is complete enough for correct invocation; only the exact shape of the read response is left implicit, which is a minor gap for a dual read/write tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and already documents every range and meaning, so the baseline is 3. The description adds rationale the schema lacks - why afterDays floors at 8 (the trial-ended reminder fires at 7, so the last offer must follow it) and what the defaults are when nothing is supplied - which meaningfully informs parameter choice.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb+resource: reads or sets the install-wide automatic last offer, with the exact object (discount on first months of a new subscription after an unpaid trial) spelled out. It is clearly distinguishable from the adjacent sibling li_set_team_discount, which is named as the one-off-per-team alternative. An agent can tell the two apart without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit when-to-use: OPERATOR ONLY, partial updates ('give any of the four; the rest stay'), and the read path ('omit all four to read the current settings'). It also names the closest alternative (li_set_team_discount) and the condition that selects it, plus the marketing mail opt-out interaction that gates the email send.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_set_user_default_proxySet a Person’s Proxy for New Accounts (operator only)AInspect

OPERATOR ONLY. Sets the proxy that every NEW LinkedIn account this person (by email) adds is created on, so one person's accounts share their IP without assigning each one. proxyId: null = new accounts use the install default. Existing accounts are NOT moved — moving a live account is li_set_account_proxy with its own egress confirmation.

NameRequiredDescriptionDefault
emailYesThe person's sign-in email
proxyIdYesA proxy id from li_proxies, or null for the install default

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations are minimal (readOnly=false, destructive=false), so the description carries real weight: it discloses the operator-only auth requirement, that existing accounts are NOT affected, and that null reverts to the install default. It does not spell out reversibility of a non-null assignment explicitly, but the null semantics largely cover it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The operator constraint and core behavior are front-loaded, followed by the exclusion and the sibling pointer. Two dense sentences with little waste, though the middle clause is information-packed.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-param mutation with no output schema, the description covers scope, the null escape hatch, and the sibling boundary. The only minor gap is the absence of a stated return/reversibility note, but nothing essential for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and already documents both params, so baseline is 3. The description nonetheless adds intent beyond the schema — that proxyId null means new accounts share the install default while existing ones stay put, and that email identifies the person whose future accounts are affected.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource (sets the default proxy for new accounts a person adds) and explicitly distinguishes itself from the sibling li_set_account_proxy. An agent can tell exactly what changes and what does not without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Front-loads 'OPERATOR ONLY' as a usage constraint, then names the alternative (li_set_account_proxy) and the condition that selects it — moving a live account. When-to-use and when-not-to-use are both explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_snooze_replyPark A Reply Until A DateAInspect

Put a conversation down until a date WITHOUT closing it — for "I am away until the 24th", "circle back after the quarter", anything where the answer is simply not now.

Why this exists rather than li_mark_reply_handled: the needs-answer queue sorts oldest-first, so an out-of-office reply sits permanently at the TOP of it, re-read every session in front of everyone genuinely waiting. Marking it handled removes it — and ALSO hands the sequence off, which makes the inbox sweep (about hourly) treat the thread as finished and never read it again. So parking somebody that way ended reply detection for them permanently: they come back on the 24th and nothing notices.

This writes one date and nothing else. The conversation stays open, the sweep keeps watching the thread, and a reply arriving while it is parked is still detected and moves them back up the queue. On the date the row simply reappears in li_replies_to_answer carrying snoozedUntil, so you can see why it was quiet. On a conversation already marked with an outcome (e.g. interested) the date is its CHECK-AGAIN date instead: it reappears in li_marked_conversations(dueOnly=true), not the reply queue.

NOTHING IS SENT when it expires — there is no scheduled follow-up here, deliberately. It reappears in a queue you already read; you decide then.

Pass until: null to un-park a date you set wrongly. Takes profileUrls for several at once.

NameRequiredDescriptionDefault
noteNoWhy — stored on the timeline with the date
untilYesISO 8601 date/time to park until, e.g. "2026-09-24T09:00:00Z". Must be in the future. null un-parks.
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
sessionIdNoThe account this conversation is on. Only used when the person is in NO campaign: then a light record is created on this account so the mark has something to act on. Needed only when you have more than one account.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
profileUrlsNoSeveral contacts in ONE call, max 200. Each is resolved and marked independently: one refusal does not stop the rest, and the reply reports every one by URL.
sessionLabelNoSame, by account label
conversationUrnNoThe thread, straight from li_list_conversations (single-person form). Recorded on the conversation so the live inbox recognises it as decided. On its own it finds the person only when their thread is already stored — pass profileUrl (the participantProfileId / ACoAA… URL) too.
participantNameNoTheir name from li_list_conversations — kept on a newly recorded contact.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare write/non-destructive/non-open-world. The description adds the traits that matter: nothing is sent on expiry, the conversation stays open, the hourly sweep keeps watching and still detects replies while parked, and the row reappears either in li_replies_to_answer (with `snoozedUntil`) or, if already marked with an outcome, in li_marked_conversations(dueOnly=true) as a check-again date.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core action in the first clause and every paragraph carries non-obvious information (queue-sort consequence, expiry behavior, check-again vs reply-queue routing). It is long, and the rationale paragraph could be tightened, but no sentence is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With 11 parameters, no output schema, and minimal annotations, the description fills the gaps an agent needs: what state changes, what does not happen on expiry, where the row resurfaces, and how marked vs unmarked conversations diverge. Return behavior is described in queue terms, which compensates for the absent output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents all 11 parameters including `until: null` semantics and the batch behavior of `profileUrls`. The description reinforces 'this writes one date and nothing else' and the batch/un-park behavior but adds little syntax or format detail beyond the schema, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource+scope ('Put a conversation down until a date WITHOUT closing it') and immediately names the sibling it must be distinguished from, li_mark_reply_handled, explaining why it exists instead. An agent can pick between the two without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use triggers ('I am away until the 24th', 'circle back after the quarter') and an explicit when-NOT-to-use with the alternative tool and the concrete harm of choosing wrong (reply detection ends permanently for that contact). Also states the un-park path via `until: null`.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_suppress_profilesAdd People To The Do-Not-Contact ListA
Destructive
Inspect

Add LinkedIn profiles to the do-not-contact list — people who asked not to be contacted, existing customers, competitors, anyone the team must never invite or message from here. The do-not-contact list is checked at IMPORT — a listed prospect is NOT imported and is reported by name in result.suppressed of li_ingest_prospects — and again at INVITE time, so a queued invite for someone added to the list after their import is retired with category suppressed instead of sent — and before every automated message step, and by the assistant send tools (li_send_reply, li_send_message, li_send_bulk_messages), which REFUSE a listed person. Only a message a person types themselves in LinkedIn or on the dashboard is not blocked. It is per TEAM: an entry made with a personal token applies to every LinkedIn account that team runs and to nobody else; entries on the INSTALL-WIDE list (made by the operator, i.e. the shared secret) apply to every team. Dry-run unless confirmSuppress=true: the preview returns exactly what the confirmed call WOULD add (wouldAdd), who is already on the list (alreadyListed) and what is not a profile URL at all (invalid), so the user can confirm the real change. The confirmed call returns the same three buckets as added / alreadyListed / invalid plus a one-line summary that names every count — relay it whole, including the invalid ones, or the user believes people are protected who are not. Adding someone does not withdraw an invite already sent or stop a sequence already running; use li_mark_stopped for a live sequence.

NameRequiredDescriptionDefault
scopeNoWhich list to write to. "mine" = this team only. "install" = EVERY team on this install, including other customers. Only an operator may choose; a personal token always writes its own team's list and this is ignored. Defaults to "install" for an operator, which is the wider of the two — check the dry run.
reasonNoWhy, in a few words (max 300 chars) — shown beside every refusal this entry causes, so write it for the colleague who reads it later.
profileUrlsYesLinkedIn profile URLs or bare /in/ slugs, max 500 per call. A longer list is REFUSED rather than trimmed — split it and call again. The same person in several spellings is one row.
confirmSuppressYesMust be true to write; false returns a dry-run preview of what would change

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true but the description goes far beyond: it maps every enforcement point (import, invite time, automated message steps, the three assistant send tools), the one exception (messages a human types), the per-team vs install-wide scope model, and the dry-run/confirm gate. This is the kind of context annotations cannot carry.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the purpose and the consequences; every sentence carries substantive information about enforcement or return shape. It is long and dashes/caps-laden, and the enforcement points could be compressed, but nothing is pure filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description must explain returns — and it does precisely: the three dry-run buckets (wouldAdd/alreadyListed/invalid), the confirmed buckets (added/alreadyListed/invalid), and the summary line, with an instruction to relay it whole including invalid counts. An agent has everything needed to call and report correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description still reinforces semantics the schema states, notably the dry-run meaning of confirmSuppress and the consequence of scope defaulting to 'install' for operators ('the wider of the two — check the dry run'), plus the 500-URL refusal behavior.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Add LinkedIn profiles to the do-not-contact list') and immediately enumerates the populations it is for. It is trivially distinguishable from siblings like li_list_suppressed and li_unsuppress_profile.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use categories (asked not to be contacted, existing customers, competitors) and an explicit boundary — 'Adding someone does not withdraw an invite already sent or stop a sequence already running; use li_mark_stopped for a live sequence.' That is a named alternative with the selecting condition.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_sweep_inboxCheck for New Replies NowA
DestructiveIdempotent
Inspect

Read the LinkedIn inbox on the worker and mark anyone who has replied, cancelling their remaining automated follow-ups. This is the on-demand counterpart to the scheduled sweep (about hourly). Without it a reply is only ever noticed in the seconds before the next scheduled message, so someone who answers with no follow-up pending waits days — and someone whose sequence already finished is never noticed at all. Returns immediately; the worker does the single-session browser work. ⚠️ DO NOT look for it in li_job_result: this job dedupes on a fixed id and DELETES its own record the moment it finishes, which is what lets you trigger it again straight away — and no jobId is handed back either. Read what it CHANGED instead: li_replies_to_answer for the queue, li_conversation_history for one thread. IMPORTANT: a failed read is reported as an error and NEVER as 'nobody replied', and threads it could not tie to a contact are reported separately — do not relay a reply count without them.

NameRequiredDescriptionDefault
sessionIdNoSweep only this account, by id
sessionLabelNoSweep only this account

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Adds substantial context beyond the annotations: it returns immediately while the worker does single-session browser work, it never surfaces in li_job_result, it dedupes on a fixed id and deletes its own record (explaining why no jobId comes back), failed reads are errors never 'nobody replied', and untied threads are reported separately. Annotations only give the read/write/destructive/idempotent flags; this explains the actual mechanics.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the action and effect, then the rationale, then the consumer warnings. It is long, but nearly every sentence carries a distinct operational warning; only the parenthetical rationale could be tightened.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates fully: it explains that the call returns immediately, that no jobId is returned, that li_job_result will not contain it, and where the resulting changes must be read. Error and edge-case reporting are also covered.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and both optional filters (sessionId, sessionLabel) are already documented in the schema. The description implies a single-account sweep but adds no format or precedence detail beyond what the schema states, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific compound action: read the LinkedIn inbox, mark repliers, and cancel their remaining automated follow-ups. It explicitly positions itself against both a sibling (the scheduled sweep) and the follow-up reads (li_replies_to_answer, li_conversation_history), so an agent can place it precisely.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use case (on-demand counterpart to the hourly sweep) plus the failure modes it prevents: a reply noticed only seconds before the next scheduled send, and a finished-sequence replier never noticed at all. It also names the alternatives for reading results.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_track_bookingRecord That They Agreed To A CallA
Destructive
Inspect

Record that a person AGREED to a call — you read it in their reply ("yes, happy to chat", "Tuesday works, send me a link"). PoliteReach never decides this; you do.

🚨 SEARCH THE CALENDAR FIRST: people often book through the link within minutes of saying yes. If they already booked, do NOT call this — record it with li_mark_booked and send them nothing.

Effect: booking status agreed, bookingAgreedAt stamped, outcome interested (which closes the automated sequence like li_mark_outcome does), and a first check date: the EARLIER of the booking link + the account's firstNudgeHours (48h by default; "now" when the link has not gone out yet) and 09:00 in THEIR timezone on the day BEFORE the day they named — never sooner than 2 hours from now. On that date they appear in li_booking_due.

Pass agreedDayText (their words: "next Tuesday") and, when you can resolve it, agreedDate as "YYYY-MM-DD" in their timezone. Batch with profileUrls. Refused for someone whose call is already booked (cancel it first with li_mark_booking_cancelled). A person in no campaign gets a light record on the account you name.

The booking link itself is detected automatically: any outgoing message containing one of the account's booking links (li_set_booking_settings) stamps it — you never record the link.

NameRequiredDescriptionDefault
noteNoWhy — quote them; stored with the verdict and on the timeline
agreedYesMust be true — this records an agreement, nothing else
contactIdNoOne contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl
messageIdNoThe exact message row, when you already have it from a dossier or a queue read.
sessionIdNoThe account this conversation is on. Only used when the person is in NO campaign: then a light record is created on this account so the mark has something to act on. Needed only when you have more than one account.
agreedDateNoThe calendar day those words mean, "YYYY-MM-DD", resolved by you in THEIR timezone. Omit when they named no day.
campaignIdNoWhich campaign membership to act on. A contact can be in more than one campaign, and without this the row is whichever was touched last — so stopping them in one campaign while leaving the other running was not possible. Pass it whenever the contact might be in two.
profileUrlNoOne contact, by profile URL
profileUrlsNoSeveral contacts in ONE call, max 200. Each is resolved and marked independently: one refusal does not stop the rest, and the reply reports every one by URL.
sessionLabelNoSame, by account label
agreedDayTextNoTheir own words for when, e.g. "next Tuesday afternoon"
conversationUrnNoThe thread, straight from li_list_conversations (single-person form). Recorded on the conversation so the live inbox recognises it as decided. On its own it finds the person only when their thread is already stored — pass profileUrl (the participantProfileId / ACoAA… URL) too.
participantNameNoTheir name from li_list_conversations — kept on a newly recorded contact.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes well beyond the annotations (which only give readOnly/destructive/openWorld hints): it discloses the exact side effects (status `agreed`, `bookingAgreedAt` stamped, outcome `interested` closing the sequence), the first-check-date computation, refusal conditions for already-booked contacts, batch independence, and the light-record fallback for non-campaign people. This is unusually rich behavioral disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core purpose and the critical 'search the calendar first' warning, then orders effects, timing, and parameters sensibly. It is dense and long, but for a 13-parameter mutation with intricate timing logic most sentences earn their place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers effects, timing, refusals, batching, and edge cases thoroughly for a complex mutating tool. The only minor gap is that with no output schema the description says little about the reply shape beyond 'reports every one by URL'.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds cross-parameter meaning the schema states only in isolation: the agreedDayText/agreedDate pairing, batching via profileUrls, and the sessionId-only-when-no-campaign rule. It also clarifies that the booking link is auto-detected and never passed as a parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Record that a person AGREED to a call') and explicitly disambiguates from the near-identical sibling li_mark_booked ('If they already booked, do NOT call this'). An agent can route between the two without opening either schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives the trigger condition ('you read it in their reply'), an explicit exclusion ('If they already booked, do NOT call this'), and names the alternative to use instead (li_mark_booked, li_mark_booking_cancelled). When/when-not and alternatives are all present.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_unmark_invitationForget A Triage DecisionA
Destructive
Inspect

Delete the triage decision recorded for ONE person on ONE account, so the next li_received_invitations(onlyUnreviewed) lists them as unreviewed again — the undo for a wrong li_mark_invitations. To change a decision instead, just re-mark with li_mark_invitations. The history keeps what it was. Touches only our database — nothing on LinkedIn: their invitation is not accepted, ignored or withdrawn. Requires confirm=true. Returns forgot (the removed decision), or found: false when none was recorded.

NameRequiredDescriptionDefault
confirmYesMust be true to delete the decision
memberIdNoThe inviter's member id (ACoAA…), as li_received_invitations returns it
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
profileUrlNoThe inviter, by profile URL (or use memberId)
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only flag destructiveHint/readOnlyHint; the description goes well beyond by disclosing that it touches only the internal database and nothing on LinkedIn (invitation not accepted, ignored or withdrawn), that history is preserved, that confirm=true is required, and the exact return shapes (`forgot`, `found: false`). This is exactly the behavioral context a destructive tool needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core action and the undo relationship, then layers constraints. It is dense but nearly every clause carries information; slightly long, but no wasted filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description still documents return values, the confirm gate, the data scope, and the ambiguity handling for multiple accounts — sufficient for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline is 3. The description adds value by clarifying the one-person/one-account addressing model and the confirm requirement, and by explaining the returned `forgot`/`found: false` semantics, though the per-parameter detail largely duplicates the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource with precise scope: 'Delete the triage decision recorded for ONE person on ONE account'. It explicitly contrasts itself with siblings (li_mark_invitations, li_received_invitations), so an agent can tell them apart without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use ('the undo for a wrong li_mark_invitations') and a when-not-to/alternative ('To change a decision instead, just re-mark with li_mark_invitations'). The routing condition is unambiguous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_unsuppress_profileRemove Someone From The Do-Not-Contact ListA
Destructive
Inspect

Remove ONE person from the do-not-contact list, by row id (from li_list_suppressed) or by profileUrl. The do-not-contact list is checked at IMPORT — a listed prospect is NOT imported and is reported by name in result.suppressed of li_ingest_prospects — and again at INVITE time, so a queued invite for someone added to the list after their import is retired with category suppressed instead of sent — and before every automated message step, and by the assistant send tools (li_send_reply, li_send_message, li_send_bulk_messages), which REFUSE a listed person. Only a message a person types themselves in LinkedIn or on the dashboard is not blocked. It is per TEAM: an entry made with a personal token applies to every LinkedIn account that team runs and to nobody else; entries on the INSTALL-WIDE list (made by the operator, i.e. the shared secret) apply to every team. A personal token may remove only its own team's rows: a person on the INSTALL-WIDE list is named as such and left alone, because lifting that refusal would lift it for every other team too — ask the operator. Answers 'Not found' for a row that does not exist or is not this caller's. Removing someone lifts the refusal for FUTURE imports and invites only; a connection already retired as suppressed stays retired — re-import them with li_ingest_prospects, or li_reset_contact, to start them again.

NameRequiredDescriptionDefault
idNoThe row id from li_list_suppressed
profileUrlNoOr the LinkedIn profile URL / slug — resolved to the row this caller may remove

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only give destructiveHint=true; the description supplies the real behavioral context — team scoping, the personal-vs-install-wide permission asymmetry, the 'Not found' answer for rows that aren't the caller's, and the crucial limitation that removal only lifts future import/invite/message refusals while already-retired connections stay retired.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The action and its two addressing modes are front-loaded in the first clause, and every subsequent sentence carries substantive routing or permission information. It is dense and dash-heavy, but not padded with filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by documenting the 'Not found' response and the exact downstream effects of removal across import, invite, and message stages. An agent has everything needed to invoke it correctly and to explain the outcome.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds meaning beyond the schema: `id` is sourced specifically from li_list_suppressed, and `profileUrl` is 'resolved to the row this caller may remove', clarifying that resolution is scoped to the caller's team.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (remove) and resource (one person from the do-not-contact list) with the two addressing modes (row `id` or `profileUrl`). It is immediately distinguishable from the sibling li_suppress_profiles (the inverse operation) and li_list_suppressed (the source of the row id).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly bounds the operation to ONE person, names li_list_suppressed as the id source, and routes the agent to li_ingest_prospects or li_reset_contact when a previously retired contact must be restarted. It also states the when-not case: an install-wide entry cannot be lifted by a personal token, so the agent should 'ask the operator' instead.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_verify_accountCheck an Account NowAInspect

Run a real session check for one account through the worker browser and record the result. This is the "Check now" the daily health routine should use: account health here is REACTIVE — an account only turns amber once a job has already failed, so an idle account whose cookies died reads healthy until something happens to run. It is also how you CONFIRM a reconnect actually took, because a successful check CLEARS the checkpoint breaker. It takes a browser slot, so expect it to queue behind other work; a wait timeout means the check is still running, not that it failed. If it shows the session dead, li_login_link gives the reconnect link.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
sessionLabelNoSending account label

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only give readOnlyHint=false and openWorldHint=true; the description adds the critical operational traits: it consumes a browser slot and may queue, a wait timeout means still-running rather than failed, and a successful run clears the checkpoint breaker. This is exactly the context annotations cannot express.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action, then layered with nuance about reactivity, queueing, and recovery. Dense and mostly earns its length, though the reactive-health explanation is slightly long-winded for a definition field.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, and the description compensates by explaining what results mean (session dead vs. timed-out still-running) and the downstream action. An agent has everything needed to call and interpret it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so sessionId and sessionLabel semantics (including 'never guessed' and the label alternative) are already documented in the schema. The description adds nothing about parameter format or precedence, so baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: runs a real session check for one account through the worker browser and records the result. It distinguishes itself from the passive status siblings (li_session_status, li_account_activity) by emphasizing 'REACTIVE' and 'real session check'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says when to use it ('Check now' for the daily health routine, and to CONFIRM a reconnect took because it clears the checkpoint breaker). It also names the follow-up alternative, li_login_link, for the case where the session is dead.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_withdraw_invitesWithdraw Pending Sent InvitationsA
Destructive
Inspect

Withdraw connection invites this account has sent that are still pending. Give profileUrls for specific people, and/or olderThanDays to retire everything past that age. Reads LinkedIn's OWN pending list, so it also covers invites sent outside this system (manual/Airtable-era outreach) — not just ones in the database. Stale ignored invites drag the acceptance rate LinkedIn judges the account on. NOTE: LinkedIn blocks re-inviting the same person for ~3 weeks after a withdrawal. State-changing — requires confirmWithdraw=true. Draws from the account's ONE shared daily withdrawal allowance (autoWithdrawPerDay), the same one the automatic sweep spends, so asking for more than is left today is not an error: it withdraws what remains and the reply's note says how many days the rest will take. Report that ETA to the user rather than implying everything was withdrawn.

NameRequiredDescriptionDefault
waitNoBlock until the run finishes. Default false — a backlog drain takes minutes; read li_withdraw_status instead.
sessionIdNoAccount id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed.
maxWithdrawNoCeiling for one run (default 25). The shared daily allowance still applies and may be lower.
profileUrlsNoWithdraw these specific profile URLs.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed.
olderThanDaysNoWithdraw every pending invite at least this many days old (e.g. 30).
confirmWithdrawYesMust be true to withdraw; false returns a dry-run preview

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag destructive/openWorld, but the description adds substantial non-obvious behavior: the ~3-week re-invite cooldown LinkedIn enforces, the confirmWithdraw=true gate, the shared daily allowance (autoWithdrawPerDay) that the automatic sweep also spends, that over-requesting is not an error, and that the reply's note carries the multi-day ETA the agent must surface.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded and the sentences are information-dense, but the passage is long and the acceptance-rate motivation plus the ETA-reporting instruction add length. Nearly every sentence earns its place, though the density works against quick scanning.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a state-changing tool with no output schema, the description covers what an agent needs: prerequisites, targeting options, allowance interaction, partial-success behavior, and the fact that a `note` field in the reply carries the ETA. Nothing critical to correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: it clarifies that profileUrls and olderThanDays can be combined, and that maxWithdraw is capped by the shared daily allowance rather than a per-run setting. It doesn't restate confirmWithdraw's dry-run semantics, which the schema already carries.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (withdraw) and resource (connection invites this account has sent that are still pending), and its scope is clearly delineated against li_pending_invites (list) and li_set_auto_withdraw (configure). The agent can identify the operation without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explains the two targeting modes explicitly (profileUrls for specific people, and/or olderThanDays for age-based retirement) and routes status polling to li_withdraw_status rather than the blocking wait. It also distinguishes manual withdrawals from the automatic sweep, so the agent knows how this fits alongside the scheduled behavior.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

li_withdraw_statusLast Invite-Withdrawal ResultA
Read-only
Inspect

Report of the most recent li_withdraw_invites run on one account (kept 24h): who was withdrawn, what was scanned, whether it hit the daily cap. Withdrawals run in the background and their job deletes itself, so this — not li_job_result — is how you confirm one finished. Reads the stored report only: no browser, instant. report: null with a note means none ran in 24h; ok: false means the report could not be read, which says nothing about whether one ran. Defaults to your default account. The DAILY automatic sweep reports in li_list_accounts (autoWithdraw.lastRun) instead.

NameRequiredDescriptionDefault
sessionIdNoAccount id from li_list_accounts. Omit (with sessionLabel) to use your default account.
sessionLabelNoAccount by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only cover the read-only/safe profile; the description adds real behavioral context beyond them: the report lives 24h, withdrawals run in background, the job deletes itself, this reads a stored report with no browser and is instant. It also disambiguates null vs ok:false semantics, which is exactly the kind of disclosure an agent needs.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with what the tool returns, then the routing rule, then the failure semantics — a logical order. It is dense and slightly long, but nearly every clause carries distinct, non-redundant information, so little is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the burden and does so: it explains the report contents (who was withdrawn, what was scanned, cap status) and the two degenerate return cases (report: null, ok: false). Nothing needed to interpret the result correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the schema already documents both sessionId and sessionLabel including the 'omit to use your default account' behavior. The description's 'Defaults to your default account' largely restates the schema, so it adds little beyond the baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource: a report of the most recent li_withdraw_invites run, scoped to one account and kept 24h. It explicitly differentiates itself from li_job_result and points to li_list_accounts for the daily automatic sweep, so an agent can distinguish it from every plausible sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use: 'this — not li_job_result — is how you confirm one finished,' with the reason (the withdrawal job deletes itself). It also names the alternative path for the daily sweep (li_list_accounts autoWithdraw.lastRun), so both the primary and alternate routes are covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updates
    • Changedli_fetch_posts2 fields changed
      • changedInput schema / properties / maxPosts / description
        Previous value: -"How many of each person's latest posts to return, 1 to 3. Default 3. Costs the same either way: 1 lookup per person."New value: +"How many of each person's latest posts to return, 1 to 2 (more is read as 2). Default 2. Costs the same either way: 1 lookup per person."
      • removedInput schema / properties / maxPosts / maximum
        Removed value: -3
    • Changedli_search_leads2 fields changed
      • changedInput schema / properties / currentCompanies / description
        Previous value: -"People who work at these companies NOW, as LinkedIn company page URLs (https://www.linkedin.com/company/<name>/). A company name alone is refused. Up to 50."New value: +"People who work at these companies NOW, as LinkedIn company page URLs (https://www.linkedin.com/company/<name>/). A company name alone is refused. Up to 50, matched as ANY of them: put every target company in this one call, never one call per company."
      • changedInput schema / properties / maxResults / description
        Previous value: -"How many people to return, default 25, at most 100. Rounded UP to whole pages of 25 (pages are billed whole), and charged per person actually returned."New value: +"How many people to return, default 25, at most 100. Rounded UP to whole pages of 25; each page that returns anyone is charged whole (25 lookups), and a search that finds nobody costs nothing."
  2. 3 tool updates
    • Changedli_create_campaign2 fields changed
      • changedInput schema / properties / csvText / description
        Previous value: -"CSV text with profile_url plus optional connection_note and step text columns (first_message, follow_up_1, follow_up_2, follow_up_3, closer), and optional engage_post_url / engage_reaction (a post of theirs the account reacts to a day or more before the invite; see engagePostUrl on targets). At most 50 rows, shared with `targets`."New value: +"CSV text with profile_url plus optional connection_note and step text columns (first_message, follow_up_1, follow_up_2, follow_up_3, closer), and optional engage_post_url / engage_reaction (a post of theirs the account reacts to about a day before the invite, never less than 16 hours; see engagePostUrl on targets). At most 50 rows, shared with `targets`."
      • changedInput schema / properties / targets / items / properties / engagePostUrl / description
        Previous value: -"Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it a day or more BEFORE the invite goes out (about one in seven is only viewed), and the invite then waits 20-60 hours. Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it."New value: +"Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it first (about one in seven is only viewed). The invite follows about a day later, never sooner than 16 hours, inside the window it goes out in: the person's own morning invite window when the account times invites to each prospect, otherwise the account's sending hours (18-30 hours when there is no window to aim at, such as sending hours switched off). Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it."
    • Changedli_experiment_assign1 field changed
      • changedInput schema / properties / prospects / items / properties / engagePostUrl / description
        Previous value: -"Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it a day or more BEFORE the invite goes out (about one in seven is only viewed), and the invite then waits 20-60 hours. Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it."New value: +"Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it first (about one in seven is only viewed). The invite follows about a day later, never sooner than 16 hours, inside the window it goes out in: the person's own morning invite window when the account times invites to each prospect, otherwise the account's sending hours (18-30 hours when there is no window to aim at, such as sending hours switched off). Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it."
    • Changedli_ingest_prospects1 field changed
      • changedInput schema / properties / prospects / items / properties / engagePostUrl / description
        Previous value: -"Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it a day or more BEFORE the invite goes out (about one in seven is only viewed), and the invite then waits 20-60 hours. Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it."New value: +"Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it first (about one in seven is only viewed). The invite follows about a day later, never sooner than 16 hours, inside the window it goes out in: the person's own morning invite window when the account times invites to each prospect, otherwise the account's sending hours (18-30 hours when there is no window to aim at, such as sending hours switched off). Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it."
  3. 2 tool updates
    • Addedli_ignore_calendar_event
    • Changedli_send_reply1 field changed
      • changedInput schema / properties / bookingNudge / description
        Previous value: -"These messages are BOOKING NUDGES for people li_booking_due listed with nextAction \"nudge\". Each is REFUSED unless that person agreed (li_track_booking), their check date has arrived, they have nudges left, no nudge went out in the last 20 hours, they have not written since the link/agreement, they are not on the do-not-contact list and the account is not paused. The booking state moves in the same step as the queueing (nudge count +1, next check = send time + the account's close-after days), and outside the account's office hours the send is queued for when they open. Applies to every reply in the call."New value: +"These messages are BOOKING NUDGES for people li_booking_due listed with nextAction \"nudge\". Each is REFUSED unless that person agreed (li_track_booking), their check date has arrived, they have nudges left, no nudge went out in the last 20 hours, they have not written since the link/agreement, they are not on the do-not-contact list and the account is not paused. The booking state moves in the same step as the queueing (nudge count +1, next check = send time + the account's close-after days), and outside the account's office hours the send is queued for when they open. With no account named, each person is sent from the account their agreed booking is on (li_booking_due's accountId), even when they are in campaigns on more than one of your accounts. Applies to every reply in the call."
  4. 5 tool updates
    • Changedli_enrich_profiles1 field changed
      • changedInput schema / properties / fields / description
        Previous value: -"Return only these parts of each profile, so a large batch does not flood the reply. The identity — profileUrl, status, name, headline, company, role, location, fetchedAt, error — comes back on EVERY profile and never needs asking for. The sections you can add are: about, audience, signals, currentRole, experience, education, skills (audience is follower and connection counts; signals is open-to-work, hiring, premium, influencer, verified), plus companyPage for each person's employer block (it rides on the default view, and on this trimmed one only when named). Pass an empty array for identity only. An unknown name is refused naming the accepted set, and nothing is looked up or charged. Costs exactly the same as the full record — it only changes what is sent back."New value: +"Return only these parts of each profile, so a large batch does not flood the reply. The identity — profileUrl, publicProfileUrl, memberId, searchId, status, name, headline, company, role, location, fetchedAt, error — comes back on EVERY profile and never needs asking for. The sections you can add are: about, audience, signals, currentRole, experience, education, skills (audience is follower and connection counts; signals is open-to-work, hiring, premium, influencer, verified), plus companyPage for each person's employer block (it rides on the default view, and on this trimmed one only when named). Pass an empty array for identity only. An unknown name is refused naming the accepted set, and nothing is looked up or charged. Costs exactly the same as the full record — it only changes what is sent back."
    • Changedli_experiment_assign1 field changed
      • changedInput schema / properties / prospects / items / properties / profileUrl / description
        Previous value: -"Canonical LinkedIn profile URL"New value: +"LinkedIn profile URL. A search id from li_search_leads (/in/ACwAA…) is accepted as is and resolved to the public profile URL before anything is written (reported as `searchIds`)."
    • Changedli_ingest_prospects1 field changed
      • changedInput schema / properties / prospects / items / properties / profileUrl / description
        Previous value: -"Canonical LinkedIn profile URL"New value: +"LinkedIn profile URL. A search id from li_search_leads (/in/ACwAA…) is accepted as is and resolved to the public profile URL before anything is written (reported as `searchIds`)."
    • Addedli_resolve_profile_url
    • Changedli_send_connection_now1 field changed
      • changedInput schema / properties / profileUrl / description
        Previous value: -"One recipient LinkedIn profile URL (or use profileUrls for several)"New value: +"One recipient LinkedIn profile URL (or use profileUrls for several). A search id from li_search_leads (/in/ACwAA…) is resolved to the public profile URL first."
  5. 16 tool updates
    • Changedli_campaign_contacts6 fields changed
      • addedInput schema / properties / connectionStatus
        Added value: +{
        +  "description": "Only contacts whose invite is in one of these statuses (\"none\" = imported already connected)",
        +  "items": {
        +    "enum": [
        +      "not_sent",
        +      "invited",
        +      "accepted",
        +      "withdrawn",
        +      "declined",
        +      "failed",
        +      "none"
        +    ],
        +    "type": "string"
        +  },
        +  "minItems": 1,
        +  "type": "array"
        +}
      • addedInput schema / properties / conversationState
        Added value: +{
        +  "description": "Only contacts whose `conversation.state` is one of these (a marked outcome wins)",
        +  "items": {
        +    "enum": [
        +      "not_started",
        +      "invite_pending",
        +      "invite_failed",
        +      "queued",
        +      "awaiting_reply",
        +      "needs_reply",
        +      "answered",
        +      "stopped",
        +      "no_reply",
        +      "send_failed",
        +      "won",
        +      "meeting_booked",
        +      "interested",
        +      "not_interested",
        +      "no_outcome"
        +    ],
        +    "type": "string"
        +  },
        +  "minItems": 1,
        +  "type": "array"
        +}
      • addedInput schema / properties / format
        Added value: +{
        +  "description": "\"csv\" = one header + rows string instead of the `contacts` array (rows view only)",
        +  "enum": [
        +    "json",
        +    "csv"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / hasInbound
        Added value: +{
        +  "description": "true = only contacts who have written to you; false = only those who have not",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / includeSteps
        Added value: +{
        +  "description": "true = each row carries its `steps` (ids for li_edit_step / li_cancel_steps). Default false: rows carry `stepCounts` only.",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / text / description
        Previous value: -"How much of the stored COPY each row carries — the connection note and every step text. \"preview\" (the default) clips each one to 120 characters and says so per field. \"full\" returns them complete: pass this when reviewing what a campaign will actually send. \"none\" omits the copy and keeps only its length, which is much the cheapest page."New value: +"How much COPY each row carries — the connection note and every step text. Setting it includes the steps. \"preview\" (the default with steps) clips each to 120 characters and says so per field; \"full\" returns them complete (to review what will send); \"none\" keeps only lengths."
    • Changedli_check_targets5 fields changed
      • addedInput schema / properties / campaignNamePrefix
        Added value: +{
        +  "description": "The campaigns you are BUILDING, by name prefix (case-insensitive), e.g. \"PR test Oct\". Anyone already in one of them is listed in `batch.members` (never capped) and marked `inBatch`, whatever their bucket says. Use it when splitting a list across several new campaigns.",
        +  "maxLength": 200,
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / fields
        Added value: +{
        +  "description": "view \"people\" only: which columns besides profile and campaign, from name, bucket, connection, message, outcome, inbound. Default all.",
        +  "items": {
        +    "enum": [
        +      "name",
        +      "bucket",
        +      "connection",
        +      "message",
        +      "outcome",
        +      "inbound"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / format
        Added value: +{
        +  "description": "view \"people\" only: \"json\" (default) returns `rows` as arrays matching `columns`; \"csv\" returns the same table as `csv` text.",
        +  "enum": [
        +    "json",
        +    "csv"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / view / description
        Previous value: -"How much to return. \"summary\" (the default) groups the colliding people by bucket and lists each campaign once in `sites`, naming up to 50 people per bucket and saying in `peopleHidden` how many it did not. \"full\" returns one flat row per person, each carrying its own copy of the same sentence and campaign name, in submission order."New value: +"How much to return. \"summary\" (the default) groups the colliding people by bucket and lists each campaign once in `sites`, naming up to 50 people per bucket (`peopleHidden` counts the rest) and `otherCampaigns` per person. \"people\" is EVERY campaign each person is in, one table row per person per campaign (account, campaign status, connection, message, outcome, inbound), uncapped and compact — for \"which of these URLs are in which campaigns\". \"full\" returns flat rows with every sentence, plus `people` with whole membership objects."
      • changedInput schema / properties / view / enum
        Previous value: -[
        -  "summary",
        -  "full"
        -]New value: +[
        +  "summary",
        +  "full",
        +  "people"
        +]
    • Changedli_create_campaign1 field changed
      • changedInput schema / properties / targets / description
        Previous value: -"Prefilled targets, at most 50 per call (CSV rows count toward the same limit). For a longer list, split it across calls under the same strategy, or use li_ingest_prospects, which takes 200."New value: +"Prefilled targets, at most 50 per call (CSV rows count toward the same limit). For a longer list, split it across calls under the same strategy, or use li_ingest_prospects, which takes 500."
    • Changedli_create_icp3 fields changed
      • addedInput schema / properties / sessionId
        Added value: +{
        +  "description": "Operator only: the account whose TEAM owns it. Members' creates are their team's.",
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionLabel
        Added value: +{
        +  "description": "Operator only: the account whose TEAM owns it. Members' creates are their team's.",
        +  "type": "string"
        +}
      • addedInput schema / properties / shared
        Added value: +{
        +  "description": "Operator only: true publishes it as a shared template every team sees. Never the default.",
        +  "type": "boolean"
        +}
    • Changedli_create_strategy3 fields changed
      • addedInput schema / properties / sessionId
        Added value: +{
        +  "description": "Operator only: the account whose TEAM owns it. Members' creates are their team's.",
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionLabel
        Added value: +{
        +  "description": "Operator only: the account whose TEAM owns it. Members' creates are their team's.",
        +  "type": "string"
        +}
      • addedInput schema / properties / shared
        Added value: +{
        +  "description": "Operator only: true publishes it as a shared template every team sees. Never the default.",
        +  "type": "boolean"
        +}
    • Changedli_edit_campaign4 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "With resnapshot: preview who would be re-copied, write nothing.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / resnapshot
        Added value: +{
        +  "description": "Re-copy every member's step kinds and delays from strategyId (or, without one, from the campaign's own strategy), keeping each person's own text. Only while EVERY member is untouched: invite not sent, no step sent or scheduled, nothing launched for them. Otherwise refused, naming who. Writes nothing on refusal.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / stepTexts
        Added value: +{
        +  "description": "Per-person text for step kinds a person has no text for under the new strategy (or to replace theirs). Implies resnapshot. Text is never invented: a person missing text for a new step is named and nothing is written.",
        +  "items": {
        +    "additionalProperties": false,
        +    "properties": {
        +      "closer": {
        +        "description": "Text for the \"closer\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "firstMessage": {
        +        "description": "Text for the \"first\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "followUp1": {
        +        "description": "Text for the \"followup_1\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "followUp2": {
        +        "description": "Text for the \"followup_2\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "followUp3": {
        +        "description": "Text for the \"followup_3\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "profileUrl": {
        +        "description": "The person, as stored or as any spelling of them",
        +        "minLength": 1,
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "profileUrl"
        +    ],
        +    "type": "object"
        +  },
        +  "maxItems": 500,
        +  "type": "array"
        +}
      • changedInput schema / properties / strategyId / description
        Previous value: -"Accepted so it can be REFUSED with the reason, rather than dropped as an unknown key. Allowed only while the campaign still holds nobody."New value: +"A new strategy. On an EMPTY campaign it simply applies. On one holding people it is refused unless resnapshot=true."
    • Addedli_experiment_assign
    • Changedli_ingest_prospects11 fields changed
      • addedInput schema / properties / defaultSteps
        Added value: +{
        +  "additionalProperties": false,
        +  "description": "Shared copy: text per step (connectionNote, firstMessage, followUp1, followUp2, followUp3, closer) for every row that brings none of its own. {first} is the row's first name (firstName, else the first word of name), filled in at import; no other variable exists. A row with no first name for a template that uses {first} is not imported, and says why.",
        +  "properties": {
        +    "closer": {
        +      "description": "Default text for the \"closer\" step",
        +      "maxLength": 8000,
        +      "type": "string"
        +    },
        +    "connectionNote": {
        +      "description": "Default invite note (connection-stage rows only)",
        +      "maxLength": 8000,
        +      "type": "string"
        +    },
        +    "firstMessage": {
        +      "description": "Default text for the \"firstMessage\" step",
        +      "maxLength": 8000,
        +      "type": "string"
        +    },
        +    "followUp1": {
        +      "description": "Default text for the \"followUp1\" step",
        +      "maxLength": 8000,
        +      "type": "string"
        +    },
        +    "followUp2": {
        +      "description": "Default text for the \"followUp2\" step",
        +      "maxLength": 8000,
        +      "type": "string"
        +    },
        +    "followUp3": {
        +      "description": "Default text for the \"followUp3\" step",
        +      "maxLength": 8000,
        +      "type": "string"
        +    }
        +  },
        +  "type": "object"
        +}
      • addedInput schema / properties / idempotencyKey
        Added value: +{
        +  "description": "Any unique string for THIS request (e.g. a UUID). Pass the same key when retrying after a timeout, 502 or 503: a call that already finished returns its stored answer (24h) instead of importing twice. Ignored on a dry run.",
        +  "maxLength": 200,
        +  "minLength": 8,
        +  "type": "string"
        +}
      • changedInput schema / properties / prospects / description
        Previous value: -"1-200 prospects, each with profileUrl and the prefilled text for every strategy step (plus connectionNote for invites)"New value: +"1-500 prospects, each with profileUrl and the prefilled text for every strategy step (plus connectionNote for invites), or that text from defaultSteps. Each may name its own account, campaign, strategy and ICP."
      • addedInput schema / properties / prospects / items / properties / campaignId
        Added value: +{
        +  "description": "Land this ONE prospect in this existing campaign (id from li_list_campaigns), overriding the call's. Its account is the campaign's when the row names none; it takes the campaign's own strategy and ICP.",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / campaignName
        Added value: +{
        +  "description": "Land this ONE prospect in the campaign of this name on its account (created if missing), overriding the call's campaign. Stored and matched exactly as given (nothing is appended), so the same name reaches the same campaign.",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / firstName
        Added value: +{
        +  "description": "First name for {first} in defaultSteps; else the first word of name. Not stored.",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / icpId
        Added value: +{
        +  "description": "ICP label for this prospect's campaign, overriding the call's (or icpName)",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / icpName
        Added value: +{
        +  "description": "ICP label by name, for this prospect only",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / strategyId
        Added value: +{
        +  "description": "Strategy for this prospect's campaign, overriding the call's (or strategyName)",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / strategyName
        Added value: +{
        +  "description": "Strategy by name, for this prospect only",
        +  "type": "string"
        +}
      • changedInput schema / properties / prospects / maxItems
        Previous value: -200New value: +500
    • Changedli_job_result1 field changed
      • addedInput schema / properties / offset
        Added value: +{
        +  "description": "Only with a research jobId (from li_research_profiles): the first row to return. Pass the previous reply's nextOffset.",
        +  "minimum": 0,
        +  "type": "integer"
        +}
    • Changedli_launch_campaign8 fields changed
      • changedInput schema / properties / campaignId / description
        Previous value: -"Campaign id from li_create_campaign or li_list_campaigns"New value: +"One campaign id from li_list_campaigns. Or campaignIds / namePrefix."
      • addedInput schema / properties / campaignId / minLength
        Added value: +1
      • addedInput schema / properties / campaignIds
        Added value: +{
        +  "description": "Bulk: up to 100 campaign ids, each launched as the single form would be",
        +  "items": {
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 100,
        +  "minItems": 1,
        +  "type": "array"
        +}
      • addedInput schema / properties / expectCount
        Added value: +{
        +  "description": "With namePrefix only: the number of campaigns the preview listed. Required to confirm, so nothing is launched that the preview did not show.",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / namePrefix
        Added value: +{
        +  "description": "Bulk: every campaign you can see whose name starts with this (case-insensitive, the same match as li_list_campaigns), at most 100; listed before anything is launched",
        +  "maxLength": 200,
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionId
        Added value: +{
        +  "description": "With namePrefix only: match on this account only",
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionLabel
        Added value: +{
        +  "description": "With namePrefix only: match on this account only",
        +  "type": "string"
        +}
      • changedInput schema / required
        Previous value: -[
        -  "campaignId",
        -  "confirmLaunch"
        -]New value: +[
        +  "confirmLaunch"
        +]
    • Changedli_list_campaigns8 fields changed
      • addedInput schema / properties / fields
        Added value: +{
        +  "description": "Only these columns per row (campaignId is always kept). Omit for every column.",
        +  "items": {
        +    "enum": [
        +      "name",
        +      "status",
        +      "accountId",
        +      "accountLabel",
        +      "strategyName",
        +      "icpName",
        +      "contactCount",
        +      "launchedAt",
        +      "priority",
        +      "createdAt",
        +      "state"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / format
        Added value: +{
        +  "description": "\"csv\" = one header + rows string instead of the `campaigns` array",
        +  "enum": [
        +    "json",
        +    "csv"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / maxContacts
        Added value: +{
        +  "description": "Only campaigns holding at most this many people (0 = empty ones)",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / minContacts
        Added value: +{
        +  "description": "Only campaigns holding at least this many people",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / namePrefix
        Added value: +{
        +  "description": "Only campaigns whose name starts with this (case-insensitive)",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / offset
        Added value: +{
        +  "description": "Skip this many matching campaigns — pass the last reply's `nextOffset`",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / sessionId
        Added value: +{
        +  "description": "Limit to one account by id (li_list_accounts). Omit to cover every account you can see.",
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionLabel
        Added value: +{
        +  "description": "Limit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see.",
        +  "type": "string"
        +}
    • Changedli_move_contact4 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "description": "Preview the move (and the re-copy) without writing.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / idempotencyKey
        Added value: +{
        +  "description": "Any unique string for THIS request (e.g. a UUID). Reuse it only to retry the same call.",
        +  "maxLength": 200,
        +  "minLength": 8,
        +  "type": "string"
        +}
      • addedInput schema / properties / resnapshot
        Added value: +{
        +  "description": "Re-copy each mover's steps from the target campaign's strategy before moving (kinds and delays; their own text kept). Untouched people only. Not with contactId.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / stepTexts
        Added value: +{
        +  "description": "Per-person text for target step kinds a mover has no text for (or to replace theirs). Implies resnapshot.",
        +  "items": {
        +    "additionalProperties": false,
        +    "properties": {
        +      "closer": {
        +        "description": "Text for the \"closer\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "firstMessage": {
        +        "description": "Text for the \"first\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "followUp1": {
        +        "description": "Text for the \"followup_1\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "followUp2": {
        +        "description": "Text for the \"followup_2\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "followUp3": {
        +        "description": "Text for the \"followup_3\" step",
        +        "maxLength": 8000,
        +        "type": "string"
        +      },
        +      "profileUrl": {
        +        "description": "The person, as stored or as any spelling of them",
        +        "minLength": 1,
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "profileUrl"
        +    ],
        +    "type": "object"
        +  },
        +  "maxItems": 500,
        +  "type": "array"
        +}
    • Changedli_outreach_analytics3 fields changed
      • addedInput schema / properties / experimentId
        Added value: +{
        +  "description": "groupBy \"arm\" only: the experiment (omit to list yours)",
        +  "type": "string"
        +}
      • changedInput schema / properties / groupBy / description
        Previous value: -"Row grouping: strategy, icp, campaign or account"New value: +"Row grouping: strategy, icp, campaign, account, or arm (an experiment's arms; needs experimentId)"
      • changedInput schema / properties / groupBy / enum
        Previous value: -[
        -  "strategy",
        -  "icp",
        -  "campaign",
        -  "account"
        -]New value: +[
        +  "strategy",
        +  "icp",
        +  "campaign",
        +  "account",
        +  "arm"
        +]
    • Changedli_pause_campaign8 fields changed
      • changedInput schema / properties / campaignId / description
        Previous value: -"Campaign id from li_list_campaigns"New value: +"One campaign id from li_list_campaigns. Or campaignIds / namePrefix."
      • addedInput schema / properties / campaignId / minLength
        Added value: +1
      • addedInput schema / properties / campaignIds
        Added value: +{
        +  "description": "Bulk: up to 100 campaign ids, each paused as the single form would be",
        +  "items": {
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 100,
        +  "minItems": 1,
        +  "type": "array"
        +}
      • addedInput schema / properties / confirm
        Added value: +{
        +  "description": "Bulk forms only: true to act. Without it the call lists what matched.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / namePrefix
        Added value: +{
        +  "description": "Bulk: every campaign you can see whose name starts with this (case-insensitive, the same match as li_list_campaigns), at most 100; listed before anything is paused",
        +  "maxLength": 200,
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionId
        Added value: +{
        +  "description": "With namePrefix only: match on this account only",
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionLabel
        Added value: +{
        +  "description": "With namePrefix only: match on this account only",
        +  "type": "string"
        +}
      • removedInput schema / required
        Removed value: -[
        -  "campaignId"
        -]
    • Addedli_research_profiles
    • Changedli_resume_campaign9 fields changed
      • changedInput schema / properties / campaignId / description
        Previous value: -"Campaign id from li_list_campaigns"New value: +"One campaign id from li_list_campaigns. Or campaignIds / namePrefix."
      • addedInput schema / properties / campaignId / minLength
        Added value: +1
      • addedInput schema / properties / campaignIds
        Added value: +{
        +  "description": "Bulk: up to 100 campaign ids, each resumed as the single form would be",
        +  "items": {
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 100,
        +  "minItems": 1,
        +  "type": "array"
        +}
      • addedInput schema / properties / confirm
        Added value: +{
        +  "description": "Bulk forms only: true to act. Without it the call lists what matched.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / expectCount
        Added value: +{
        +  "description": "With namePrefix only: the number of campaigns the preview listed. Required to confirm, so nothing is resumed that the preview did not show.",
        +  "minimum": 0,
        +  "type": "integer"
        +}
      • addedInput schema / properties / namePrefix
        Added value: +{
        +  "description": "Bulk: every campaign you can see whose name starts with this (case-insensitive, the same match as li_list_campaigns), at most 100; listed before anything is resumed",
        +  "maxLength": 200,
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionId
        Added value: +{
        +  "description": "With namePrefix only: match on this account only",
        +  "type": "string"
        +}
      • addedInput schema / properties / sessionLabel
        Added value: +{
        +  "description": "With namePrefix only: match on this account only",
        +  "type": "string"
        +}
      • removedInput schema / required
        Removed value: -[
        -  "campaignId"
        -]
  6. 1 tool update
    • Changedli_list_conversations1 field changed
      • changedInput schema / properties / onlyAddressable / description
        Previous value: -"Drop threads with no identifiable participant — LinkedIn sponsored/InMail blasts, which still count as \"they spoke last\" but cannot be replied to. Off by default so nothing is hidden; the response always reports how many were unaddressable."New value: +"Drop threads that cannot be replied to — LinkedIn sponsored/InMail blasts, with or without a named sender, which still count as \"they spoke last\". Off by default so nothing is hidden; the response always reports how many were unaddressable."
  7. 1 tool update
    • Changedli_edit_campaign1 field changed
      • addedInput schema / properties / priority
        Added value: +{
        +  "description": "true = send this campaign's invites before every non-priority campaign on its LinkedIn account (they wait while it has anyone to invite); false = take turns again. Only when the user asks. It reorders invites only: it NEVER raises a daily or weekly cap and never changes sending hours or messages. Omitted leaves it alone.",
        +  "type": "boolean"
        +}
  8. 2 tool updates
    • Addedli_set_team_discount
    • Addedli_set_trial_end_offer
  9. 1 tool update
    • Addedli_clear_invite_hold
  10. 1 tool update
    • Changedli_send_reply1 field changed
      • addedInput schema / properties / calendarChecked
        Added value: +{
        +  "description": "REQUIRED (true) for anyone with our booking link out (bookingPending / bookingCheckRequired in li_replies_to_answer) or a call already booked: it says you searched the calendar for THIS person just before this call. This server reads no calendar, so a booking made minutes ago is invisible here until li_mark_booked records it. If they booked: send NOTHING (no \"grab a slot\", no \"see you then\") and record it with li_mark_booked. Without it such a row is refused and nothing is queued. Implied by bookingNudge / bookingClose (their routine sweeps the calendar first), but search again right before those too. Applies to every reply in the call.",
        +  "type": "boolean"
        +}
  11. 2 tool updates
    • Changedli_enrich_profiles7 fields changed
      • changedInput schema / properties / fields / description
        Previous value: -"Return only these parts of each profile, so a large batch does not flood the reply. The identity — profileUrl, status, name, headline, company, role, location, fetchedAt, error — comes back on EVERY profile and never needs asking for. The sections you can add are: about, audience, signals, currentRole, experience, education, skills (audience is follower and connection counts; signals is open-to-work, hiring, premium, influencer, verified), plus companyPage for each person's employer block (it rides on the default view, and on this trimmed one only when named). Pass an empty array for identity only. An unknown name is refused naming the accepted set, and nothing is looked up or charged. Costs exactly the same as the full record — it only changes what is sent back. Cannot be combined with includeRaw."New value: +"Return only these parts of each profile, so a large batch does not flood the reply. The identity — profileUrl, status, name, headline, company, role, location, fetchedAt, error — comes back on EVERY profile and never needs asking for. The sections you can add are: about, audience, signals, currentRole, experience, education, skills (audience is follower and connection counts; signals is open-to-work, hiring, premium, influencer, verified), plus companyPage for each person's employer block (it rides on the default view, and on this trimmed one only when named). Pass an empty array for identity only. An unknown name is refused naming the accepted set, and nothing is looked up or charged. Costs exactly the same as the full record — it only changes what is sent back."
      • removedInput schema / properties / includeRaw
        Removed value: -{
        -  "description": "Return the data provider's record verbatim instead of the compact profile. Costs NO extra lookups, but the reply is MUCH larger — one profile measured ~8KB, so twenty is ~160KB of mostly unrelated people, followed companies and image URLs. Ask for it only when the compact profile is genuinely missing something, and prefer a small batch when you do.",
        -  "type": "boolean"
        -}
      • removedInput schema / properties / maxAgeDays
        Removed value: -{
        -  "description": "Treat a cached profile older than this as stale. Default 30.",
        -  "maximum": 365,
        -  "minimum": 1,
        -  "type": "integer"
        -}
      • changedInput schema / properties / profileUrls / description
        Previous value: -"LinkedIn profile URLs. One real lookup takes at most 100 and a longer list is refused rather than silently truncated; estimateOnly answers for up to 500 and says how many calls to split it into."New value: +"The LinkedIn profile URLs of the people to look up — the only input passed to the data provider. One real lookup takes at most 100 and a longer list is refused rather than silently truncated; estimateOnly answers for up to 500 and says how many calls to split it into."
      • changedInput schema / properties / refresh / description
        Previous value: -"Ignore the cache and re-fetch. Costs a lookup for every URL."New value: +"Ignore the cache and re-fetch, for when a cached profile is known to be out of date. Costs a lookup for every URL."
      • changedInput schema / properties / sessionId / description
        Previous value: -"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."New value: +"Only when you have more than one LinkedIn account: which one pays for the lookup (its plan and allowance gate the run). Account id from li_list_accounts. Never sent to the data provider."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."New value: +"Same as sessionId, by account label, LinkedIn name, profile slug or member id (li_list_accounts). Only when you have more than one account. Never sent to the data provider."
    • Changedli_list_conversations1 field changed
      • changedInput schema / properties / limit / description
        Previous value: -"How many to return AFTER filtering (default 20). The inbox is walked by cursor, 20 threads per round trip, until this many MATCH or the inbox runs out — so a large number with onlyNeedsReply is honest but costs browser time. Check `reachedEnd`."New value: +"How many threads to collect (default 20, max 200), counted after onlyNeedsReply / onlyAddressable / sinceDays / profileUrl but BEFORE the decided / internal / system / reviewed drops, so the reply can hold fewer. The inbox is walked by cursor, 20 threads per round trip, until this many match, the inbox runs out or the page ceiling is hit — a large number costs browser time. Check `reachedEnd`."
  12. 6 tool updates
    • Changedli_contact_dossier2 fields changed
      • changedInput schema / properties / fields / description
        Previous value: -"Which blocks to return: contact, sequences, steps, events, conversation, enriched, company. Naming one of about, audience, signals, currentRole, experience, education, skills narrows \"enriched\" to that part of the bought research. profileUrl and name always come back. Omit this and one profileUrl returns everything, while profileUrls returns everything except the bought research."New value: +"Which blocks to return: contact, sequences, steps, events, conversation, enriched, company, posts. Naming one of about, audience, signals, currentRole, experience, education, skills narrows \"enriched\" to that part of the bought research. profileUrl and name always come back. Omit this and one profileUrl returns everything, while profileUrls returns everything except the bought research."
      • changedInput schema / properties / profileUrls / description
        Previous value: -"Several contacts in ONE call, max 50. Each is resolved independently: one refusal does not stop the rest, and the reply reports every one by URL. Unless `fields` says otherwise this leaves out enriched and company — they are the largest blocks and a whole queue rarely wants them — and says so in the reply."New value: +"Several contacts in ONE call, max 50. Each is resolved independently: one refusal does not stop the rest, and the reply reports every one by URL. Unless `fields` says otherwise this leaves out enriched and company and posts — they are the largest blocks and a whole queue rarely wants them — and says so in the reply."
    • Changedli_create_campaign3 fields changed
      • changedInput schema / properties / csvText / description
        Previous value: -"CSV text with profile_url plus optional connection_note and step text columns (first_message, follow_up_1, follow_up_2, follow_up_3, closer). At most 50 rows, shared with `targets`."New value: +"CSV text with profile_url plus optional connection_note and step text columns (first_message, follow_up_1, follow_up_2, follow_up_3, closer), and optional engage_post_url / engage_reaction (a post of theirs the account reacts to a day or more before the invite; see engagePostUrl on targets). At most 50 rows, shared with `targets`."
      • addedInput schema / properties / targets / items / properties / engagePostUrl
        Added value: +{
        +  "description": "Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it a day or more BEFORE the invite goes out (about one in seven is only viewed), and the invite then waits 20-60 hours. Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it.",
        +  "type": "string"
        +}
      • addedInput schema / properties / targets / items / properties / engageReaction
        Added value: +{
        +  "description": "Which reaction to leave on engagePostUrl: LIKE (default), PRAISE (Celebrate), EMPATHY (Love), INTEREST (Insightful), APPRECIATION (Support) or ENTERTAINMENT (Funny). Needs engagePostUrl.",
        +  "type": "string"
        +}
    • Addedli_fetch_posts
    • Changedli_ingest_prospects2 fields changed
      • addedInput schema / properties / prospects / items / properties / engagePostUrl
        Added value: +{
        +  "description": "Engage-before-invite: ONE of this person's own recent posts (https://www.linkedin.com/feed/update/urn:li:activity:<id>/ or a /posts/ link), the one the note and first message mention. The sending account visits their profile and reacts to it a day or more BEFORE the invite goes out (about one in seven is only viewed), and the invite then waits 20-60 hours. Connection stage only: ignored for message-stage prospects. A link that is not a LinkedIn post refuses the row. If the post is gone or turns out not to be theirs, nothing is clicked and the invite goes ahead without it.",
        +  "type": "string"
        +}
      • addedInput schema / properties / prospects / items / properties / engageReaction
        Added value: +{
        +  "description": "Which reaction to leave on engagePostUrl: LIKE (default), PRAISE (Celebrate), EMPATHY (Love), INTEREST (Insightful), APPRECIATION (Support) or ENTERTAINMENT (Funny). Needs engagePostUrl.",
        +  "type": "string"
        +}
    • Addedli_react_to_post
    • Addedli_search_leads
  13. 62 tool updates
    • Changedli_accept_invitations2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_accepted_pending_message3 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most rows to return (default 100); `truncated` says when there are more"
      • changedInput schema / properties / sessionId / description
        Previous value: -"Scope to one account id"New value: +"Limit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Scope to one account label (omit for all accounts)"New value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account."
    • Changedli_booking_due2 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most rows to return (default 50, max 200); more are flagged"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
    • Changedli_bookings4 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most rows to return (default 100, max 500)"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see."
      • addedInput schema / properties / status / description
        Added value: +"Booking states to include (default [\"booked\"])"
    • Changedli_campaign_contacts1 field changed
      • addedInput schema / properties / campaignId / description
        Added value: +"Campaign id from li_list_campaigns"
    • Changedli_campaign_status1 field changed
      • addedInput schema / properties / campaignId / description
        Added value: +"Campaign id from li_list_campaigns"
    • Changedli_check_acceptance2 fields changed
      • changedInput schema / properties / sessionId / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_connections2 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most invite rows to return (default 50, max 200); `total` is the full count"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account."
    • Changedli_create_campaign13 fields changed
      • addedInput schema / properties / name / description
        Added value: +"Campaign name, shown on the dashboard and in li_list_campaigns"
      • addedInput schema / properties / targets / items / properties / collaborationAngle / description
        Added value: +"The angle the sequence leads with"
      • addedInput schema / properties / targets / items / properties / company / description
        Added value: +"Current employer"
      • addedInput schema / properties / targets / items / properties / context / description
        Added value: +"Free-text notes on how you found them or why now"
      • addedInput schema / properties / targets / items / properties / fitQuality / description
        Added value: +"Your fit grade, e.g. strong / medium / weak"
      • addedInput schema / properties / targets / items / properties / headline / description
        Added value: +"LinkedIn headline"
      • addedInput schema / properties / targets / items / properties / icpBucket / description
        Added value: +"Your ICP bucket for them (free text)"
      • addedInput schema / properties / targets / items / properties / location / description
        Added value: +"Location as shown on the profile"
      • addedInput schema / properties / targets / items / properties / name / description
        Added value: +"Full name"
      • addedInput schema / properties / targets / items / properties / productUrl / description
        Added value: +"Their product or company URL"
      • addedInput schema / properties / targets / items / properties / role / description
        Added value: +"Current job title"
      • addedInput schema / properties / targets / items / properties / whatTheySell / description
        Added value: +"What their company sells, in a line"
      • addedInput schema / properties / targets / items / properties / whyTheyFit / description
        Added value: +"Why they fit, in a line"
    • Changedli_create_icp2 fields changed
      • addedInput schema / properties / description / description
        Added value: +"Optional note on who this audience is"
      • addedInput schema / properties / name / description
        Added value: +"Label name, unique within your team (check li_list_icps first)"
    • Changedli_create_post2 fields changed
      • changedInput schema / properties / sessionId / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_create_strategy3 fields changed
      • addedInput schema / properties / description / description
        Added value: +"Optional note on when to use this template"
      • addedInput schema / properties / name / description
        Added value: +"Template name, unique within your team (check li_list_strategies first)"
      • addedInput schema / properties / steps / items / properties / kind / description
        Added value: +"Which message this step is; each kind at most once"
    • Changedli_daily_digest1 field changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
    • Changedli_detect_account_tier1 field changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
    • Changedli_due_messages3 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most rows to return (default 100); `truncated` says when there are more"
      • changedInput schema / properties / sessionId / description
        Previous value: -"Scope to one account id"New value: +"Limit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Scope to one account label (omit for all accounts)"New value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account."
    • Changedli_enrich_company2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_enrich_profiles2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_ingest_prospects23 fields changed
      • addedInput schema / properties / icpId / description
        Added value: +"ICP label id from li_list_icps (or use icpName); optional"
      • addedInput schema / properties / icpName / description
        Added value: +"ICP label by name, from li_list_icps; optional"
      • addedInput schema / properties / prospects / description
        Added value: +"1-200 prospects, each with profileUrl and the prefilled text for every strategy step (plus connectionNote for invites)"
      • addedInput schema / properties / prospects / items / properties / closer / description
        Added value: +"Text for the \"closer\" step"
      • addedInput schema / properties / prospects / items / properties / collaborationAngle / description
        Added value: +"The angle the sequence leads with"
      • addedInput schema / properties / prospects / items / properties / company / description
        Added value: +"Current employer"
      • addedInput schema / properties / prospects / items / properties / connectionNote / description
        Added value: +"Note sent WITH the invite (connection stage only; free accounts cannot deliver notes)"
      • addedInput schema / properties / prospects / items / properties / context / description
        Added value: +"Free-text notes on how you found them or why now"
      • addedInput schema / properties / prospects / items / properties / firstMessage / description
        Added value: +"Text for the \"first\" step, sent connectionWaitHours after acceptance"
      • addedInput schema / properties / prospects / items / properties / fitQuality / description
        Added value: +"Your fit grade, e.g. strong / medium / weak"
      • addedInput schema / properties / prospects / items / properties / followUp1 / description
        Added value: +"Text for the \"followup_1\" step"
      • addedInput schema / properties / prospects / items / properties / followUp2 / description
        Added value: +"Text for the \"followup_2\" step"
      • addedInput schema / properties / prospects / items / properties / followUp3 / description
        Added value: +"Text for the \"followup_3\" step"
      • addedInput schema / properties / prospects / items / properties / headline / description
        Added value: +"LinkedIn headline"
      • addedInput schema / properties / prospects / items / properties / icpBucket / description
        Added value: +"Your ICP bucket for them (free text)"
      • addedInput schema / properties / prospects / items / properties / location / description
        Added value: +"Location as shown on the profile"
      • addedInput schema / properties / prospects / items / properties / name / description
        Added value: +"Full name"
      • addedInput schema / properties / prospects / items / properties / productUrl / description
        Added value: +"Their product or company URL"
      • addedInput schema / properties / prospects / items / properties / role / description
        Added value: +"Current job title"
      • addedInput schema / properties / prospects / items / properties / timezone / description
        Added value: +"IANA timezone, e.g. Asia/Kolkata; used by prospect-local sending windows"
      • addedInput schema / properties / prospects / items / properties / whatTheySell / description
        Added value: +"What their company sells, in a line"
      • addedInput schema / properties / prospects / items / properties / whyTheyFit / description
        Added value: +"Why they fit, in a line"
      • addedInput schema / properties / strategyId / description
        Added value: +"Sequence template id from li_list_strategies (or use strategyName)"
    • Changedli_invitation_decisions6 fields changed
      • addedInput schema / properties / changedSinceDecision / description
        Added value: +"true = only people whose headline or company moved since they were judged"
      • addedInput schema / properties / decision / description
        Added value: +"Only this decision: accepted, rejected, wrong_lane, failed or deferred"
      • addedInput schema / properties / icp / description
        Added value: +"Only decisions filed under this ICP label"
      • addedInput schema / properties / offset / description
        Added value: +"Rows to skip, for paging (default 0)"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see."
    • Changedli_job_result2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see."
    • Changedli_launch_campaign1 field changed
      • addedInput schema / properties / campaignId / description
        Added value: +"Campaign id from li_create_campaign or li_list_campaigns"
    • Changedli_list_conversations2 fields changed
      • changedInput schema / properties / sessionId / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_mark_booked2 fields changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
      • addedInput schema / properties / note / description
        Added value: +"Optional note for the contact timeline"
    • Changedli_mark_booking_cancelled2 fields changed
      • addedInput schema / properties / calendarEventId / description
        Added value: +"The calendar event id recorded with li_mark_booked"
      • addedInput schema / properties / note / description
        Added value: +"Optional note for the contact timeline"
    • Changedli_mark_invitations9 fields changed
      • addedInput schema / properties / decisions / description
        Added value: +"1-200 decisions, one per inviter; each is applied or refused on its own"
      • addedInput schema / properties / decisions / items / properties / campaignId / description
        Added value: +"The campaign they joined, for an accepted person"
      • addedInput schema / properties / decisions / items / properties / decision / description
        Added value: +"accepted (only after a confirmed accept), rejected, wrong_lane, failed or deferred"
      • addedInput schema / properties / decisions / items / properties / icp / description
        Added value: +"ICP label you judged them against"
      • addedInput schema / properties / decisions / items / properties / profileUrl / description
        Added value: +"The inviter, by profile URL (or memberId)"
      • addedInput schema / properties / decisions / items / properties / reason / description
        Added value: +"Why; required for rejected and wrong_lane"
      • addedInput schema / properties / decisions / items / properties / routeTo / description
        Added value: +"wrong_lane only: the account that should reach them cold instead"
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_mark_outcome1 field changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
    • Changedli_mark_replied2 fields changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
      • addedInput schema / properties / note / description
        Added value: +"Optional note for the contact timeline"
    • Changedli_mark_reply_handled2 fields changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
      • addedInput schema / properties / note / description
        Added value: +"Optional note for the contact timeline, e.g. what you answered"
    • Changedli_mark_stopped2 fields changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
      • addedInput schema / properties / reason / description
        Added value: +"Why they are finished with, stored on the timeline (e.g. \"not a fit\")"
    • Changedli_marked_conversations2 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most rows to return (default 100, max 200)"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
    • Changedli_match_contacts2 fields changed
      • addedInput schema / properties / candidates / description
        Added value: +"1-200 calendar bookings to match, one per event, as read from the calendar"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
    • Changedli_move_contact1 field changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id, instead of profileUrl"
    • Changedli_new_connections1 field changed
      • addedInput schema / properties / limit / description
        Added value: +"Most new people to return (1-500)"
    • Changedli_outcome_summary1 field changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
    • Changedli_outreach_analytics4 fields changed
      • addedInput schema / properties / groupBy / description
        Added value: +"Row grouping: strategy, icp, campaign or account"
      • addedInput schema / properties / sendWindowMode / description
        Added value: +"Filter `localHours` to accounts in this send-window mode (account | prospect | both); rows are not filtered"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Omit to cover every account you can see."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Omit to cover every account you can see."
    • Changedli_pause_campaign1 field changed
      • addedInput schema / properties / campaignId / description
        Added value: +"Campaign id from li_list_campaigns"
    • Changedli_pending_invites2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_read_conversation2 fields changed
      • changedInput schema / properties / sessionId / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_read_conversations_bulk2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_received_invitations2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_replies_to_answer3 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most rows to return (default 100); `truncated` says when there are more"
      • changedInput schema / properties / sessionId / description
        Previous value: -"Scope to one account id"New value: +"Limit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Scope to one account label (omit for all accounts)"New value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account."
    • Changedli_reset_contact2 fields changed
      • addedInput schema / properties / contactId / description
        Added value: +"The contact id from li_contact_dossier or li_campaign_contacts"
      • addedInput schema / properties / profileUrl / description
        Added value: +"The contact, by LinkedIn profile URL (or use contactId)"
    • Changedli_resume_campaign1 field changed
      • addedInput schema / properties / campaignId / description
        Added value: +"Campaign id from li_list_campaigns"
    • Changedli_schedule_post1 field changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
    • Changedli_scheduled_posts3 fields changed
      • addedInput schema / properties / limit / description
        Added value: +"Most posts to return, newest scheduled time first (default 50, max 200)"
      • addedInput schema / properties / sessionId / description
        Added value: +"Limit to one account by id (li_list_accounts). Needed (or sessionLabel) when you have more than one account."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Omit to list across every account"New value: +"Limit to one account by label, LinkedIn name, profile slug or member id. Needed (or sessionId) when you have more than one account."
    • Changedli_selector_canary2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_send_bulk_messages3 fields changed
      • addedInput schema / properties / items / description
        Added value: +"Recipients and their exact text, one per person; batches of 15 or fewer are safest"
      • addedInput schema / properties / items / items / properties / message / description
        Added value: +"Exact text for this recipient, never generated"
      • addedInput schema / properties / items / items / properties / profileUrl / description
        Added value: +"Recipient profile URL"
    • Changedli_send_connection_now2 fields changed
      • changedInput schema / properties / sessionId / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_send_message3 fields changed
      • addedInput schema / properties / debugArtifactsDir / description
        Added value: +"Ignored; accepted only so older callers do not fail validation."
      • changedInput schema / properties / sessionId / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • changedInput schema / properties / sessionLabel / description
        Previous value: -"Optional saved LinkedIn session selector"New value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_session_status2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
    • Changedli_set_account_proxy2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_set_booking_settings6 fields changed
      • addedInput schema / properties / cancelRecheckHours / description
        Added value: +"Hours after a cancelled booking before it is due again (default 24, 1-336)"
      • addedInput schema / properties / closeAfterDays / description
        Added value: +"Days after a nudge before the next action becomes a close (default 3, 1-30)"
      • addedInput schema / properties / firstNudgeHours / description
        Added value: +"Hours after the booking link before the first check (default 48, 1-336)"
      • addedInput schema / properties / maxNudges / description
        Added value: +"Nudges before a close (default 1, 0-3)"
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_set_default_pricing_tier1 field changed
      • addedInput schema / properties / tier / description
        Added value: +"'founding' or 'standard'; omit to read the current default"
    • Changedli_set_office_hours10 fields changed
      • addedInput schema / properties / prospectWindow / description
        Added value: +"Prospect-local hours used by the prospect and both modes; omitted fields keep their stored values"
      • addedInput schema / properties / prospectWindow / properties / inviteEnd / description
        Added value: +"Prospect's local hour invites stop, EXCLUSIVE"
      • addedInput schema / properties / prospectWindow / properties / inviteStart / description
        Added value: +"Prospect's local hour invites may start, inclusive"
      • addedInput schema / properties / prospectWindow / properties / maxDeferHours / description
        Added value: +"Hours an invite may wait for the prospect window before the account window applies (default 72)"
      • addedInput schema / properties / prospectWindow / properties / messageEnd / description
        Added value: +"Prospect's local hour scheduled steps stop, EXCLUSIVE"
      • addedInput schema / properties / prospectWindow / properties / messageStart / description
        Added value: +"Prospect's local hour scheduled steps may start, inclusive"
      • addedInput schema / properties / prospectWindow / properties / weekdaysOnly / description
        Added value: +"true = never on the prospect's Saturday or Sunday"
      • addedInput schema / properties / sendWindowMode / description
        Added value: +"account (default): the account window gates everything; prospect: each contact's local window gates invites and scheduled steps; both: both must be open"
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_set_team_billing3 fields changed
      • addedInput schema / properties / billingCurrency / description
        Added value: +"'INR' (Razorpay) or 'USD' (PayPal); cannot change while a subscription is live"
      • addedInput schema / properties / pricingTier / description
        Added value: +"'founding' or 'standard' — the plan the team checks out on next"
      • addedInput schema / properties / teamId / description
        Added value: +"Team id (or use email)"
    • Changedli_set_team_complimentary1 field changed
      • addedInput schema / properties / teamId / description
        Added value: +"Team id (or use the email of anyone in it)"
    • Changedli_snooze_reply1 field changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
    • Changedli_track_booking1 field changed
      • addedInput schema / properties / contactId / description
        Added value: +"One contact by id (from li_contact_dossier or li_campaign_contacts), instead of profileUrl"
    • Changedli_unmark_invitation5 fields changed
      • addedInput schema / properties / confirm / description
        Added value: +"Must be true to delete the decision"
      • addedInput schema / properties / memberId / description
        Added value: +"The inviter's member id (ACoAA…), as li_received_invitations returns it"
      • addedInput schema / properties / profileUrl / description
        Added value: +"The inviter, by profile URL (or use memberId)"
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_verify_account1 field changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
    • Changedli_withdraw_invites2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Needed (or sessionLabel) when you have more than one account; never guessed."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Needed (or sessionId) when you have more than one account; never guessed."
    • Changedli_withdraw_status2 fields changed
      • addedInput schema / properties / sessionId / description
        Added value: +"Account id from li_list_accounts. Omit (with sessionLabel) to use your default account."
      • addedInput schema / properties / sessionLabel / description
        Added value: +"Account by label, LinkedIn name, profile slug or member id (li_list_accounts). Omit (with sessionId) to use your default account."
  14. 1 tool update
    • Addedli_hold_sending

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    AI agent for LinkedIn outreach: finds the right people, writes in your voice, follows up, replies. Six campaign goals: sell a product or service, find a job, hire people, find partners or investors, find a vendor, research interviews. Campaigns stay drafts until you launch them.
    22
    1,936 PyPI
    12
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables automated LinkedIn growth by posting, commenting, and managing connections through Claude Code, with daily scheduled tasks for inbox replies and connection requests.
    17
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    LinkedIn prospect intelligence inside Claude. It automates the research behind outreach, not the sending: scrapes posts, profiles and comment threads into a local SQLite database, classifies what a person or company actually posts about, mines comment threads for people already describing your problem, and surfaces uncommon commonalities worth opening with.
    18
    33
    MIT
Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources