# Contact (/entities/crm/contact)



Schema [#schema]

```typescript
import { Noun } from 'digital-objects'

export const Contact = Noun('Contact', {
  name: 'string!',
  firstName: 'string',
  lastName: 'string',
  email: 'string##',
  phone: 'string',
  mobile: 'string',
  avatar: 'string',
  title: 'string',
  department: 'string',
  organization: '-> Organization.contacts',
  role: 'DecisionMaker | Influencer | Champion | Blocker | User',
  stage: 'Lead | Qualified | Customer | Churned | Partner',
  status: 'Active | Inactive | Bounced | Unsubscribed',
  source: 'string',
  leadScore: 'number',
  preferredChannel: 'Email | Phone | SMS | Chat',
  timezone: 'string',
  language: 'string',
  leads: '<- Lead.contact[]',
  activities: '<- Activity.contact[]',
  manager: '-> Contact.reports',
  reports: '<- Contact.manager[]',
  linkedinUrl: 'string',
  twitterHandle: 'string',
  marketingConsent: 'string',
  lastEngagement: 'datetime',
  qualify: 'Qualified',
  capture: 'Captured',
  assign: 'Assigned',
  merge: 'Merged',
  enrich: 'Enriched',
})
```

Fields [#fields]

| Field              | Type                     | Required | Description                                           |
| ------------------ | ------------------------ | -------- | ----------------------------------------------------- |
| `name`             | string                   | Yes      | Full display name                                     |
| `firstName`        | string                   | No       | First name                                            |
| `lastName`         | string                   | No       | Last name                                             |
| `email`            | string (unique, indexed) | No       | Primary email address                                 |
| `phone`            | string                   | No       | Office phone number                                   |
| `mobile`           | string                   | No       | Mobile phone number                                   |
| `avatar`           | string                   | No       | URL to avatar image                                   |
| `title`            | string                   | No       | Job title                                             |
| `department`       | string                   | No       | Department within the organization                    |
| `organization`     | -> Organization          | No       | Organization this contact belongs to                  |
| `role`             | enum                     | No       | DecisionMaker, Influencer, Champion, Blocker, or User |
| `stage`            | enum                     | No       | Lead, Qualified, Customer, Churned, or Partner        |
| `status`           | enum                     | No       | Active, Inactive, Bounced, or Unsubscribed            |
| `source`           | string                   | No       | Acquisition source                                    |
| `leadScore`        | number                   | No       | Computed lead score for prioritization                |
| `preferredChannel` | enum                     | No       | Email, Phone, SMS, or Chat                            |
| `timezone`         | string                   | No       | IANA timezone identifier                              |
| `language`         | string                   | No       | Preferred language code                               |
| `leads`            | \<- Lead\[]              | No       | Leads associated with this contact                    |
| `activities`       | \<- Activity\[]          | No       | All activities involving this contact                 |
| `manager`          | -> Contact               | No       | Manager of this contact                               |
| `reports`          | \<- Contact\[]           | No       | Direct reports                                        |
| `linkedinUrl`      | string                   | No       | LinkedIn profile URL                                  |
| `twitterHandle`    | string                   | No       | Twitter/X handle                                      |
| `marketingConsent` | string                   | No       | Marketing consent status                              |
| `lastEngagement`   | datetime                 | No       | Timestamp of the last interaction                     |

Relationships [#relationships]

| Field          | Direction | Target                | Description                                    |
| -------------- | --------- | --------------------- | ---------------------------------------------- |
| `organization` | ->        | Organization.contacts | The company this contact works at              |
| `leads`        | \<-       | Lead.contact\[]       | Leads generated by this contact                |
| `activities`   | \<-       | Activity.contact\[]   | Calls, emails, meetings involving this contact |
| `manager`      | ->        | Contact.reports       | Reporting manager (self-referencing hierarchy) |
| `reports`      | \<-       | Contact.manager\[]    | Direct reports under this contact              |

Verbs [#verbs]

| Verb      | Event       | Description                                            |
| --------- | ----------- | ------------------------------------------------------ |
| `create`  | `Created`   | Create a new contact                                   |
| `update`  | `Updated`   | Update contact fields                                  |
| `delete`  | `Deleted`   | Delete a contact                                       |
| `qualify` | `Qualified` | Move from Lead to Qualified stage                      |
| `capture` | `Captured`  | Record a new lead capture with source                  |
| `assign`  | `Assigned`  | Assign the contact to an owner for follow-up           |
| `merge`   | `Merged`    | Merge duplicate contacts into one record               |
| `enrich`  | `Enriched`  | Enrich with external data (social profiles, job title) |

Verb Lifecycle [#verb-lifecycle]

```typescript
import { Contact } from '@headlessly/crm'

// BEFORE hook -- validate prerequisites
Contact.qualifying(contact => {
  if (!contact.email) throw new Error('Email required for qualification')
  if (contact.leadScore < 40) throw new Error('Lead score too low')
})

// Execute -- qualify the contact
await Contact.qualify('contact_fX9bL5nRd')

// AFTER hook -- trigger downstream actions
Contact.qualified((contact, $) => {
  $.Deal.create({
    name: `${contact.name} - Inbound`,
    value: 12_000,
    contact: contact.$id,
    organization: contact.organization,
    stage: 'Prospecting',
  })
  $.Activity.create({
    subject: `Follow up with ${contact.name}`,
    type: 'Task',
    contact: contact.$id,
    status: 'Pending',
    priority: 'High',
  })
})
```

Status State Machine [#status-state-machine]

Stage Progression [#stage-progression]

```
Lead --> Qualified --> Customer --> Churned
                          |
                          v
                        Partner
```

* **Lead**: First contact, not yet evaluated
* **Qualified**: Passed BANT or scoring threshold
* **Customer**: Active paying customer
* **Churned**: Former customer who left
* **Partner**: Strategic or integration partner

Status Values [#status-values]

* **Active**: Engaged, receiving communications
* **Inactive**: No recent engagement
* **Bounced**: Email delivery failed
* **Unsubscribed**: Opted out of marketing

Cross-Domain Patterns [#cross-domain-patterns]

Contact is the central identity entity that connects CRM to every other domain:

* **Billing**: When `stage` moves to Customer, a billing Customer record links this contact to Stripe. `Contact.organization.subscriptions` gives the full billing picture.
* **Marketing**: Contacts belong to Segments and receive Campaigns. `Contact.source` and `Contact.leads[].campaign` track marketing attribution.
* **Support**: Contact becomes the reporter on Tickets. Historical activities give support agents context.
* **Analytics**: Every verb on a Contact (qualify, capture, assign) emits Events that feed Funnels.
* **Communication**: Messages link to contacts via `Message.sender` and `Message.recipient`.

```typescript
import { Contact } from '@headlessly/crm'

// When a contact churns, trigger a win-back campaign
Contact.updated((contact, $) => {
  if (contact.stage === 'Churned') {
    $.Campaign.create({
      name: `Win-back: ${contact.name}`,
      type: 'Email',
      segment: 'churned-customers',
    })
  }
})
```

Query Examples [#query-examples]

SDK [#sdk]

```typescript
import { Contact } from '@headlessly/crm'

// Find qualified leads at enterprise organizations
const qualified = await Contact.find({
  stage: 'Qualified',
  role: 'DecisionMaker',
})

// Get a contact with all related data
const contact = await Contact.get('contact_fX9bL5nRd', {
  include: ['organization', 'leads', 'activities'],
})

// Capture a new lead
await Contact.capture({
  name: 'Bob Rivera',
  email: 'bob@startup.io',
  source: 'demo-request-form',
  organization: 'org_Nw8rTxJv',
})
```

MCP [#mcp]

```json title="headless.ly/mcp#search"
{
  "type": "Contact",
  "filter": { "stage": "Lead", "source": "website" },
  "sort": { "leadScore": "desc" },
  "limit": 50
}
```

REST [#rest]

```bash
