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
- 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’seventfield becomes the event keywebhook.lead.created, which matches the agent’s webhook trigger and spawns a session. - 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.
- 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.
- 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. - 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..auto/fragments/environments/agent-runtime.yaml
.auto/agents/lead-researcher.yaml
slack.
Walkthrough
The webhook trigger declares its own endpoint
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 and 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-levelevent 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 theinitialPrompt 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:
Attributions split fresh mentions from thread feedback
The two chat triggers cover the samechat.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.
Guardrails live at three layers
- Structural: no mount, and only two tools — the
autoplatform tools and Slackchat. 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
#salesthread. - Process: output is a recommendation (
send / revise / skip), and disposition is a human replying in the thread.
mcp_remote tool and extend the prompt deliberately; see tools.
Coming Soon reference
Adapt it
- Replace
slackand#sales, and map the webhook payload to your real lead source’s fields — the five ininitialPromptare 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: heartbeattrigger that processes a queue (a CRM view, a spreadsheet) on a daily cadence — see the 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 apply the resources, then copy the ingest URL from the apply receipt (from a session, theauto.webhooks.get tool returns it too) and POST a smoke lead:
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.