JSPM

@headlessly/crm

0.1.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 8
  • Score
    100M100P100Q44160F
  • License MIT

headless.ly CRM entities — Contact, Company, Deal

Package Exports

  • @headlessly/crm

Readme

@headlessly/crm

Your CRM was designed for humans clicking buttons. Your agents deserve better.

import { Contact, Deal } from '@headlessly/crm'

await Contact.create({ name: 'Alice Chen', email: 'alice@acme.co', stage: 'Lead' })
await Contact.qualify('contact_fX9bL5nRd')

// A deal closes — billing, support, and marketing react instantly
Deal.closed(async (deal, $) => {
  await $.Subscription.create({ plan: 'pro', customer: deal.contact })
  await $.Ticket.create({ subject: `Welcome ${deal.name}`, requester: deal.contact })
  await $.Campaign.create({ name: `Onboard ${deal.name}`, type: 'Email' })
})

No HubSpot API. No Salesforce SOQL. No webhook middleware. Contacts, deals, and the entire business graph — in one typed system your agent can operate autonomously.

The Problem

HubSpot has 500+ API endpoints. Salesforce has SOQL, SOSL, Apex, and 40 years of enterprise baggage. Pipedrive is simpler but still human-first — built for sales reps dragging cards across a Kanban board.

None of them were built for an AI agent to operate.

Your agent doesn't need a drag-and-drop pipeline view. It needs Deal.close(). It doesn't need a contact record page with 47 tabs. It needs Contact.qualify(). It doesn't need a "workflow builder" GUI. It needs a BEFORE hook:

Contact.qualifying((contact) => {
  if (contact.leadScore < 50) throw new Error('Score too low to qualify')
})

One Typed Graph

When you close a deal in HubSpot, does your Stripe subscription activate? Does your Zendesk ticket system know? Does your analytics pipeline capture it?

Not without Zapier, webhook handlers, and a prayer.

In headless.ly, closing a deal IS activating a subscription IS tracking an event IS creating a ticket — because they're all nodes in the same graph:

import { Deal, Contact } from '@headlessly/crm'

Deal.closed(async (deal, $) => {
  await $.Subscription.create({ plan: 'pro', customer: deal.contact })
  await $.Event.create({ type: 'deal.closed', value: deal.value })
  await $.Campaign.create({ name: `Onboard ${deal.name}`, type: 'Email' })
})

Contact.qualified(async (contact, $) => {
  await $.Deal.create({ name: `${contact.name} opportunity`, contact: contact.$id })
  await $.Activity.create({ subject: `Follow up with ${contact.name}`, type: 'Task' })
})

No webhooks. No Zapier. No integration tax. One graph.

Install

npm install @headlessly/crm

Entities

Contact

People in your graph — leads, customers, partners, stakeholders.

import { Contact } from '@headlessly/crm'

const alice = await Contact.create({
  name: 'Alice Chen',
  email: 'alice@acme.co',
  role: 'DecisionMaker',
  stage: 'Lead',
})

await Contact.qualify(alice.$id)

Contact.qualified((contact, $) => {
  $.Deal.create({ name: `${contact.name} opportunity`, contact: contact.$id })
})

Verbs: qualify() · capture() · assign() · merge() · enrich() — each with full lifecycle conjugation

Key fields: name, email, phone, title, role (DecisionMaker | Influencer | Champion | Blocker | User), status (Active | Inactive | Bounced | Unsubscribed), leadScore, source

Relationships: → Organization, → Manager, ← Leads[], ← Activities[], ← Deals[]

Deal

Revenue opportunities with real verbs — not status string updates.

import { Deal } from '@headlessly/crm'

const deal = await Deal.create({
  name: 'Acme Enterprise',
  value: 50000,
  stage: 'Qualification',
  contact: 'contact_fX9bL5nRd',
  organization: 'organization_e5JhLzXc',
})

await Deal.close(deal.$id)
await Deal.win(deal.$id)

Verbs: close() · win() · lose() · advance() · reopen() — each with full lifecycle conjugation

Key fields: name, value, currency, stage (Prospecting | Qualification | Proposal | Negotiation | Closed | Won | Lost), probability, expectedCloseDate, source

Relationships: → Organization, → Contact, → Owner, → Campaign, ← Leads[], ← Activities[]

Company

Organizations in your CRM graph with hierarchy support.

import { Organization } from '@headlessly/crm'

await Organization.create({
  name: 'Acme Corp',
  type: 'Customer',
  tier: 'Startup',
  industry: 'Software',
})

Key fields: name, domain, type (Prospect | Customer | Partner | Vendor | Competitor), status, tier (Enterprise | Business | Startup | SMB), industry, employeeCount, annualRevenue

Relationships: → Parent, ← Subsidiaries[], ← Contacts[], ← Deals[], ← Subscriptions[]

Lead

Inbound and outbound leads with conversion tracking.

import { Lead } from '@headlessly/crm'

await Lead.create({ name: 'Acme Inbound', source: 'Website', contact: 'contact_fX9bL5nRd' })
await Lead.convert('lead_k7TmPvQx')

Verbs: convert() · lose() — each with full lifecycle conjugation

Activity

Calls, emails, meetings, and tasks linked to contacts and deals.

import { Activity } from '@headlessly/crm'

await Activity.create({
  subject: 'Discovery call with Alice',
  type: 'Call',
  contact: 'contact_fX9bL5nRd',
  deal: 'deal_k7TmPvQx',
})

await Activity.complete('activity_mN8pZwKj')

Verbs: complete() · cancel() · log() — each with full lifecycle conjugation

Pipeline

Named pipeline configurations for organizing deal stages.

import { Pipeline } from '@headlessly/crm'

await Pipeline.create({
  name: 'Enterprise Sales',
  stages: 'Discovery, Qualification, Proposal, Negotiation, Closed',
})

Agent-Native

Your agent connects to one MCP server. It can operate your entire CRM:

{ "type": "Contact", "filter": { "stage": "Lead", "leadScore": { "$gte": 50 } } }
{ "type": "Deal", "id": "deal_fX9bL5nRd", "include": ["contact", "organization", "activities"] }
const qualified = await $.Contact.find({ stage: 'Qualified' })
for (const contact of qualified) {
  await $.Deal.create({
    name: `${contact.name} opportunity`,
    contact: contact.$id,
    stage: 'Prospecting',
  })
}

Three tools. Not three hundred endpoints.

Cross-Domain Operations

Query results are standard arrays — chain operations with familiar JavaScript:

const active = await Contact.find({ status: 'Active' })
for (const contact of active) {
  const deals = await Deal.find({ contact: contact.$id })
  const open = deals.filter((d) => d.stage !== 'Won' && d.stage !== 'Lost')
  for (const deal of open) {
    await Deal.update(deal.$id, { lastContactedAt: new Date().toISOString() })
  }
}

License

MIT