Skip to main content
Glama

LinkedIn Campaign Contacts

li_campaign_contacts
Read-only

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.

Input Schema

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)

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed6 schema 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."
  2. Changed1 schema field changed
    • addedInput schema / properties / campaignId / description
      Added value: +"Campaign id from li_list_campaigns"
  3. First observed

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.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources