DEV Community

Kevin Hall
Kevin Hall

Posted on

Champion tracking in Claude Code: who in my CRM changed jobs

Every CRM goes stale the same way. The record says she runs sales ops at Acme. She left last spring. The sequence still writes to her old address, and her new company, where she already knows your product, never hears from anyone.

Champion tracking tools sell the fix as a subscription that watches your contacts. Most of the value is in a one-off check: the profile URLs you already have, one Apify Actor and a comparison that does not lie to you. The comparison is the part people get wrong, so most of this post is about it.

I tested it on a list where I already knew the answer. Eighty people from four sales-software companies, the kind of accounts a vendor keeps in its CRM. Half were still listed at the company and half had left it since 2024, half and half on purpose, so nothing below says how fast a real CRM decays. A list with a known answer lets you check the check before you trust it on the real thing. Rows below show roles and dates only, no names and no addresses.

In one paragraph: export the profile URL and the company domain from the CRM, run Profile Lookup on the URLs, turn the domains into company records with Domain to Company, and call someone moved only when none of their open positions is at that company. Then get the movers' new work address and find out who took the seat they left.

1. Who still works where the CRM says

The input is the list. Full links, bare handles and mobile links all work.

{
  "profileUrls": [
    "https://www.linkedin.com/in/jane-doe-example",
    "john-roe-example"
  ],
  "contacts": true
}
Enter fullscreen mode Exit fullscreen mode

Eighty profiles came back in seconds. Each row is the whole career record, and the part that matters here is experience.work: every position with a start date and an end date. An open position has no end date.

Your CRM stores the company as a name and maybe a domain. A name is a bad key (Acme, Acme Inc, Acme Software), so resolve the domain first. Domain to Company takes {"domains": [...]}, and the linkedinUrl on its row ends with the company slug. The same slug sits in companyId.publicId on every position of the profile. After that the check is a few lines:

def verdict(row, crm_slug):
    if row["_status"] != "found":
        return row["_status"]            # not_found, profile_removed: free rows
    open_jobs = [w for w in row["experience"]["work"] if not w.get("endDate")]
    if not open_jobs:
        return "no_open_job"
    slugs = {(w.get("companyId") or {}).get("publicId") for w in open_jobs}
    return "still_there" if crm_slug in slugs else "moved"
Enter fullscreen mode Exit fullscreen mode

crm_slug is row["linkedinUrl"].rstrip("/").split("/")[-1] from the Domain to Company row.

On my test list it got every leaver right, except one person whose profile had been removed. That comes back as a free row and is a contact to archive anyway. On the half that was supposed to still be there it found two people with no open job at all: their position at the company had ended in spring 2025. The roster I built the test list from was behind their profiles. Which is the whole reason to check the profile and not a list of employees.

The trap: companyName is not the employer

Every row also has a companyName column, and it is tempting to compare that with the CRM. Don't. It holds the first open position on the profile, and people keep plenty of open positions that are not their job: a board seat, "Member" of a sales community, an advisor title, their own consulting shop, a founder title on a meetup they started years ago.

On my list about a quarter of the people who never left would have been marked as moved by that one column. A real row, names replaced:

{
  "companyName": "A sales community",
  "experience": { "work": [
    { "company": "A sales community", "companyId": { "publicId": "a-sales-community" },
      "title": "Member", "startDate": "2024-12-01", "endDate": null },
    { "company": "Acme", "companyId": { "publicId": "acme" },
      "title": "Senior Director, Account Development", "startDate": "2022-05-01", "endDate": null }
  ]}
}
Enter fullscreen mode Exit fullscreen mode

It goes the other way too. Some people never close the old job when they start a new one, so the profile shows the CRM company and a newer company, both open. My check calls that "still there". Look at the start dates and the headline: if the newest open job is somewhere else and the headline names it, they moved.

2. They moved: a warm account and a new address

The new company is the interesting part. Someone there already used your product and liked it enough to be your contact. That is the warmest cold account you will ever get.

With contacts: true the row also says what is on record for reaching them. About a third of my movers had an address at the new company (employerMatch: true), most of the rest only a personal one, and the others an old work address or nothing. For the ones without a work address, Work Email Finder takes the name and the new company's domain.

The uncomfortable bit: for almost half of the movers the address at the old company was still on record. That is the address sitting in your CRM, the one the sequence keeps sending to. Fix those before the next campaign, not after the bounce report.

3. The CRM only has emails

Plenty of CRMs never stored a profile URL. Reverse Email Lookup starts from the address you have, including an old work address that would bounce today:

{ "emails": ["jane.doe@acme.example"] }
Enter fullscreen mode Exit fullscreen mode

I gave it the old company addresses of the movers. All but one came back as the same person, with the new job on the row. The one that didn't was a different person with the same first name. Compare the name on the row with the name in the CRM before acting on it.

4. The account lost its champion

The old account matters too. Your contact left, someone else sits in that seat now, and they have never heard of you. Company Employees on the account's domain with a word from the old title:

{
  "companies": ["acme.com"],
  "titleContains": ["revenue operations"],
  "roles": ["director", "vp"],
  "maxRows": 10
}
Enter fullscreen mode Exit fullscreen mode

Each row has positionStartYear. A revenue operations director left one of my accounts in 2024, and the roster showed a couple of revenue operations leaders who started in 2025. That is who to write to. A VP of inside sales left another account, and nobody holds that title there now, only a manager. The seat was folded or renamed, which is good to know before you write "congrats on the new role".

5. The same check from Claude Code

Every Actor here is also an MCP tool. I pinned only the one I needed:

claude mcp add --transport http apify "https://mcp.apify.com?tools=b2bsearch/profile-lookup" --header "Authorization: Bearer <APIFY_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

Then a Claude Code session with that one tool, shell and file tools switched off, and twenty rows of the test list as CSV: profile URL, company, domain. The prompt without the CSV:

This is an export from our CRM: contacts we sold to or talked to in 2024, with the company we have on file for each.

Before we start sequences again I need to know who still works at the company on file and who moved, and for the ones who moved, where they work now. Use only the Apify MCP tools you have. One paid pass over the list is fine, do not run the same lookup twice. Give me a short table: contact number, still there / moved / unclear, current company, and anything I should double-check by hand.
Enter fullscreen mode Exit fullscreen mode

It ran one lookup over the twenty, read the dataset twice and answered in under two minutes. All ten leavers came back as moved, with the new company and, for most, the month they started there. It did not fall for companyName: it pointed out on its own that the board seat, the community and the advisor title were not the employer. It caught the person with two open jobs and marked them "probably moved", because the headline named the new company. And it flagged two contacts whose job at the company on file had ended before 2024 or right at its start. In a real CRM that means the record was probably wrong the day it was entered.

Three people it left as unclear, and the reason is worth knowing. It asked for compact rows, and a compact row carries a shortened list of positions, so for those three the job at the CRM company was not in it. It said so and asked before paying for a second pass, as the prompt told it to. Next time "use full rows, not compact" goes into the prompt.

Things that bit me

  • Dates. Every row has updatedAt, the day the record was last refreshed, and on my list most were from this spring. A move after that date does not show up. Compare it with the day the contact entered your CRM: old CRM rows are where this check pays off.
  • A cost cap on the call. My first API run stopped partway with "stopped at your maximum charge limit" in the status line. The call had a default cap below what the list needed. Set maxTotalChargeUsd yourself or split the list. Nothing past the cap is charged.
  • A domain that matches two companies. One domain I tried came back ambiguous from Domain to Company. Pick the company by hand, or keep the slug in the CRM next to the domain.
  • Two open jobs. Covered above, and it happens more than you would think. The start dates decide.

The recipe

  1. Export profile URL (or email), company and company domain from the CRM.
  2. Domain to Company on the domains, keep the slug.
  3. Profile Lookup on the URLs with contacts: true and full rows. Reverse Email Lookup if the address is all you have.
  4. Moved means no open position at the CRM company's slug. Not a different companyName.
  5. For movers: update the record, add the new work address, open the new company as an account.
  6. For the old accounts: Company Employees with the old title, newest positionStartYear first.

Run it before a big campaign, on contacts older than the dates on the rows.

If you want the people who left a customer as a list, rather than checking the CRM you have, Former Employees Finder does it from the domain, and the alumni post shows how. The address side for a list of profile URLs is in the bulk email finder post.

FAQ

Is this a job-change tracker? No. It checks a list against the profiles as they are on record. Run it again in a few months and you get whatever changed on those records since.

Do I pay for misses? No. A URL that matches nobody comes back as a free not_found row, a removed profile as a free profile_removed row.

Does it need my login or a browser extension? No. The answers come from a database, so there are no cookies, sessions or proxies on your side.

If your CRM keeps something I did not cover here, a title history or a deal stage, tell me in the comments and I will try the check on it.

Top comments (1)

Collapse
 
alexcodebytes profile image
Oleksandr •

The companyName trap is such a crucial insight. I would have absolutely fallen for it and flagged half my list as "moved" because they joined a community or took an advisor role. Checking for the CRM company's slug inside the experience.work array is the correct, bulletproof way to do it.

I also love the honesty about the test setup — building a list with a known 50/50 answer to verify the logic before trusting it on real data is the mark of someone who actually does this for a living. The part where the agent caught the "member" and "advisor" roles on its own without being told is impressive.

The note about old addresses still being on record for half the movers is a stark reminder of how much dead data sits in CRMs.