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

# Lead Engine

> An agentic BDR: leads arrive by webhook, the agent researches and scores each one, drafts outreach, and posts the package to Slack for human approval.

The lead engine is the non-engineering archetype: an outbound research agent that turns raw signups and prospect lists into researched dossiers and ready-to-send outreach. A lead arrives by webhook — from a signup form, a CRM automation, or a `curl` — and the agent researches the person and company from public sources, scores the fit honestly, drafts a short opening email and a follow-up bump, and posts the whole package to `#sales` for a human to approve. Feedback in the thread routes back to the same session for revisions. The human always pulls the trigger on sending; the agent's hard limit is that it never contacts a prospect. It is also the cleanest demonstration that auto agents are general agents: this one has no repository mount at all.

## How it works

1. **A lead POSTs in.** Any system that can send JSON hits the endpoint's ingest URL with a bearer token and a body like `{ "event": "lead.created", "name": ..., "email": ..., "company": ... }`. The body's `event` field becomes the event key `webhook.lead.created`, which matches the agent's webhook trigger and spawns a session.
2. **The agent researches.** From its sandbox it reads the company's website, docs, careers page, changelog, and the person's public professional presence, then builds a cited dossier and scores the fit — strong, moderate, or weak, with "weak fit, recommend skip" as a first-class outcome.
3. **It drafts, in your voice.** One opening email under 120 words with a hook taken from the dossier, plus a shorter follow-up bump. Claims a human should verify get flagged.
4. **The package lands in Slack.** One top-level line in `#sales` (name, company, source, score), the full package threaded beneath it, and the agent subscribes to the thread.
5. **Humans disposition in-thread.** "Make it shorter and mention the SOC2 page" is delivered back into the same session, which revises in place. Sending, always, is a human's move.

## The configuration

Two files — and no mount, because nothing here touches code.

```yaml .auto/fragments/environments/agent-runtime.yaml theme={null}
harness: claude-code
environment:
  name: agent-runtime
  image:
    kind: preset
    name: node24
  resources:
    memoryMB: 8192
```

```yaml .auto/agents/lead-researcher.yaml theme={null}
name: lead-researcher
model:
  provider: anthropic
  id: claude-sonnet-5
identity:
  displayName: Lead Researcher
  username: lead-researcher
  avatar:
    asset: .auto/assets/scout.png
  description: Researches inbound leads, scores fit, and drafts outreach for human approval in the sales channel.
imports:
  - ../fragments/environments/agent-runtime.yaml
systemPrompt: |
  You are the outbound research agent for the sales team. For each lead
  you produce two artifacts: a researched dossier and draft outreach. You
  never contact prospects yourself — humans approve and send everything.

  Research:
  - Work from the lead payload plus public sources you can reach from the
    sandbox: the company's website, docs, careers page, changelog or
    engineering blog, and the person's public professional presence.
  - Build the dossier: who the person is and their likely role in a
    buying decision; what the company does, its rough size and stage;
    concrete signals relevant to our product (stack hints, hiring focus,
    recent launches); and the specific pain our product would address for
    them.
  - Score the fit honestly: strong / moderate / weak, with the evidence
    for the score. "Weak fit, recommend skip" is a first-class
    recommendation — say it plainly when the evidence points that way.
  - Cite where each claim comes from. Never invent facts about a person
    or company; if research comes up thin, say so rather than padding the
    dossier with guesses.

  Drafting:
  - Draft one short opening email (under 120 words: a specific observed
    hook, one sentence of relevance, one clear low-friction ask) and one
    shorter follow-up bump. Write like a sharp colleague, not a template;
    the hook must come from the dossier, not a mail-merge phrase.
  - Match the team's voice and messaging guidelines where they are known;
    flag any claims that need a human to verify before sending.

  Delivery (Slack #sales):
  - Slack renders raw mrkdwn links (<https://url|text>).
  - Post one top-level message: lead name, company, source, and the fit
    score in a single line.
  - Thread the full package under it: the dossier, the drafts, and your
    recommendation (send / revise / skip).
  - After posting, call auto.chat.subscribe for the thread. Treat replies
    as revision requests or disposition decisions: revise drafts in the
    same thread, and confirm when a human marks the lead handled.

  Hard limits: never email, message, or otherwise contact a prospect;
  never invent personal data; never post a lead's details anywhere except
  the #sales thread.
initialPrompt: |
  A new lead arrived.

  Lead:
  - Name: {{name}}
  - Email: {{email}}
  - Company: {{company}}
  - Source: {{source}}
  - Notes: {{notes}}

  Research the lead per your profile, then post the dossier and draft
  package to Slack #sales and subscribe to the thread for revisions and
  disposition.
tools:
  auto:
    kind: local
    implementation: auto
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
triggers:
  - event: chat.message.mentioned
    connection: slack
    where:
      $.chat.provider: slack
      $.auto.authored: false
      $.auto.attributions:
        exists: false
    message: |
      {{message.author.userName}} mentioned you on Slack:

      {{message.text}}

      Channel: {{chat.channelId}}
      Thread: {{chat.threadId}}

      Reply in that thread with chat.send. If the user provides lead details
      or clearly asks for lead research, handle it. If required context is
      missing, ask for the lead details. Otherwise, briefly explain that you
      research inbound leads, score fit, draft outreach, and post packages to
      #sales for human approval.
    routing:
      kind: spawn
  - event: webhook.lead.created
    endpoint: lead-webhook
    auth:
      kind: bearer_token
      secretRef: lead-webhook-secret
    routing:
      kind: spawn
  - events:
      - chat.message.mentioned
      - chat.message.subscribed
    connection: slack
    where:
      $.chat.provider: slack
      $.auto.authored: false
      $.auto.attributions:
        exists: true
    message: |
      {{message.author.userName}} replied in your lead thread:

      {{message.text}}

      Channel: {{chat.channelId}}
      Thread: {{chat.threadId}}

      Treat this as a revision request or a disposition decision. Revise
      drafts in the same thread, or confirm the lead is handled.
    routing:
      kind: deliver
      routeBy:
        kind: attributedSessions
      onUnmatched: drop
```

The example assumes a Slack connection named `slack`.

## Walkthrough

### The webhook trigger declares its own endpoint

```yaml theme={null}
- event: webhook.lead.created
  endpoint: lead-webhook
  auth:
    kind: bearer_token
    secretRef: lead-webhook-secret
  routing:
    kind: spawn
```

Custom webhook triggers replace `connection:` with `endpoint:` plus an `auth:` block. On apply, auto reserves the endpoint and returns a receipt with its globally unique ingest URL — `POST <base>/api/v1/webhook-endpoints/<slug>/events` — which an agent can look up later with the `auto.webhooks.get` tool. Auth is one of three kinds: `bearer_token` (caller sends `Authorization: Bearer <secret>`), `hmac_sha256` (caller signs the raw body into `x-auto-signature-256`), or `none`. Both authenticated kinds resolve `secretRef` against the project's secret store. See [cron and webhooks](/reference/events/cron-and-webhooks) and [secrets](/reference/secrets).

Create the secret before the trigger applies: a project secret named `lead-webhook-secret`, set from the web app's Secrets settings so the value never touches the repository. Secrets are write-only by default — no surface returns the plaintext back.

### The event key comes from the body

A webhook body must be a JSON object. Its top-level `event` string becomes the event key with a `webhook.` prefix — `"event": "lead.created"` routes as `webhook.lead.created`. A body with *no* `event` field routes under the fixed fallback key `webhook.received`; if your lead source cannot shape its payload, author the trigger on `event: webhook.received` (optionally with `fallback: true` as a catch-all) and discriminate with `where` clauses on the raw body instead. An optional top-level `dedupKey` string deduplicates redeliveries — same key, no second session; without it every delivery is a fresh event. The endpoint answers `202` with `{ eventRecordId, created: true }` on first delivery and `200` with `created: false` on a dedup hit.

### The payload is whatever you POST

Unlike provider events, webhook payloads are not normalized — the raw request body *is* the payload. That means the `initialPrompt` template reads your fields directly: `{{name}}`, `{{email}}`, `{{company}}`, `{{source}}`, `{{notes}}`. Missing fields render as empty strings, so a sparse lead still spawns cleanly. If you want routing-level filtering — say, only leads from the demo-request form — add a `where` clause on your own fields:

```yaml theme={null}
where:
  $.source: demo-request
```

### Attributions split fresh mentions from thread feedback

The two chat triggers cover the same `chat.message.mentioned` event with opposing filters on `$.auto.attributions`, the payload field linking a thread to the sessions already working it: `exists: false` spawns a conversational session for a fresh mention (someone pasting lead details directly in Slack), while `exists: true` delivers into the attributed session — the one that posted the package and subscribed via `auto.chat.subscribe`. This pairing is enforced at apply time, as is the `$.auto.authored: false` filter on the deliver arm. The result is the revision loop: every reply in a lead's thread reaches the exact session that owns that lead. See [triggers](/reference/triggers).

### Guardrails live at three layers

* **Structural:** no mount, and only two tools — the `auto` platform tools and Slack `chat`. There is no email tool, no CRM write, no repository. The agent cannot contact a prospect because nothing it can call sends email.
* **Prompt:** the hard-limits paragraph makes non-contact an explicit standing order, and confines lead details to the `#sales` thread.
* **Process:** output is a recommendation (`send / revise / skip`), and disposition is a human replying in the thread.

When you do want the agent writing to external systems — logging dossiers to a CRM — add the CRM's MCP server as a `mcp_remote` tool and extend the prompt deliberately; see [tools](/reference/tools).

## Coming Soon reference

<Warning>
  Lead Researcher is Coming Soon and is not currently installable. This page is
  a design reference until the role's catalog disposition changes.
</Warning>

## Adapt it

* Replace `slack` and `#sales`, and map the webhook payload to your real lead source's fields — the five in `initialPrompt` are an example shape, not a contract.
* Encode your actual ICP and messaging guidelines in the system prompt. That is what makes drafts sound like your company instead of a mail merge.
* Keep honest scoring. An outbound engine that can say "skip" is the one sales teams learn to trust.
* For batch prospecting, add a `kind: heartbeat` trigger that processes a queue (a CRM view, a spreadsheet) on a daily cadence — see the [research loop](/examples/research-loop) for heartbeat mechanics.
* Lead details are personal data: keep the prompt's rule that they appear nowhere except the approval surface.

## Try it

Merge the PR and let [GitHub Sync](/concepts/github-sync) apply the resources, then copy the ingest URL from the apply receipt (from a session, the `auto.webhooks.get` tool returns it too) and POST a smoke lead:

```sh theme={null}
curl -X POST "<ingest-url>" \
  -H "Authorization: Bearer $LEAD_WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "lead.created",
    "dedupKey": "smoke-jane-doe-1",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "company": "Example Corp",
    "source": "demo-request",
    "notes": "asked about enterprise pricing"
  }'
```

Expect a `202` with `created: true`, a fresh lead-researcher session in the web app, and a dossier-plus-drafts thread in `#sales`. Reply in the thread with a revision request and confirm the same session posts the revision. POST the same body again and confirm the `dedupKey` suppresses a second session.
