# Job Change Tracker — Contacts Who Moved to a New Company (`inovaflow/job-change-tracker`) Actor

Your contacts (name + last-known company) in, job changes out: who moved, new company, title, evidence URL, new domain and a pattern-matched, MX-checked work e-mail. Confirmed by the company site or two sources; per-watch memory reports each move once. No login, dataset-only, MCP-ready.

- **URL**: https://apify.com/inovaflow/job-change-tracker.md
- **Developed by:** [inovaflow](https://apify.com/inovaflow) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 job changes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Your best-converting leads are the people who already bought from you — and every year a fifth of them change jobs. **Job Change Tracker** takes your contact list (name + the company you know them at) and tells you **who moved, where and in what role**, with the public evidence, the new company's domain and the most likely new work e-mail. No LinkedIn login, no cookies, no data-vendor subscription.

- **Sales teams** — champion tracking: a buyer who moves brings their budget and their vendor list to the new company.
- **Customer success** — know when the sponsor of an account leaves before renewal.
- **Recruiters & investors** — keep a watch list of operators and founders current.
- **RevOps** — clean CRM contacts whose company field is stale; get the new domain and e-mail pattern.

### What does Job Change Tracker do?

For every contact it runs a public-profile search (the profile headline names the current employer), a press / company-site search when that points somewhere new (or nowhere), and reads the candidate company's own home / team / leadership pages. A move is **confirmed** only when the new company's site lists the person, or when two independent public sources agree; one source alone is reported as a **possible move** and never charged as a job change. Contacts still at their company come back as `no-change`, contacts the web knows nothing about as `not-found` — never a guess. Give the same `watchId` on every scheduled run and each move is reported once.

### Why use this job change tracker?

- **Confirmed, with evidence.** Every `changed: true` row carries the URL that proves it (team page, announcement, profile headline) and a confidence score.
- **Priced per check, per confirmed move.** $0.01 per contact checked, $0.05 only when a move is confirmed and only the first time. Possible moves, duplicates and unusable inputs are free.
- **The new seat, ready to contact.** Current company, title, domain, website, a pattern-matched e-mail verified down to the mail server (or the mailbox where SMTP is reachable), plus `whyNow` and `sellTo` hints.
- **No login.** Public search results, announcements and company websites only. LinkedIn pages are never fetched.
- **Runs on a schedule.** Per-watch memory: known movers are skipped, unchanged contacts are re-checked, new moves are the delta.
- **MCP-ready.** Flat rows, stable `contactId`, `sources[]`, `scrapedAt`; call it from an AI agent, Zapier / Make / n8n, or the API.

### What data does it extract?

| Field | Description |
| --- | --- |
| `fullName`, `firstName`, `lastName`, `contactId` | The contact (id = hash of name + last-known company; stable across runs) |
| `previousCompany`, `previousTitle` | What you knew (title from your input or from the sources) |
| `currentCompany`, `currentTitle` | Where the person is now, as the sources state it |
| `changed` | `true` only for a confirmed move |
| `status` | `confirmed-move` · `possible-move` (one source) · `no-change` · `not-found` |
| `confidence` | 0–100 (team page 92–97, two sources 80–85, one source 45–60, unchanged 65–90) |
| `currentDomain`, `currentWebsite` | The new company's domain and website (verified page, not a guess) |
| `newEmail`, `emailPattern`, `emailConfidence`, `emailVerification`, `emailCandidates` | Most likely new address (`first.last@`, `flast@` …, the domain's own pattern when visible on its site) with `mx-only` / `smtp-valid` / `smtp-catch-all` verification |
| `movedAt`, `announcedAt`, `changeDetectedAt` | Start date only when a source states it ("effective …"), the announcement date, when this run detected the move |
| `evidenceUrl`, `evidenceTitle`, `evidenceSnippet`, `signals[]`, `signalCount`, `sources[]` | The proof |
| `linkedinUrl` | Public profile URL as the search engine shows it |
| `seniority`, `department`, `whyNow`, `sellTo[]` | Outreach hints |
| `isNewSinceLastRun` | With a `watchId`: whether this move is new to the watch |
| `searchesUsed`, `scrapedAt`, `error` | Provenance |

### How to track job changes for your contacts

1. Open the Actor and click **Try for free**.
2. Under **Contacts to check** paste your list — JSON objects (`{"name": "Jane Doe", "company": "Acme", "title": "VP Sales"}`) or plain lines `Jane Doe, Acme, VP Sales`. A title or LinkedIn URL helps with common names.
3. Leave **Confirm on the new company's website** and **Guess the new work e-mail** on. Set a **Watch id** if you will run the same list again.
4. Click **Start**. About 10–15 seconds per contact; results stream into the **Output** tab.
5. Export as CSV / Excel / JSON, or use the **Job changes** view for the outreach-ready columns.

#### Scheduling

Create a schedule with the same input and the same `watchId`. Every run re-checks the contacts who had not moved, skips the ones already reported as moved (not charged), and reports only new moves as `isNewSinceLastRun: true`.

### How much does it cost?

Pay-per-event, no subscription: **$0.01 per contact checked** plus **$0.05 per confirmed job change** (first report only) and a small run-start fee. A 100-contact list with 20 confirmed moves ≈ $2. Compute and proxy usage is a small fraction of that (about $0.001–0.003 per contact).

### Input

Only **contacts** is required. Example:

```json
{
    "contacts": [
        { "name": "Laura Zwahlen", "company": "NextRoll", "title": "Chief Revenue Officer" },
        { "name": "Arnab Bose", "company": "Okta", "title": "Chief Product Officer" },
        { "name": "Amy Hood", "company": "Microsoft", "title": "Chief Financial Officer" }
    ],
    "onlyChanged": false,
    "findEmails": true,
    "verifyEmails": true,
    "confirmOnTeamPage": true,
    "watchId": "customers-q4"
}
```

| Input | Default | Notes |
| --- | --- | --- |
| `contacts` | — | Objects with `name`, `company` (+ optional `title`, `email`, `linkedinUrl`) or `Name, Company, Title` lines |
| `onlyChanged` | false | Deliver (and charge) only confirmed moves |
| `daysBack` | 365 | Ignore announcements older than this |
| `confirmOnTeamPage` | true | Read the candidate company's site (authoritative evidence) |
| `findEmails`, `verifyEmails` | true | New e-mail guess + MX / SMTP verification |
| `watchId` | — | Memory across runs; each move reported once |
| `maxContacts`, `maxSearchesPerContact`, `maxConcurrency` | 5000 / 3 / 6 | Limits |
| `searchEngine` | auto | Yahoo → Bing → Google SERP proxy |
| `proxyConfiguration` | residential | Fallback when a site challenges the run's own connection |

### Output sample

```json
{
    "contactId": "0c2f3c1e9c7d4a1b",
    "fullName": "Laura Zwahlen",
    "previousCompany": "NextRoll",
    "previousTitle": "Chief Revenue Officer",
    "currentCompany": "Airship",
    "currentTitle": "Chief Revenue Officer",
    "currentDomain": "airship.com",
    "changed": true,
    "status": "confirmed-move",
    "confidence": 97,
    "movedAt": null,
    "announcedAt": "2026-06-23",
    "changeDetectedAt": "2026-09-26T13:26:46.970Z",
    "evidenceUrl": "https://www.airship.com/company/why-airship/",
    "linkedinUrl": "https://www.linkedin.com/in/laura-zwahlen-2613514",
    "newEmail": "laura.zwahlen@airship.com",
    "emailPattern": "first.last",
    "emailVerification": "mx-only",
    "signals": [
        "profile-headline:linkedin.com → Airship (Chief Revenue Officer)",
        "company-site:airship.com → Airship (Chief Revenue Officer)",
        "team-page:airship.com → Airship (Chief Revenue Officer)"
    ],
    "whyNow": "Laura Zwahlen moved from NextRoll to Airship as Chief Revenue Officer (3 months ago, announced 2026-06-23) — inside the first-90-days window when a new leader picks their tools.",
    "sellTo": ["Laura Zwahlen at Airship (Chief Revenue Officer) — a champion in a new seat re-buys sales engagement, CRM/RevOps, data & enrichment", "Their successor at NextRoll — the seat they left is re-evaluating the same vendors"]
}
```

The key-value store holds `OUTPUT` (run summary: checked, confirmed, possible, unchanged, not found, e-mails, search-engine stats, watch state) and `CONTACTS.csv`.

### FAQ

**How accurate is it?** On a 70-contact benchmark (43 executives whose moves were announced in 2025–2026 press releases, 28 who stayed) confirmed moves were right 9 times out of 10 and about half of the real moves were confirmed; most of the rest came back as `possible-move` (one source) or `not-found`, never as a wrong "confirmed". See `docs/DECISIONS.md` §3.

**Why is a move only "possible"?** One public source named the new company (usually the profile headline) and neither an announcement nor the company's own site confirmed it yet. It is delivered for you to verify and is not charged as a job change.

**Does it read LinkedIn?** No page on linkedin.com is ever fetched. It reads what search engines show publicly about profiles (title and snippet), announcements and company websites.

**Will a promotion inside the same company count as a move?** No — `changed` stays false, `currentTitle` carries the new title.

**Why is the e-mail "mx-only"?** Outbound SMTP (port 25) is blocked on the platform, so addresses are verified down to the mail server; the pattern is the domain's own where it is visible on its site, otherwise the most common one.

**Common names?** Add `title` and/or `linkedinUrl` to the contact. A lone profile that neither mentions the last-known company nor matches the title nor agrees with an announcement is not trusted.

# Actor input Schema

## `contacts` (type: `array`):

One object per contact: `name` and `company` (the last-known employer) are required; `title`, `email` and `linkedinUrl` help disambiguate common names. Plain lines like `Jane Doe, Acme, VP Sales` work too.

## `onlyChanged` (type: `boolean`):

When on, contacts still at their company (and contacts not found) are checked but not delivered or charged — the dataset holds only confirmed moves.

## `daysBack` (type: `integer`):

Press announcements dated earlier than this are ignored as evidence (the person may have moved again since). Profile headlines are always current.

## `confirmOnTeamPage` (type: `boolean`):

Reads the candidate company's home / team / leadership pages. A person listed there is authoritative evidence of the move (and often gives the exact title).

## `findEmails` (type: `boolean`):

For contacts who moved: resolves the new company's domain and builds the most likely address (first.last@, first@, flast@ …), using the domain's own pattern when it is visible on the site.

## `verifyEmails` (type: `boolean`):

Checks the domain's mail server (MX) and, where outbound port 25 is available, the mailbox itself (SMTP). Otherwise addresses are verified down to the mail server only (`mx-only`).

## `watchId` (type: `string`):

Give the same id on every scheduled run of the same list and each move is reported — and charged — only once: contacts already reported as moved are skipped, unchanged contacts are re-checked. Leave empty for a one-off run.

## `maxContacts` (type: `integer`):

Cap for this run (the first N contacts of the list).

## `maxSearchesPerContact` (type: `integer`):

Search queries spent per contact: the profile search, then a press / company search when the profile points somewhere new or was not found. 3 is enough for almost everyone; lower it to save cost on very large lists.

## `searchEngine` (type: `string`):

`auto` tries Yahoo and Bing (no per-query fee) before the Google SERP proxy (paid per page). Pick one engine only to force it.

## `language` (type: `string`):

Two-letter language for the search results (`en`, `de`, `fr` …).

## `countryCode` (type: `string`):

Two-letter country for the search results (`us`, `gb`, `de` …).

## `maxConcurrency` (type: `integer`):

Contacts checked in parallel.

## `proxyConfiguration` (type: `object`):

Searches and website reads go through the run's own connection first and fall back to this proxy when a site challenges them. Residential is the default.

## Actor input object example

```json
{
  "contacts": [
    {
      "name": "Laura Zwahlen",
      "company": "NextRoll",
      "title": "Chief Revenue Officer"
    },
    {
      "name": "Arnab Bose",
      "company": "Okta",
      "title": "Chief Product Officer"
    },
    {
      "name": "Rachel Pyles",
      "company": "Ansys",
      "title": "Chief Financial Officer"
    },
    {
      "name": "Amy Hood",
      "company": "Microsoft",
      "title": "Chief Financial Officer"
    },
    {
      "name": "Aaron Levie",
      "company": "Box",
      "title": "CEO"
    }
  ],
  "onlyChanged": false,
  "daysBack": 365,
  "confirmOnTeamPage": true,
  "findEmails": true,
  "verifyEmails": true,
  "maxContacts": 5000,
  "maxSearchesPerContact": 3,
  "searchEngine": "auto",
  "language": "en",
  "countryCode": "us",
  "maxConcurrency": 6,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `contacts` (type: `string`):

One row per contact: previous company, current company, current title, changed, status, confidence, evidence URL, dates.

## `moves` (type: `string`):

The outreach view: who moved where, new domain, new e-mail with its verification, why now and whom to sell to.

## `csv` (type: `string`):

Spreadsheet-ready copy of the rows (first 5,000).

## `summary` (type: `string`):

Contacts checked, moves confirmed and charged, possible moves, unchanged, not found, e-mails found, search-engine stats and the watch state.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "contacts": [
        {
            "name": "Laura Zwahlen",
            "company": "NextRoll",
            "title": "Chief Revenue Officer"
        },
        {
            "name": "Arnab Bose",
            "company": "Okta",
            "title": "Chief Product Officer"
        },
        {
            "name": "Rachel Pyles",
            "company": "Ansys",
            "title": "Chief Financial Officer"
        },
        {
            "name": "Amy Hood",
            "company": "Microsoft",
            "title": "Chief Financial Officer"
        },
        {
            "name": "Aaron Levie",
            "company": "Box",
            "title": "CEO"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("inovaflow/job-change-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "contacts": [
        {
            "name": "Laura Zwahlen",
            "company": "NextRoll",
            "title": "Chief Revenue Officer",
        },
        {
            "name": "Arnab Bose",
            "company": "Okta",
            "title": "Chief Product Officer",
        },
        {
            "name": "Rachel Pyles",
            "company": "Ansys",
            "title": "Chief Financial Officer",
        },
        {
            "name": "Amy Hood",
            "company": "Microsoft",
            "title": "Chief Financial Officer",
        },
        {
            "name": "Aaron Levie",
            "company": "Box",
            "title": "CEO",
        },
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("inovaflow/job-change-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "contacts": [
    {
      "name": "Laura Zwahlen",
      "company": "NextRoll",
      "title": "Chief Revenue Officer"
    },
    {
      "name": "Arnab Bose",
      "company": "Okta",
      "title": "Chief Product Officer"
    },
    {
      "name": "Rachel Pyles",
      "company": "Ansys",
      "title": "Chief Financial Officer"
    },
    {
      "name": "Amy Hood",
      "company": "Microsoft",
      "title": "Chief Financial Officer"
    },
    {
      "name": "Aaron Levie",
      "company": "Box",
      "title": "CEO"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call inovaflow/job-change-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,inovaflow/job-change-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/D7QgjFh4WKHAFeVcd/builds/zs5Bk0qaUTOWrmqb1/openapi.json
