Twenty One Media
automationJune 18, 2026

Five Funnels, One Table: How We Keep Lead Attribution Clean

We have five ways a person can come into our pipeline. They can request a free ops audit from the homepage. They can fill out the Operations Engine inquiry form. They can grab the /impeccable skill. They can sign up for the proposal toolkit. They can use the contact form.

Five entry points. Five separate API routes. Five different emails that go out.

One Airtable table.

The Shared Function

Every route calls the same function:

await captureLead({
  name,
  email,
  path: "/toolkit",
  leadMagnet: "proposal-toolkit-v1",
  useCase: context,
});

captureLead is a thin wrapper around the Airtable REST API. It normalizes the email, checks if a record already exists for that address, and creates one if it doesn't. Every new record gets a Lead Magnet field populated with whatever identifier the calling route passed in.

The identifiers we're using right now:

  • ai-audit-v1 (homepage audit form)
  • operations-engine-audit-v1 (Operations Engine inquiry)
  • impeccable-skill-v1 (/impeccable skill request)
  • proposal-toolkit-v1 (toolkit signup)
  • contact-v1 (contact form)

In Airtable, filtering by that field is one click. A grouped view by Lead Magnet tells us which product is generating leads this month without building a dashboard or running a query.

First Touch, Not Last

When a person submits two different forms, they get one record. The deduplication check runs before every write:

const existing = await findByEmail(pat, normalizedEmail);
if (existing) {
  return { ok: true, existed: true, recordId: existing };
}

If the record already exists, we return without updating it. The first lead magnet that brought them in stays on the record. If someone grabbed /impeccable in February and then filled out the Operations Engine form in June, the record still says impeccable-skill-v1.

That's intentional. We want to know what originally pulled someone into our orbit, not just the last thing they touched before booking a call.

The Use Case Field

Each route also passes a useCase string when there's one available. The audit form sends the "what's broken" paragraph. The toolkit form sends the optional "what kind of video work do you do?" field. The Operations Engine form sends a structured string with their challenge, services, timeline, and budget tier.

That field goes straight into the Airtable record. When a call gets scheduled, the pre-call prep is: open Airtable, pull the record, read Use Case. The intake form did its job before anyone picked up the phone.

Why Not a Full CRM

We don't need one. Five lead sources, one table, two fields that matter before a call (Lead Magnet and Use Case). A full CRM adds setup time, ongoing maintenance, and a new system to train on.

Airtable with filtered views and a function that writes to it consistently is enough. The constraint that makes it work is that every route uses the same function and every record uses the same field names. A new lead source is ten minutes of work: add the route, call captureLead() with a new identifier, add a filter view in Airtable.

The discipline is in the shared contract, not the tooling.