URL: /capabilities/keep-and-grow/champion-moved · Id: `page:/capabilities/keep-and-grow/champion-moved`

# Champion moved

> Each month, your champions checked for a new job, and each who moved to a company that fits added to your accounts with why now.

Each month, your champions checked for a new job, and each who moved to a company that fits added to your accounts with why now.

```text title="Run it · Ask"
Find where my champions moved this quarter, spend up to $2, and show me the plan first.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Contact.follow({ goal: 'Find where my champions moved this quarter, spend up to $2, and show me the plan first.', budget: { amount: 2, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 42¢ per 200 champions checked",
  "sample": {
    "person": "Maya C.",
    "from": "Cedar Hollow Rentals",
    "to": "Lakeshore Living",
    "title": "COO",
    "fit": "strong"
  }
}
```

- **Kind:** Workflow
- **Price:** 42¢ per 200 checked
- **Your time:** 8 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:retention`](https://startupbuilder.dev/workflows/processes/retention)

## What you get
- **200 champions checked** · Each month, your champions checked for a new job, and each who moved to a company that fits added to your accounts with why now. · [`noun:contact`](https://startupbuilder.dev/business/nouns/contact)
- `checked` · Champions read this month.
- `moved[]` · Each person who changed jobs: from, to, the new title and the new company’s fit.
- `added[]` · New companies added to your accounts, with why now: “your champion joined as COO”.

**How long:** About 3 minutes.

## Trigger
It runs monthly, and on each list refresh row marked `moved`.

```ts
every.month(($) => $.Contact.follow({ contacts: 'champions' }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. List your champions | `Collection.query` | Each champion, with their last known employer | Free | Runs on its own |
| 2. Read each champion’s profile | [`Contact.enrich`](/code/data/people/enrich) | Current title and employer | About 40¢ | Runs on its own |
| 3. Find who moved | `Collection.query` | Each person whose employer changed, by the declared rule | Free | Runs on its own |
| 4. Ask each new employer the fit question | [`Organization.find`](/code/data/organizations/find) | `fit` with the quote code finds on the page | About 1.2¢ | Runs on its own |
| 5. Read each new employer’s profile | [`Organization.enrich`](/code/data/organizations/enrich) | Headcount and location | About 1.2¢ | Runs on its own |
| 6. Add each fit to your accounts | `Audience.update` | The company on the account Audience, with why now, which a Play can act on | Free | Runs on its own |

Calls: `Collection.query`, then `Contact.enrich`, then `Collection.query`, then `Organization.find`, then `Organization.enrich`, then `Audience.update`.

About 42¢ per 200 champions checked, each time it runs, capped at $1.00.

## Example: give it a goal
> Find where my champions moved this quarter, spend up to $2, and show me the plan first.

The plan it shows runs 6 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| A profile cannot be read | The champion keeps their last state and is read first next month. |
| The new employer does not fit | The move is recorded on the contact and nothing is added. |
| You call it twice in one month | The month’s reading returns: it is idempotent per contact and month. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-contact-follow-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Write each one a note to say congratulations and ask for an intro. | [`Prospect.reach`](/capabilities/reach-out/personal-first-touch) |
| Follow every user, not only champions. | [`Contact.follow`](/capabilities/keep-and-grow/champion-moved) |
| Check every two weeks instead of monthly. | [`Contact.follow`](/capabilities/keep-and-grow/champion-moved) |

```ts
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'contact-follow',
  verb: 'Contact.follow',
  input: { contacts: "'champions' | string[]", question: 'string?' },
  trigger: { schedule: 'monthly' },
  maxCost: 1,
  execute: async (ctx, { contacts = 'champions', question }) => {
    const list = await ctx.step('list', () => $.Collection.query({ from: 'contact', where: contacts === 'champions' ? "role = 'champion'" : `$id in ${contacts}` }))
    const now = await ctx.step('profiles', () => $.Contact.enrich({ contacts: list.map((c) => c.$id), fields: ['title', 'organization'], maxCost: 0.45 }))
    const moved = now.filter((p) => p.organization.hostname !== p.was.organization.hostname)      // the declared rule
    const fit = await ctx.step('fit', () => $.Organization.find({ hostnames: moved.map((m) => m.organization.hostname), question, maxCost: 0.05 }))
    return ctx.step('add', () => $.Audience.update({ id: 'audiences/audience_accounts', add: fit.organizations.filter((o) => o.fit === 'strong'), whyNow: 'champion joined' }))
  },
})
```

Extend it with `extends: 'Contact.follow'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Champion follow](/plays/champion-follow), [Signal expansion](/plays/signal-expansion).

It serves [Keep and grow customers](/capabilities/keep-and-grow), and runs in [Keep and grow](/strategy/keep-and-grow). It deepens [`process:retention`](https://startupbuilder.dev/workflows/processes/retention).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Contact.follow` |
| Guide approval | `limits` |
| Goal | Find where my champions moved this quarter, spend up to $2, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `every.month(($) => $.Contact.follow({ contacts: 'champions' }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-contact-follow-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. List your champions | None | None. |
| 2. Read each champion’s profile | None | None. |
| 3. Find who moved | None | None. |
| 4. Ask each new employer the fit question | None | None. |
| 5. Read each new employer’s profile | None | None. |
| 6. Add each fit to your accounts | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. List your champions | `code` | Code | Code checks each result against its declared type | `measured` | `$0.contacts` |
| 2. Read each champion’s profile | `code` | Code | Code checks each result against its declared type | `measured` | `$1.contacts` |
| 3. Find who moved | `code` | Code | Code checks each result against its declared type | `measured` | `$1, $2` |
| 4. Ask each new employer the fit question | `code` | Code | Code finds the quoted evidence on the page | `measured` | `$3.moved, $0.question` |
| 5. Read each new employer’s profile | `code` | Code | Code checks each result against its declared type | `measured` | `$3.moved` |
| 6. Add each fit to your accounts | `code` | Code | Code reads the result back from where it landed | `measured` | `$4.strong` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [CRM](/code/apps/crm) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:contact`](https://startupbuilder.dev/business/nouns/contact), [`noun:organization`](https://startupbuilder.dev/business/nouns/organization), [`noun:audience`](https://startupbuilder.dev/business/nouns/audience) |
| Events | `Collection.queried`, `Contact.enriched`, `Audience.updated` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `contacts` | `'champions' \| Contact[]` | `'champions'` | The people to follow: every contact marked champion, or a list. |
| `question` | `string` | `the question saved with your ICP` | Asked of each new employer’s site; a fit needs the quote. |
| `every` | `string` | `'30d'` | How often they are read. |
| `maxCost` | `number` | `1.00` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Contact.followed`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).