URL: /capabilities/convert-visitors/inbound-routing · Id: `page:/capabilities/convert-visitors/inbound-routing`

# Inbound routing

> A new signup or form fill in, who they are, where they work, whether they fit, and the next step out, in under 10 seconds.

A new signup or form fill in, who they are, where they work, whether they fit, and the next step out, in under 10 seconds.

```text title="Run it · Ask"
Re-route every inbound from the last week with my new fit rule, spend up to $1, and show me the plan first.
```

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

const result = await $.try.Lead.route({ goal: 'Re-route every inbound from the last week with my new fit rule, spend up to $1, and show me the plan first.', budget: { amount: 1, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 0.4¢ an inbound",
  "sample": {
    "lead": "Dana W., Director of Operations",
    "company": "Harbor Point Residential",
    "units": "340",
    "fit": "fit",
    "route": "booking"
  }
}
```

- **Kind:** Workflow
- **Price:** 0.4¢ per inbound
- **Your time:** 11 minutes
- **Approval:** Runs on its own
- **Deepens:** [`workflow:answer-inbound`](https://startupbuilder.dev/workflows/processes/inbound-sales/answer-inbound)

## What you get
- **1 inbound** · A new signup or form fill in, who they are, where they work, whether they fit, and the next step out, in under 10 seconds. · [`noun:lead`](https://startupbuilder.dev/business/nouns/lead)
- `person` · Name, title and seniority, from the work email.
- `organization` · The company behind the domain, with headcount and what your fit rule reads.
- `fit` · `fit`, `maybe` or `not`, by the Policy, with the field that decided it.
- `route` · The next step: `booking` (a call), `trial`, `nurture` or `person` for a question only a person can answer.

**How long:** Under 10 seconds.

## Trigger
It runs on every new lead.

```ts
on.Lead.created((lead, $) => $.Lead.route({ lead: lead.$id }))
```

## 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. Read the person behind the email | [`Contact.enrich`](/code/data/people/enrich) | Name, title, seniority and employer | About 0.2¢ | Runs on its own |
| 2. Find the company behind the domain | [`Organization.resolve`](/code/data/organizations/resolve) | The company’s hostname; a free-mail address skips this step | Free | Runs on its own |
| 3. Read the company’s profile | [`Organization.enrich`](/code/data/organizations/enrich) | Headcount, location and industry | About 0.2¢ | Runs on its own |
| 4. Score fit by the Policy | [`Lead.qualify`](/code/apps/crm/qualify) | `fit`, `maybe` or `not`, with the deciding field | Free | Runs on its own |
| 5. Route by the rule | `Lead.assign` | The lead assigned, with the booking or trial link attached | Free | Runs on its own |

Calls: `Contact.enrich`, then `Organization.resolve`, then `Organization.enrich`, then `Lead.qualify`, then `Lead.assign`.

About 0.4¢ an inbound, each time it runs, capped at 5¢.

## Example: give it a goal
> Re-route every inbound from the last week with my new fit rule, spend up to $1, and show me the plan first.

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

## When something fails

| When | Then |
| --- | --- |
| The lead used a free-mail address | The company step is skipped and the lead routes on its form answers alone. |
| No source knows the person | The row reads `unknown` for the person and still routes by the company; the miss is free. |
| You route the same lead twice | It returns the first route: it is idempotent per lead. Re-routing after a rule change is a new call. |

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-lead-route-1-<step>-<item>`.

## Change it
| Ask | Compiles to |
| --- | --- |
| Send every lead over 100 units to a call, not a trial. | [`Lead.route`](/capabilities/convert-visitors/inbound-routing) |
| Answer each one within five minutes too. | [`Lead.respond`](/capabilities/convert-visitors/answer-every-inbound) |
| Re-route last week’s leads with the new rule. | [`Lead.route`](/capabilities/convert-visitors/inbound-routing) |

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

export default workflow({
  name: 'lead-route',
  verb: 'Lead.route',
  input: { lead: 'string' },
  trigger: { event: 'Lead.created' },
  maxCost: 0.05,
  execute: async (ctx, { lead }) => {
    const l = await ctx.step('lead', () => $.Collection.query({ from: 'lead', id: lead }))
    const who = await ctx.step('person', () => $.Contact.enrich({ email: l.email, fields: ['title', 'organization', 'seniority'], maxCost: 0.01 }))
    const org = isFreeMail(l.email) ? undefined : await ctx.step('company', () => $.Organization.resolve({ email: l.email }))
    const profile = org?.match ? await ctx.step('profile', () => $.Organization.enrich({ hostname: org.match.hostname, fields: ['profile'], maxCost: 0.01 })) : undefined
    const q = await ctx.step('qualify', () => $.Lead.qualify({ lead, from: { who, profile } }))          // the fit Policy
    return ctx.step('assign', () => $.Lead.assign({ lead, route: q.fit === 'fit' ? 'booking' : q.fit === 'maybe' ? 'trial' : 'nurture' }))
  },
})
```

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

## Works with
**Plays that order it:** [Agent-to-agent selling](/plays/agent-to-agent), [Events and webinars](/plays/events-and-webinars), [Family opt-in](/plays/family-opt-in), [Instant response](/plays/instant-response), [lead nurture](/plays/lead-nurture), [Outbound calling](/plays/outbound-calling), [Product-led growth](/plays/product-led), [Waitlist interviews](/plays/signup-interviews).

It serves [Convert visitors](/capabilities/convert-visitors). It deepens [`workflow:answer-inbound`](https://startupbuilder.dev/workflows/processes/inbound-sales/answer-inbound).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Lead.route` |
| Guide approval | `limits` |
| Goal | Re-route every inbound from the last week with my new fit rule, spend up to $1, 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 | `on.Lead.created((lead, $) => $.Lead.route({ lead: lead.$id }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-lead-route-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read the person behind the email | None | None. |
| 2. Find the company behind the domain | None | None. |
| 3. Read the company’s profile | None | None. |
| 4. Score fit by the Policy | None | None. |
| 5. Route by the rule | None | None. |

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

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Read the person behind the email | `code` | Code | Code checks each result against its declared type | `measured` | `$0.lead.email` |
| 2. Find the company behind the domain | `code` | Code | Code checks each result against its declared type | `measured` | `$0.lead.email` |
| 3. Read the company’s profile | `code` | Code | Code checks each result against its declared type | `measured` | `$2.hostname` |
| 4. Score fit by the Policy | `code` | Code | Code reads the result back from where it landed | `measured` | `$1, $3, $0.rule` |
| 5. Route by the rule | `code` | Code | Code reads the result back from where it landed | `measured` | `$4.fit, $0.rule` |

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:contact`](https://startupbuilder.dev/business/nouns/contact), [`noun:organization`](https://startupbuilder.dev/business/nouns/organization), [`noun:lead`](https://startupbuilder.dev/business/nouns/lead) |
| Events | `Contact.enriched`, `Lead.qualified`, `Lead.assigned` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `lead` | `Lead` | Required | The new lead, from a form, a signup, a reply or a lead ad. |
| `rule` | `Policy` | `your Lead fit Policy` | The declared fit rule and routes, such as “over 200 units books a call; under that starts a trial”. |
| `maxCost` | `number` | `0.05` | 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 `Lead.routed`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).