Back to guides
/Linked API

AI for Sales Prospecting on LinkedIn: A Worked Agent Workflow

Most advice on AI for sales prospecting stops at "let it write your emails". This guide walks one real workflow on LinkedIn from start to finish – who to look for, what to read, how to decide, what to send – and shows it two ways: run by an AI agent, and orchestrated in code.

The short version. AI is useful for the research-heavy half of prospecting: building a list from your ideal customer profile, reading each person's profile and recent posts, judging fit, and drafting a first note. The decision to contact and the tone stay with you. On LinkedIn that can run two ways – an AI agent working in your own account through an MCP server, an agent-friendly CLI or a ready-made skill; or your own code through a REST API, Node or Python SDKs or a CLI. Either way it reads what your account can see, a person reviews the shortlist and the notes, and each invitation is sent only after approval, at a human pace within per-action limits.

What AI should and should not do in prospecting

Delegate to AIKeep with a person
Building a candidate list from your ICPDeciding who to contact
Reading profiles and recent postsThe final tone of a note
Writing a fit rationale with sourcesAnything after the first touch
Drafting a first notePricing and timing conversations

The reason for the split is practical: an agent that sends unreviewed messages at volume costs you replies, reputation and, eventually, account health. Research and drafting are where AI saves real time; judgement is where it should hand back.

The worked example: from ICP to an approved invitation

The example below is illustrative – the company, the offer and the sample output are invented to show the workflow, and no live invitation was sent while writing it.

The seller and the offer. A RevOps analytics product that flags stale pipeline. The one offer sentence the note may use, at most once: "we help RevOps teams spot stale pipeline before forecast calls".

The ideal customer profile.

  • Title: head, director or VP of Revenue Operations.
  • Location: United Kingdom.
  • Industry: Software Development.
  • Company size: 50–500 employees.

The workflow, step by step.

  1. Find candidates. People search with the position, location and industry filters, capped at 25 results. The standard people search used here has no company-size filter, so size is checked later.
  2. Gather evidence. For each candidate, fetch the profile with up to five posts from the last 90 days. Then fetch their current company from the company link on the profile, which returns employee count and industry. Every fact keeps the link it came from – the profile, the company page or the post.
  3. Qualify with explicit rules.
    • Exclude – the title is not a head, director or VP of Revenue Operations; it includes "former" or "ex-"; the company is outside 50–500 employees or not in Software Development; or the company is on your competitor list.
    • Fit – passes every ICP check.
    • Strong fit – fit, plus a post in the last 90 days that mentions pipeline, forecast, CRM or data quality. The rationale links that post.
    • Unknown – the company could not be fetched or reports no employee count. It goes to review flagged, never approved automatically.
  4. Draft a note. At most 200 characters – LinkedIn's limit for free accounts (300 with Premium). It refers to the cited post or the role, uses the offer sentence at most once, and avoids flattery, invented familiarity and "I saw you viewed my profile". Free accounts can add a note to only a few invitations a month (LinkedIn Help), so on a free account most of a 25-person list goes without one: once the cap is reached, invitations with a note fail with that reason, and you either send without a note or use Premium.
  5. Review. A person reads each rationale and draft, then approves, edits or drops it. Edits are recorded.
  6. Send the approved invitations and record the result. Each send ends as sent, already pending, already connected, or failed with the reason – for example a note over the length limit, a person who requires their email address, or a free account that has used up its personalized notes.

One record per person carries the whole trail – an illustrative example:

json
{
  "name": "Alex Morgan",
  "profileUrl": "https://www.linkedin.com/in/alex-morgan-example",
  "rule": "strong-fit",
  "rationale": [
    "Title: Head of Revenue Operations (https://www.linkedin.com/in/alex-morgan-example)",
    "Company: 180 employees, Software Development (https://www.linkedin.com/company/example-analytics)",
    "Recent post: https://www.linkedin.com/posts/example"
  ],
  "draftNote": "Hi Alex, your recent post on forecasting caught my attention – we help RevOps teams spot stale pipeline before forecast calls. Happy to connect.",
  "review": { "decision": "approve", "edited": false },
  "outcome": "sent",
  "date": "2026-10-05"
}

Checking the output. Before you trust the workflow, spot-check it:

  • does each cited source actually say what the rationale claims?
  • reject any fit or intent claim that is not backed by a link;
  • note what reviewers changed – repeated edits mean the rules or the template need work;
  • track research and review time per person.

Sales value is judged later, by you, through qualified replies and meetings.

Run it with an AI agent

The same workflow runs from a plain-language instruction. Connect an assistant such as Claude to your LinkedIn account through the MCP server, the agent-friendly CLI or a ready-made skill – setup is covered in giving an AI agent access to LinkedIn. The agent then uses tools such as search_people, fetch_person, fetch_company and send_connection_request.

A runnable instruction – the agent's behaviour depends on how you set it up, so treat this as a starting point, not an observed run:

Find heads, directors or VPs of Revenue Operations in the United Kingdom at Software
Development companies, up to 25 people. For each, read their profile and posts from the
last 90 days, then check their company's employee count and industry.

Apply these rules: exclude titles that are not head/director/VP of RevOps, "former" or
"ex-" titles, companies outside 50–500 employees or not in Software Development, and
these competitors: [list]. Mark "strong fit" if a recent post mentions pipeline,
forecast, CRM or data quality, and cite that post's URL. Mark "unknown" if the company
size is missing.

For every fit, draft a connection note under 200 characters that refers to the cited
post or the role, uses "we help RevOps teams spot stale pipeline before forecast calls"
at most once, and never mentions profile views.

Stop and show me a table: name, profile URL, rule, rationale with links, draft note.
Send invitations only for the rows I approve, then report each outcome.

The pause before sending is part of your instruction, not a built-in approval queue – keep it explicit in the prompt.

Or orchestrate it in code

The same contract, in code. Qualification and drafting are deterministic rules here, so the reference path needs no LLM.

typescript
import LinkedApi from '@linkedapi/node';
import { readFileSync, writeFileSync } from 'node:fs';

const linkedapi = new LinkedApi({
  linkedApiToken: 'your-linked-api-token',
  identificationToken: 'your-identification-token',
});

const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
const NOTE_LIMIT = 200;
const COMPETITORS = ['Example Competitor Ltd'];
const OFFER = 'we help RevOps teams spot stale pipeline before forecast calls';
const SIGNAL_TOPICS = [
  { pattern: /pipeline/i, topic: 'pipeline' },
  { pattern: /forecast/i, topic: 'forecasting' },
  { pattern: /\bcrm\b/i, topic: 'CRM data' },
  { pattern: /data quality/i, topic: 'data quality' },
];

async function findCandidates() {
  const workflow = await linkedapi.searchPeople.execute({
    filter: {
      position: 'Revenue Operations',
      locations: ['United Kingdom'],
      industries: ['Software Development'],
    },
    limit: 25,
  });
  const { data } = await linkedapi.searchPeople.result(workflow.workflowId);
  return data ?? [];
}

async function gatherEvidence(personUrl: string) {
  const personRun = await linkedapi.fetchPerson.execute({
    personUrl,
    retrievePosts: true,
    postsRetrievalConfig: { limit: 5, since: new Date(Date.now() - NINETY_DAYS_MS).toISOString() },
  });
  const { data: person } = await linkedapi.fetchPerson.result(personRun.workflowId);
  if (!person) return undefined;
  if (!person.companyHashedUrl) return { person, company: undefined };

  const companyRun = await linkedapi.fetchCompany.execute({ companyUrl: person.companyHashedUrl });
  const { data: company } = await linkedapi.fetchCompany.result(companyRun.workflowId);
  return { person, company };
}

type TEvidence = NonNullable<Awaited<ReturnType<typeof gatherEvidence>>>;

function qualify({ person, company }: TEvidence) {
  const title = person.position ?? '';
  const titleFact = `Title: ${title} (${person.publicUrl})`;
  const isRevOpsLeader = /revenue operations/i.test(title) && /\b(head|director|vp|vice president)\b/i.test(title);
  if (!isRevOpsLeader || /\b(former|ex-)/i.test(title)) return { rule: 'exclude', rationale: [titleFact] };
  if (!company?.employeesCount) return { rule: 'unknown', rationale: [titleFact, 'Company size not available'] };

  const size = company.employeesCount;
  const companyFact = `Company: ${size} employees, ${company.industry} (${company.publicUrl})`;
  const isIcpCompany = size >= 50 && size <= 500 && company.industry === 'Software Development';
  if (!isIcpCompany || COMPETITORS.includes(company.name)) return { rule: 'exclude', rationale: [companyFact] };

  const rationale = [titleFact, companyFact];
  for (const post of person.posts ?? []) {
    const signal = SIGNAL_TOPICS.find(({ pattern }) => pattern.test(post.text ?? ''));
    if (signal) return { rule: 'strong-fit', rationale: [...rationale, `Recent post: ${post.url}`], topic: signal.topic };
  }
  return { rule: 'fit', rationale };
}

function draftNote(firstName: string, topic?: string) {
  const opener = topic
    ? `your recent post on ${topic} caught my attention`
    : `I'm reaching out to RevOps leaders in UK software`;
  const note = `Hi ${firstName}, ${opener} – ${OFFER}. Happy to connect.`;
  return note.length <= NOTE_LIMIT ? note : `Hi ${firstName}, ${OFFER}. Happy to connect.`;
}

async function writeReviewFile(path: string) {
  const rows = [];
  for (const candidate of await findCandidates()) {
    const evidence = await gatherEvidence(candidate.publicUrl);
    if (!evidence) continue;
    const { rule, rationale, topic } = qualify(evidence);
    if (rule === 'exclude') continue;
    const firstName = candidate.name.split(' ')[0] ?? candidate.name;
    rows.push({
      name: candidate.name,
      profileUrl: candidate.publicUrl,
      rule,
      rationale,
      draftNote: draftNote(firstName, topic),
      review: { decision: 'pending', edited: false },
    });
  }
  writeFileSync(path, JSON.stringify(rows, null, 2));
}

async function sendApproved(path: string) {
  const rows = JSON.parse(readFileSync(path, 'utf8'));
  for (const row of rows) {
    if (row.review.decision !== 'approve') continue;
    const invite = await linkedapi.sendConnectionRequest.execute({ personUrl: row.profileUrl, note: row.draftNote });
    const { errors } = await linkedapi.sendConnectionRequest.result(invite.workflowId);
    const errorType = errors[0]?.type;
    row.outcome = errorType === undefined ? 'sent'
      : errorType === 'alreadyPending' || errorType === 'alreadyConnected' ? errorType
      : `failed: ${errorType}`;
    row.date = new Date().toISOString().slice(0, 10);
  }
  writeFileSync(path, JSON.stringify(rows, null, 2));
}

Run writeReviewFile (or write_review_file) and open the file. For each row, set review.decision to approve or drop; if you rewrite draftNote, set review.edited to true. Then run sendApproved (or send_approved). Every call is asynchronous – execute starts the work and returns a workflow ID, result waits for it – and action failures come back in errors, not as exceptions.

Optional: an LLM for drafting. To get richer drafts, replace draftNote with a call to your model that receives the person's rationale and the same constraints – the length limit, the offer sentence at most once, no flattery. Keep the length check and the review step either way.

Which tool does which part

Part of the jobFits bestWhen another route is better
Research and drafting from sources you supplyA general AI assistant such as ChatGPT or ClaudeIt needs a LinkedIn tool for live data – on its own it works from what you paste in
Account-visible LinkedIn research and the approved first touchLinked API, run by an agent or from codeWhen your motion is a fixed multi-step sequence you would rather manage in a UI
Email and phone sourcingA verified contact-data providerLinked API returns neither
Managed follow-up after the first touchAn established CRM or sequencerWhen follow-ups need multichannel timing, templates and a shared team inbox

Guardrails: review, pace and limits

Two kinds of control keep this safe, and they are not the same thing:

  • Review and approval are yours. The pause before sending lives in your agent instruction or your code, as above.
  • Action caps are the platform's. Linked API enforces per-action limits by category – profile views, connection requests, messages, search queries – over daily, weekly or monthly periods, and you can set your own (limits API).

Keep the pace human, stay inside LinkedIn's practical limits, and treat invitations as one touch in a wider plan – more in automating connection requests. Before you message instead of invite, check the connection degree.

What this does not do

  • It does not find email addresses or phone numbers.
  • It works only with what your connected account can see on LinkedIn.
  • It does not replace a sequencer's multi-step follow-up – see LinkedIn automation tools for that category.
  • Drafts need a human eye before they go out.

Frequently Asked Questions (FAQ)

Yes, the research-heavy part: building a list from your ICP, reading profiles and recent posts, judging fit and drafting a first note. The decision to contact and the tone of the message are best kept with a person.

Give the assistant LinkedIn actions: ChatGPT and Claude connect through the MCP server; coding agents such as Claude Code, Cursor and Codex can also use the agent-friendly CLI or a ready-made skill. Then describe the workflow in plain language – who to find, what to check, how to draft – and tell it to stop for your approval before sending anything.

Any automation carries some risk. What keeps it in check is how it runs: on your own account, in a dedicated cloud browser, at a human pace, within per-action limits you configure, with a person approving each invitation.

No. The workflow above uses standard LinkedIn search and profiles. If your account has a Sales Navigator seat, the Plus plan adds Sales Navigator actions.

Send unreviewed messages at volume. An agent can research and draft at scale; whether and how to contact someone stays a human decision.

Build it in, or hand it to an agent

Use it both ways. Build the workflow into your own tools with the REST API, the Node and Python SDKs or the CLI. Or have an AI agent run it out of the box through the MCP server, the agent-friendly CLI or a ready-made skill. Plans start at $49 a seat a month billed annually – see pricing.