Inbound qualification
Turn the website's demo form into qualified inbound: each submission runs a Cargo tool that identifies the company by work email, qualifies it against the ICP, lands account and contact in the shared GTM models, posts it to Slack, and answers on the page with a booking link or a thank-you; an optional play then tiers and summarises each qualified contact.
Set up the inbound-qualification cookbook in this project.
1. If the Cargo CLI is not installed yet, follow every step in https://api.getcargo.io/INSTALL.md
2. From the Cargo project, run: cargo-ai cdk add cookbook/inbound-qualification
3. Then follow .claude/skills/inbound-qualification/SKILL.md. Stop for my approval before any paid call, and stop at "cargo-ai project plan" before deploying anything.$cargo-ai cdk add cookbook/inbound-qualificationSay this to your agent
Add a demo form to www.fabrikam.example: qualified companies get our booking link, the rest a thank-you, and the sales channel hears about every request.
Illustrative output #
Fictional records, to show the shape of what comes back.
tool:inbound_form deployed public form on https://www.fabrikam.example
POST /contact ada@northwind.example -> qualified (320 employees, United States)
gtm_accounts northwind.example upserted
gtm_contacts ada@northwind.example lead_source website, inbound_status qualified
#inbound "New inbound, qualified from Ada Lovelace at Northwind"
page "Thanks, let's find a time." [Book a demo]
play research_qualified_leads (minutes later, once enabled)
gtm_contacts ada@northwind.example inbound_tier A, inbound_brief written
#inbound ":mag: Ada Lovelace: tier A. Head of RevOps at Northwind, ..."
POST /contact sam@gmail.com -> work_email_required (nothing paid or written)
Done when #
node --import tsx evals/contract.mjspasses: one tool whose public form is enabled for exactly the canonical origin with no CAPTCHA secret in code, a workflow that refuses personal email before the enrichment call and writes onlygtm_accountsandgtm_contacts, both models matching the shared definitions- the plan shows the tool, the models and the connectors, and the app update with the env token
- a work-email submission on the live site returns the right answer on the page, upserts the account and the contact (with
account_id,lead_sourcewebsite,inbound_status), and posts once to the Slack channel - a personal-email submission is refused on the page, with no enrichment call and no row written
- a submission from another origin is refused with 403, and one sent faster than the minimum fill time is refused
- loading the contact page sets no
cargo_anon_idcookie until a submission - the privacy page has a contact form section the operator reviewed
- the research play, run by hand on one qualified contact, wrote
inbound_tier,inbound_brief,inbound_rationaleandinbound_researched_atonto it and posted one note; a second manual run on the same contact did nothing - every fact in the brief traces to the record or a listed source
What it costs #
Each submission from a work email pays for one LinkedIn company enrichment; a refused personal
email pays nothing. Read the live price from the integration (cargo-ai connection integration get linkedin) and say it per submission. The server’s rate limit caps a single address at ten
submissions a minute, and the origin allow-list keeps other sites from spending it. The models and
the Slack post add nothing. The deep research, once enabled, adds one agent run per qualified
contact (LLM tokens on the Anthropic connector plus up to four web searches); a not-qualified or
personal-email submission never reaches it.
The website’s demo form, wired to a Cargo tool: each submission is qualified, lands in the shared GTM accounts and contacts, is posted to Slack, and is answered on the page.
visitor submits /contact/ (headless form SDK, the site's own markup)
-> public form API: origin, honeypot, time-trap, rate limit
-> tool inbound_form: personal email? -> company from LinkedIn -> ICP rules
-> gtm_accounts + gtm_contacts -> Slack
<- booking link or thank-you, when the run finishes (async: the SDK polls)
later, for each contact marked qualified (play research_qualified_leads, ships disabled)
-> agent: ICP + tiering rubric from context, web research -> tier, brief
-> inbound_tier / inbound_brief / inbound_rationale / inbound_researched_at on the contact
-> one note to the same Slack channel
Why it is built this way #
A tool, not a backend. Cargo tools take a public form: the workflow’s input is the field list,
publicForm holds the origin allow-list and the spam guards, and each submission runs the
workflow. Nothing else to host.
The shared GTM models. The account and the contact land in gtm_accounts and gtm_contacts,
declared exactly as every pipeline declares them, so the worked example deploys on a bare
workspace and other pipelines read the same inbound contacts. A CRM is the crm-backed variation.
Cheapest refusal first. A personal email is refused before the paid company lookup; the lookup comes before any write. The company is enriched, never the person.
Rules, not a model call. Headcount band and countries from the ICP: free, explainable, the same answer twice. An agent judgement is a variation.
Research after the answer. The deep research is a separate play on the contact row, never part of the form’s workflow, so the visitor never waits on a model call. It ships disabled and runs once per qualified contact.
The SDK loads after hydration. Its load time is what the server’s minimum fill time counts from, so it loads when the form becomes usable. It sets its id cookie only on submit, so a visitor who never submits gets none.
Placeholders (edit before deploy) #
- ICP rules:
icpMinEmployees,icpMaxEmployees,icpCountriesininfra/tools/inbound-form.ts. - Booking link:
bookingUrl. - Slack channel:
slackChannelIdininfra/settings.ts, shared by the form and the research note. - Origin:
publicForm.allowedOrigins, the site’s canonical origin.
Files #
| Path | What it is |
|---|---|
infra/tools/inbound-form.ts | The tool, its workflow and its public form |
infra/plays/research-qualified-leads.ts | The deep-research play, shipped disabled |
infra/agents/lead-researcher.ts | The research agent: tier and brief, as JSON |
infra/settings.ts | The Slack channel both post to |
infra/connectors/anthropic.ts | The model the research agent runs on |
infra/models/gtm-accounts.ts | The shared GTM accounts model |
infra/models/gtm-contacts.ts | The shared GTM contacts model, with the inbound columns |
infra/connectors/linkedin.ts | LinkedIn company data on Cargo’s credits |
infra/connectors/slack.ts | The Slack workspace the inbound channel lives in |
site/components/inbound-form.tsx | The form, headless, loading the SDK after hydration |
site/app/contact/page.tsx | The page that holds it |
references/form.md | Wiring the site, the spam checks, privacy |
references/data.md | The rows, querying, the crm-backed mapping |
evals/contract.mjs | Graph contract |
Verify #
node --import tsx inbound-qualification/evals/contract.mjs