Headlessly
CRM

Lead

Inbound leads with BANT qualification, campaign attribution, and conversion tracking.

Schema

import { Noun } from 'digital-objects'

export const Lead = Noun('Lead', {
  name: 'string!',
  contact: '-> Contact.leads',
  organization: '-> Organization',
  owner: '-> Contact',
  status: 'New | Contacted | Qualified | Converted | Lost',
  source: 'string!',
  sourceDetail: 'string',
  campaign: '-> Campaign.leads',
  score: 'number',
  budget: 'number',
  authority: 'string',
  need: 'string',
  timeline: 'string',
  deal: '-> Deal.leads',
  convertedAt: 'datetime',
  lostReason: 'string',
  lostAt: 'datetime',
  firstTouchAt: 'datetime',
  lastActivityAt: 'datetime',
  convert: 'Converted',
  lose: 'Lost',
})

Fields

FieldTypeRequiredDescription
namestringYesLead name or description
contact-> ContactNoThe person associated with this lead
organization-> OrganizationNoThe company associated with this lead
owner-> ContactNoSales rep or agent responsible for this lead
statusenumNoNew, Contacted, Qualified, Converted, or Lost
sourcestringYesAcquisition channel (website, referral, event, etc.)
sourceDetailstringNoSpecific source detail (e.g., campaign name, URL)
campaign-> CampaignNoMarketing campaign that generated this lead
scorenumberNoLead qualification score
budgetnumberNoBANT: Estimated budget
authoritystringNoBANT: Decision-making authority
needstringNoBANT: Business need or pain point
timelinestringNoBANT: Purchase timeline
deal-> DealNoDeal created from this lead after conversion
convertedAtdatetimeNoTimestamp when the lead was converted
lostReasonstringNoWhy the lead was lost
lostAtdatetimeNoTimestamp when the lead was marked as lost
firstTouchAtdatetimeNoTimestamp of first interaction
lastActivityAtdatetimeNoTimestamp of most recent activity

Relationships

FieldDirectionTargetDescription
contact->Contact.leadsThe person behind this lead
organization->OrganizationThe company behind this lead
owner->ContactSales rep or agent assigned to work this lead
campaign->Campaign.leadsMarketing campaign that generated this lead
deal->Deal.leadsDeal created when the lead converts

Verbs

VerbEventDescription
createCreatedCreate a new lead
updateUpdatedUpdate lead fields
deleteDeletedDelete a lead
convertConvertedConvert the lead into a deal
loseLostMark the lead as lost with a reason

Verb Lifecycle

import { Lead } from '@headlessly/crm'

// BEFORE hook -- validate conversion prerequisites
Lead.converting(lead => {
  if (!lead.contact) throw new Error('Lead must have a contact before conversion')
  if (lead.status === 'Lost') throw new Error('Cannot convert a lost lead')
})

// Execute -- convert the lead
await Lead.convert('lead_pQ8xNfKm')

// AFTER hook -- create the deal and update related records
Lead.converted((lead, $) => {
  const deal = $.Deal.create({
    name: lead.name,
    contact: lead.contact,
    organization: lead.organization,
    value: lead.budget || 0,
    stage: 'Prospecting',
    source: lead.source,
    campaign: lead.campaign,
  })
  $.Lead.update(lead.$id, { deal: deal.$id })
  $.Contact.update(lead.contact, { stage: 'Qualified' })
})

Status State Machine

New --> Contacted --> Qualified --> Converted
  \         \            \
   \         \            \
    -----------\-----------> Lost
  • New: Lead just entered the system, no outreach yet
  • Contacted: Initial outreach has been made
  • Qualified: Passed BANT criteria or scoring threshold
  • Converted: Successfully converted to a Deal
  • Lost: Disqualified or unresponsive, with a recorded reason

Leads are terminal once Converted or Lost. A lost lead can be reopened by updating its status back to New.

BANT Qualification

The Lead entity has built-in fields for BANT qualification:

FieldBANTDescription
budgetBudgetWhat can the prospect spend?
authorityAuthorityWho makes the purchasing decision?
needNeedWhat problem are they solving?
timelineTimelineWhen do they need a solution?
import { Lead } from '@headlessly/crm'

await Lead.create({
  name: 'Acme Platform Evaluation',
  contact: 'contact_fX9bL5nRd',
  organization: 'org_Nw8rTxJv',
  source: 'demo-request',
  budget: 50_000,
  authority: 'CTO - final decision maker',
  need: 'Replace fragmented SaaS tools with unified platform',
  timeline: 'Q2 2025',
})

Cross-Domain Patterns

Lead is the bridge between Marketing and CRM:

  • Marketing: Lead.campaign tracks which Campaign generated the lead. Lead.source records the channel. This data feeds attribution models via Analytics.
  • CRM (Contact): Every lead references a Contact. When the lead converts, the contact's stage advances to Qualified or Customer.
  • CRM (Deal): Lead.convert() creates a Deal. The Lead.deal back-link preserves the full attribution chain from campaign to revenue.
  • Analytics: Lead creation, conversion, and loss events feed Funnels. Time from firstTouchAt to convertedAt measures sales cycle length.
import { Lead } from '@headlessly/crm'

// Track lead-to-deal conversion metrics
Lead.converted((lead, $) => {
  const cycleMs = new Date(lead.convertedAt).getTime() - new Date(lead.firstTouchAt).getTime()
  const cycleDays = Math.round(cycleMs / 86_400_000)
  $.Event.create({
    name: 'lead_converted',
    properties: {
      source: lead.source,
      cycleDays,
      budget: lead.budget,
    },
  })
})

Query Examples

SDK

import { Lead } from '@headlessly/crm'

// Find high-value leads from a specific campaign
const hotLeads = await Lead.find({
  status: 'Qualified',
  campaign: 'campaign_hR5wLxPn',
  score: { $gte: 80 },
})

// Get a lead with full context
const lead = await Lead.get('lead_pQ8xNfKm', {
  include: ['contact', 'organization', 'campaign', 'deal'],
})

// Create a lead from a form submission
await Lead.create({
  name: 'Website Demo Request',
  contact: 'contact_fX9bL5nRd',
  source: 'website',
  sourceDetail: '/pricing',
  score: 65,
})

MCP

headless.ly/mcp#search
{
  "type": "Lead",
  "filter": { "status": "New", "source": "website" },
  "sort": { "score": "desc" },
  "limit": 25
}

REST

# List leads filtered by status
curl https://crm.headless.ly/~acme/leads?status=Qualified

# Get a specific lead
curl https://crm.headless.ly/~acme/leads/lead_pQ8xNfKm

# Create a lead
curl -X POST https://crm.headless.ly/~acme/leads \
  -H 'Content-Type: application/json' \
  -d '{"name": "Inbound Demo", "contact": "contact_fX9bL5nRd", "source": "website"}'

# Convert a lead
curl -X POST https://crm.headless.ly/~acme/leads/lead_pQ8xNfKm/convert

On this page