> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fuseai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate job change outreach with signal agents

> Catch buyers in their first weeks in a new role: a Fuse signal agent finds them with a work email, and a dynamic campaign contacts each one automatically.

People in their first months in a new role are often reviewing tools and vendors, which makes a job change one of the strongest buying signals. This workflow sets up a [signal agent](/signals/agents) that finds people starting a role that matches your buyer, adds their work email, and delivers them into a list. A dynamic campaign watches that list and enrolls each new person automatically, so outreach keeps running without another API call.

```mermaid theme={null}
%%{init: {"flowchart": {"rankSpacing": 20, "nodeSpacing": 20}}}%%
flowchart TD
  A[New job agent] -->|listId| B[Results list]
  B -->|isDynamic| C[AI campaign]
  C --> D[Emails sent]
```

## Before you start

| You need | Why |
| - | - |
| An API key with the `agents`, `lists` and `campaigns` [scopes](/concepts/scopes) | The agent, its results list and the campaign each sit on a different surface. |
| A connected mailbox | The campaign cannot be approved without one. See [Connect a mailbox](/guides/connect-a-mailbox). |
| Credits | The agent charges 100 credits per person it delivers, and `enrichEmail` adds 20 to 50 for each work email it finds. Starting it is free, and `maxCreditSpend` caps a single run. |

## Step by step

<Steps>
  <Step title="Find the exact values the agent matches on">
    Agent filters match exact strings. A value outside the vocabulary is still accepted with `201`, and the agent then quietly finds nobody, so resolve every value first:

    ```bash theme={null}
    curl -G https://api.tryfuse.ai/api/v1/business/agents/autocomplete \
      -u "$FUSE_API_KEY:" \
      --data-urlencode "field=industry" \
      --data-urlencode "q=financial services"
    ```

    ```json theme={null}
    { "field": "industry", "suggestions": ["Financial Services", "Capital Markets"] }
    ```

    Use `field=title` the same way for job titles. Three rules for this agent:

    * `industries` takes exactly one value. To cover several industries, create one agent per industry.
    * `leadCompanyHeadcount` takes exact bands: `1-10`, `11-50`, `51-200`, `201-500`, `501-1,000`, `1,001-5,000`, `5,001-10,000`, `10,001+`.
    * `leadCompanyCountries` takes full English country names such as `"United Kingdom"`. No endpoint serves this list, so spell them out.

    More detail is in [Finding accepted values](/concepts/field-values).
  </Step>

  <Step title="Save the agent as a draft">
    `POST /business/agents/person-starting-new-job` with `start` left out saves a draft. Nothing runs and nothing is billed yet.

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/agents/person-starting-new-job \
      -u "$FUSE_API_KEY:" \
      -H "Content-Type: application/json" \
      -d '{
        "listName": "New finance leaders - UK fintech",
        "industries": ["Financial Services"],
        "authorTitle": ["Chief Financial Officer", "Vice President of Finance"],
        "leadCompanyHeadcount": ["51-200", "201-500"],
        "leadCompanyCountries": ["United Kingdom"],
        "enrichEmail": true,
        "maxCreditSpend": 5000,
        "expirationDate": null
      }'
    ```

    ```json theme={null}
    {
      "agent": {
        "id": "6a8dce4e65fd738c00d8927b",
        "type": "person-starting-new-job",
        "name": "New finance leaders - UK fintech",
        "status": "initializing",
        "started": false
      }
    }
    ```

    | Field | What it does |
    | - | - |
    | `listName` | Names the agent and the results list it creates. Must not match any existing list or agent. |
    | `authorTitle` | The titles people are starting in. Several titles are OR-joined. |
    | `enrichEmail` | Adds a work email to every person delivered, so the campaign can reach them. |
    | `maxCreditSpend` | The most a single run may spend, from 1,000 to 200,000. |
    | `expirationDate` | `null` runs the agent until you pause it. Leave it out and the agent stops after a month. |
  </Step>

  <Step title="Review the draft, then start it">
    `GET /business/agents/{agentId}` returns the stored `config`, exactly as you would send it to create the agent again. Fix anything with `PATCH /business/agents/{agentId}`, which merges the fields you send. Then activate it:

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/agents/6a8dce4e65fd738c00d8927b/activate \
      -u "$FUSE_API_KEY:"
    ```

    Activation answers `202`, creates the results list and starts the first run. It is free: the agent charges 100 credits for each person it delivers, plus the email price when `enrichEmail` finds one. The agent moves through `processing` to `active`.
  </Step>

  <Step title="Find the agent's results list">
    The results list id is on the agent's row in `GET /business/agents`. `GET /business/agents/{agentId}` returns the config but not the list.

    ```bash theme={null}
    curl "https://api.tryfuse.ai/api/v1/business/agents?limit=25" -u "$FUSE_API_KEY:"
    ```

    ```json theme={null}
    {
      "agents": [
        {
          "id": "6a8dce4e65fd738c00d8927b",
          "type": "person-starting-new-job",
          "name": "New finance leaders - UK fintech",
          "status": "active",
          "entityType": "person",
          "listId": "6a8dcf1fc5017e82fb6091aa",
          "contactsFound": 50
        }
      ],
      "pagination": { "pageNum": 1, "totalPages": 1, "totalRecords": 1 }
    }
    ```
  </Step>

  <Step title="Wait for the first results">
    Poll `GET /business/agents` every few minutes until `contactsFound` is above zero, then check `GET /business/lists/{listId}` for `metrics.totalWithProfessionalEmail`. The campaign needs at least one reachable contact on the list before it can be created.
  </Step>

  <Step title="Point a dynamic campaign at the results list">
    Create the campaign on the agent's list with `isDynamic: true`:

    ```bash theme={null}
    curl -X POST https://api.tryfuse.ai/api/v1/business/campaigns \
      -u "$FUSE_API_KEY:" \
      -H "Content-Type: application/json" \
      -d '{
        "campaignName": "New finance leaders - welcome",
        "campaignType": "ai-generated",
        "listId": "6a8dcf1fc5017e82fb6091aa",
        "isDynamic": true,
        "channels": ["email", "email"],
        "userPrompt": "Congratulate them on the new role. Offer a short call on how new finance leaders speed up month-end close in their first 90 days. Warm, brief, no hard sell.",
        "ctaLink": "https://example.com/demo",
        "excludeContacted": "last_6_months"
      }'
    ```

    Then wait for initialization, preview the steps and approve, exactly as in steps 7 to 9 of [Build an AI outbound campaign from a prompt](/workflows/ai-campaign-from-a-prompt).
  </Step>

  <Step title="Let it run">
    From here, the agent delivers new starters into its list on its own schedule, and the campaign checks the list about every 30 minutes. Each check enrolls up to 200 contacts that are not in the campaign yet, and the campaign's exclusions still apply. A check enrolls nobody while you have no usable sending account, or while one of the campaign's exclude lists cannot be read; the next check tries again.

    Watch it with `GET /business/agents` for `contactsFound`, `GET /business/campaigns/{campaignId}/stats` for sends and replies, and `GET /business/analytics/agents` for delivery over time.
  </Step>
</Steps>

## Keep it running

* **If the agent runs out of credits, it pauses itself.** Top up, then choose **Activate** on it in the app. Over the API, pausing is one-way: `POST /business/agents/{agentId}/duplicate`, activate the copy, and add its results list to the campaign with `PATCH /business/campaigns/{campaignId}` and `{ "listIds": ["<old list>", "<new list>"] }`. The next dynamic check enrolls the new list's contacts.
* **Expiry stops it too.** An agent past its `expirationDate` reads `expired`. Set `expirationDate: null` for an agent you want running indefinitely.
* **Cap the total.** `maxContacts` stops the agent after that many results in all, and `notificationCount` caps how many results a single run delivers.

## Use other signals

The same pattern works with any agent that delivers people. Each of these accepts `enrichEmail`:

| Agent | Finds |
| - | - |
| `job-changes-watcher` | People on a list you already have, such as your customers' champions, when they move to a new company |
| `linkedin-posts` | People posting about keywords that signal your problem |
| `linkedin-post` | People who liked or commented on specific LinkedIn posts |
| `person-discovery-via-filters` | New people matching a filter set as they appear |

Agents that deliver companies, such as `funding-announcements` or `job-postings`, need one more step: take the company domains and search for the people who work there. [Funding round outreach](/workflows/funding-round-outreach) walks through it. Every kind is listed in [Create an agent](/guides/create-an-agent).

## What it costs

| Part | Credits |
| - | - |
| The agent | 100 per person delivered. Starting it is free. The app also shows the agent's cost model before you activate it. |
| `enrichEmail` | 20 to 50 for each work email found, nothing when none is found. |
| `maxCreditSpend` | The ceiling for any single run. |
| Reading agents, lists and campaign stats | Free |

See [Credits & billing](/concepts/credits) and [Agents](/signals/agents#what-it-costs).

## Troubleshooting

| Symptom | Cause | Fix |
| - | - | - |
| The agent is `active` but `contactsFound` stays at 0 | A config value is outside the vocabulary. The agent was created, but matches nobody. | Check every value against `GET /business/agents/autocomplete`, then duplicate the agent, correct the copy with `PATCH` and activate it. |
| `409 LIST_NAME_TAKEN` | `listName` is already used by a list or another agent. | Choose a unique name. |
| The agent turns `paused` | The balance could not cover a delivery, or the agent reached `maxContacts`. | Top up, then choose **Activate** on it in the app, or duplicate it and activate the copy. |
| `500 UNEXPECTED_ERROR` on activate | A config problem that activation does not classify. | Create the agent with `start: true`, which reports the specific error. |
| `409 AGENT_NOT_A_DRAFT` | The agent already started, or is paused. | Duplicate it and activate the copy. |
| `422 LIST_INCOMPATIBLE_WITH_CHANNELS` creating the campaign | The results list is empty, or nobody on it has an email yet. | Wait for the first delivery and keep `enrichEmail` on. |
| `403 AGENT_LIMIT_REACHED` | Free-plan workspaces hold at most 10 agents, drafts included. | Delete an agent or upgrade. |

## Run the whole workflow

<Accordion title="Python script: job change agent to dynamic campaign">
  ```python theme={null}
  """Find new job starters with a signal agent and contact each one through a dynamic campaign."""
  import json
  import os
  import time

  import requests

  BASE = "https://api.tryfuse.ai/api/v1/business"
  AUTH = (os.environ["FUSE_API_KEY"], "")


  class FuseError(Exception):
      def __init__(self, status, code, message):
          super().__init__(f"{status} {code}: {message}")
          self.code = code


  def call(method, path, **kwargs):
      """Send one request, wait out 429s, and raise FuseError on any other failure."""
      while True:
          response = requests.request(method, BASE + path, auth=AUTH, timeout=60, **kwargs)
          if response.status_code == 429:
              time.sleep(int(response.headers.get("Retry-After", 5)))
              continue
          if response.status_code >= 400:
              try:
                  error = response.json().get("error", {})
              except ValueError:
                  error = {}
              raise FuseError(response.status_code, error.get("code"), error.get("message"))
          return response.json() if response.content else None


  def wait_until(read, done, delay=10, max_delay=60):
      """Poll read() until done(result) is true, waiting before each poll and backing off."""
      while True:
          time.sleep(delay)
          result = read()
          if done(result):
              return result
          delay = min(delay * 1.5, max_delay)


  def find_agent(agent_id):
      """GET /agents is the only read that returns an agent's results listId."""
      page = 1
      while True:
          body = call("GET", "/agents", params={"limit": 100, "pageNum": page})
          for agent in body["agents"]:
              if agent["id"] == agent_id:
                  return agent
          if page >= body["pagination"]["totalPages"]:
              raise SystemExit(f"Agent {agent_id} not found")
          page += 1


  # 1. Resolve the exact values the agent matches on
  industry = call("GET", "/agents/autocomplete",
                  params={"field": "industry", "q": "financial services"})["suggestions"][0]
  titles = call("GET", "/agents/autocomplete",
                params={"field": "title", "q": "chief financial officer"})["suggestions"][:3]
  print("Industry:", industry, "| Titles:", titles)

  # 2. Save the agent as a draft (nothing is billed yet)
  agent_id = call("POST", "/agents/person-starting-new-job", json={
      "listName": "New finance leaders - UK fintech",
      "industries": [industry],
      "authorTitle": titles,
      "leadCompanyHeadcount": ["51-200", "201-500"],
      "leadCompanyCountries": ["United Kingdom"],
      "enrichEmail": True,
      "maxCreditSpend": 5000,
      "expirationDate": None,
  })["agent"]["id"]

  # 3. Review the stored config, then start the agent
  print(json.dumps(call("GET", f"/agents/{agent_id}")["agent"]["config"], indent=2))
  if input("Start the agent? It bills 100 credits per person it delivers. [y/N] ").strip().lower() != "y":
      raise SystemExit("The draft is saved and nothing was billed.")
  call("POST", f"/agents/{agent_id}/activate")

  # 4-5. Wait for the first people to land in the agent's results list
  agent = wait_until(
      lambda: find_agent(agent_id),
      lambda current: current["contactsFound"] > 0 or current["status"] in ("failed", "paused", "expired"),
      delay=300,
      max_delay=1800,
  )
  if not agent["contactsFound"]:
      raise SystemExit(f"The agent stopped with status {agent['status']} before delivering anyone.")
  list_id = agent["listId"]
  print(f"{agent['contactsFound']} new starters delivered into list {list_id}")

  # 6. Point a dynamic AI campaign at the results list
  campaign_body = {
      "campaignName": "New finance leaders - welcome",
      "campaignType": "ai-generated",
      "listId": list_id,
      "isDynamic": True,
      "channels": ["email", "email"],
      "ctaLink": "https://example.com/demo",
      "userPrompt": "Congratulate them on the new role. Offer a short call on how new finance leaders "
                    "speed up month-end close in their first 90 days. Warm, brief, no hard sell.",
      "excludeContacted": "last_6_months",
  }
  for attempt in range(5):
      try:
          campaign_id = call("POST", "/campaigns", json=campaign_body)["campaignId"]
          break
      except FuseError as error:
          if error.code != "LIST_INCOMPATIBLE_WITH_CHANNELS" or attempt == 4:
              raise
          time.sleep(90)

  campaign = wait_until(
      lambda: call("GET", f"/campaigns/{campaign_id}")["campaign"],
      lambda current: current["initializationStatus"] in ("completed", "failed"),
  )
  if campaign["initializationStatus"] == "failed":
      raise SystemExit(campaign["initializationError"])

  first_step = call("GET", f"/campaigns/{campaign_id}/steps")["steps"][0]
  print(call("POST", f"/campaigns/{campaign_id}/steps/{first_step['id']}/generate")["preview"]["body"])

  # 7. Approving sends real email, so ask first
  if input("Approve the campaign? [y/N] ").strip().lower() == "y":
      call("POST", f"/campaigns/{campaign_id}/approve")
      print("Live. New starters will be enrolled automatically about every 30 minutes.")
  ```
</Accordion>

## Do it from Claude

With the [Fuse MCP server](/mcp/connect) connected:

> Create a signal agent that finds people starting a new job as CFO or VP of Finance at UK financial services companies with 51 to 500 employees, with work emails, capped at 5,000 credits per run. Once it has delivered its first people, draft an AI campaign on its results list that enrolls new people automatically and congratulates them on the new role. Don't launch the campaign.

The assistant sets up the signal agent, then drafts the campaign on its results list. Creating a campaign never launches it, so it stays a draft until you ask.

## Related

<CardGroup cols={2}>
  <Card title="Create an agent" icon="robot" href="/guides/create-an-agent">
    Every agent kind, its config fields and its spend controls.
  </Card>

  <Card title="Signals: agents" icon="tower-broadcast" href="/signals/agents">
    What each signal finds and how agents are billed.
  </Card>

  <Card title="Campaigns" icon="paper-plane" href="/guides/campaigns">
    Dynamic enrollment, exclusions and approval.
  </Card>

  <Card title="Website visitor outreach" icon="globe" href="/workflows/website-visitor-outreach">
    Another always-on pipeline, driven by who visits your site.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.