Headlessly
CRM

Deal

Sales opportunities with value, pipeline stage tracking, and close-to-subscribe automation.

Schema

import { Noun } from 'digital-objects'

export const Deal = Noun('Deal', {
  name: 'string!',
  organization: '-> Organization.deals',
  contact: '-> Contact',
  owner: '-> Contact',
  value: 'number!',
  currency: 'string',
  recurringValue: 'number',
  recurringInterval: 'Monthly | Quarterly | Yearly',
  stage: 'Prospecting | Qualification | Proposal | Negotiation | Closed | Won | Lost',
  probability: 'number',
  expectedCloseDate: 'date',
  actualCloseDate: 'date',
  description: 'string',
  nextStep: 'string',
  competitorNotes: 'string',
  lostReason: 'string',
  wonReason: 'string',
  leads: '<- Lead.deal[]',
  source: 'string',
  campaign: '-> Campaign',
  activities: '<- Activity.deal[]',
  lastActivityAt: 'datetime',
  close: 'Closed',
  win: 'Won',
  lose: 'Lost',
  advance: 'Advanced',
  reopen: 'Reopened',
})

Fields

FieldTypeRequiredDescription
namestringYesDeal title or opportunity name
organization-> OrganizationNoThe company this deal is with
contact-> ContactNoPrimary contact for this deal
owner-> ContactNoSales rep or agent responsible
valuenumberYesTotal deal value in base currency
currencystringNoCurrency code (USD, EUR, etc.)
recurringValuenumberNoRecurring revenue component
recurringIntervalenumNoMonthly, Quarterly, or Yearly
stageenumNoCurrent pipeline stage
probabilitynumberNoWin probability (0-100)
expectedCloseDatedateNoProjected close date
actualCloseDatedateNoActual close date
descriptionstringNoDeal description and notes
nextStepstringNoNext action item
competitorNotesstringNoCompetitive intelligence
lostReasonstringNoWhy the deal was lost
wonReasonstringNoWhy the deal was won
leads<- Lead[]NoLeads that fed into this deal
sourcestringNoAcquisition source
campaign-> CampaignNoMarketing campaign attribution
activities<- Activity[]NoAll activities on this deal
lastActivityAtdatetimeNoTimestamp of the most recent activity

Relationships

FieldDirectionTargetDescription
organization->Organization.dealsThe company this deal is with
contact->ContactPrimary point of contact
owner->ContactSales rep or agent who owns this deal
campaign->CampaignMarketing campaign that sourced this deal
leads<-Lead.deal[]Leads that converted into this deal
activities<-Activity.deal[]All logged activities (calls, emails, meetings)

Verbs

VerbEventDescription
createCreatedCreate a new deal
updateUpdatedUpdate deal fields
deleteDeletedDelete a deal
closeClosedClose the deal (neutral -- use win or lose for outcome)
winWonMark the deal as won
loseLostMark the deal as lost with a reason
advanceAdvancedMove to the next pipeline stage
reopenReopenedReopen a closed, won, or lost deal

Verb Lifecycle

import { Deal } from '@headlessly/crm'

// BEFORE hook -- validate the deal before closing
Deal.winning(deal => {
  if (!deal.value) throw new Error('Deal value required to close')
  if (!deal.contact) throw new Error('Contact required to close')
})

// Execute -- win the deal
await Deal.win('deal_k7TmPvQx')

// AFTER hook -- trigger billing and update the contact
Deal.won((deal, $) => {
  const customer = $.Customer.create({
    name: deal.organization,
    organization: deal.organization,
  })
  $.Subscription.create({
    customer: customer.$id,
    plan: 'pro',
    organization: deal.organization,
  })
  $.Contact.update(deal.contact, { stage: 'Customer' })
})

Status State Machine

Prospecting --> Qualification --> Proposal --> Negotiation --> Won
     |               |               |              |          |
     v               v               v              v          v
    Lost            Lost            Lost           Lost     Reopened
                                                              |
                                                              v
                                                          Prospecting
  • Prospecting: Initial outreach and discovery
  • Qualification: Evaluating fit and budget
  • Proposal: Formal proposal delivered
  • Negotiation: Terms and pricing under discussion
  • Closed: Deal completed (general close)
  • Won: Deal closed with a positive outcome
  • Lost: Deal closed with a negative outcome
  • Reopened: Previously closed deal re-entered the pipeline

The advance verb moves a deal forward one stage. The win and lose verbs jump directly to terminal states from any stage.

Pipeline Integration

Deals flow through stages defined by a Pipeline. The default pipeline stages mirror the Deal stage enum, but custom pipelines can define different stage sequences:

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

// Use a custom pipeline
const pipeline = await Pipeline.get('pipeline_mR4nVkTw')

await Deal.create({
  name: 'Acme Enterprise',
  value: 120_000,
  stage: 'Prospecting',
  organization: 'org_Nw8rTxJv',
  contact: 'contact_fX9bL5nRd',
})

// Advance through stages
await Deal.advance('deal_k7TmPvQx')
await Deal.advance('deal_k7TmPvQx')

Cross-Domain Patterns

Deal is where CRM meets revenue. It connects relationships to money:

  • Billing: Deal.won() is the natural trigger for Customer and Subscription creation. The deal value maps to the subscription amount. recurringValue and recurringInterval feed directly into billing.
  • Marketing: Deal.campaign and Deal.source close the attribution loop. Revenue from won deals flows back to Campaign ROI calculations.
  • Analytics: Deal stage transitions are events. Weighted pipeline value (value * probability) feeds forecast metrics. Time-to-close and win rate are derived from deal events.
  • CRM (Lead): Deal.leads shows which leads converted into this deal, preserving the full acquisition chain.
  • CRM (Activity): Deal.activities logs every touchpoint -- calls, demos, proposals -- that contributed to the outcome.
import { Deal } from '@headlessly/crm'

// Track weighted pipeline for forecasting
Deal.advanced((deal, $) => {
  $.Metric.create({
    name: 'pipeline_weighted',
    value: deal.value * (deal.probability / 100),
    dimensions: { stage: deal.stage, owner: deal.owner },
  })
})

Query Examples

SDK

import { Deal } from '@headlessly/crm'

// Find high-value deals in negotiation
const bigDeals = await Deal.find({
  stage: 'Negotiation',
  value: { $gte: 50_000 },
})

// Get a deal with full context
const deal = await Deal.get('deal_k7TmPvQx', {
  include: ['organization', 'contact', 'activities', 'leads'],
})

// Create a deal from a converted lead
await Deal.create({
  name: 'Acme Platform License',
  value: 48_000,
  recurringValue: 4_000,
  recurringInterval: 'Monthly',
  organization: 'org_Nw8rTxJv',
  contact: 'contact_fX9bL5nRd',
  stage: 'Prospecting',
  source: 'inbound',
  expectedCloseDate: '2025-06-30',
})

MCP

headless.ly/mcp#search
{
  "type": "Deal",
  "filter": { "stage": "Negotiation", "value": { "$gte": 50000 } },
  "sort": { "value": "desc" },
  "limit": 20
}

REST

# List deals in a specific stage
curl https://crm.headless.ly/~acme/deals?stage=Negotiation

# Get a specific deal
curl https://crm.headless.ly/~acme/deals/deal_k7TmPvQx

# Create a deal
curl -X POST https://crm.headless.ly/~acme/deals \
  -H 'Content-Type: application/json' \
  -d '{"name": "Acme Enterprise", "value": 48000, "stage": "Prospecting", "contact": "contact_fX9bL5nRd"}'

# Win a deal
curl -X POST https://crm.headless.ly/~acme/deals/deal_k7TmPvQx/win

# Advance a deal to the next stage
curl -X POST https://crm.headless.ly/~acme/deals/deal_k7TmPvQx/advance

On this page