Headlessly
Concepts

Verbs

Complete reference for the verb conjugation system — CRUD defaults, custom verbs, hooks, execution modes, and the full verb table.

Conjugation Pattern

Every verb in the system follows the same four-part conjugation:

verb
  ├── verb()        → execute (imperative)
  ├── verbing()     → BEFORE hook (present participle)
  ├── verbed()      → AFTER hook (past tense)
  └── verbedBy      → audit trail (passive voice)
import { Contact } from '@headlessly/crm'

// Execute the verb
await Contact.qualify({ id: 'contact_fX9bL5nRd' })

// BEFORE hook — runs before execution
Contact.qualifying(contact => {
  if (!contact.email) throw new Error('Cannot qualify without email')
  return contact
})

// AFTER hook — runs after execution
Contact.qualified((contact, $) => {
  $.Issue.create({
    title: `Follow up with ${contact.name}`,
    contact: contact.$id,
    status: 'Open',
  })
})

// Audit — queryable reverse lookup
const events = await Contact.qualifiedBy('agent_mR4nVkTw')

Default CRUD Verbs

Every Noun receives three CRUD verbs automatically. No declaration needed.

VerbParticiplePast TenseAuditEvent
createcreatingcreatedcreatedByEntity.Created
updateupdatingupdatedupdatedByEntity.Updated
deletedeletingdeleteddeletedByEntity.Deleted
import { Contact } from '@headlessly/crm'

// Full CRUD conjugation — available on every entity
Contact.creating(contact => { /* validate before create */ })
const created = await Contact.create({ name: 'Alice', stage: 'Lead' })
Contact.created(contact => { /* react after create */ })

Contact.updating((contact, changes) => { /* validate before update */ })
await Contact.update('contact_fX9bL5nRd', { stage: 'Qualified' })
Contact.updated(contact => { /* react after update */ })

Contact.deleting(contact => { /* guard before delete */ })
await Contact.delete('contact_fX9bL5nRd')
Contact.deleted(contact => { /* clean up after delete */ })

To remove a CRUD verb, set it to null in the Noun definition. See Digital Objects for details.

Custom Verb Declaration

Custom verbs are declared as properties on the Noun definition. The key is the verb infinitive; the value is the PascalCase past-tense event name:

import { Noun } from 'digital-objects'

export const Deal = Noun('Deal', {
  name: 'string!',
  value: 'number!',
  stage: 'Discovery | Proposal | Negotiation | Closed | Lost',
  contact: '-> Contact.deals',

  advance: 'Advanced',
  close:   'Closed',
  lose:    'Lost',
  reopen:  'Reopened',
})

Each declaration generates the full conjugation:

DeclarationExecuteBeforeAfterAudit
advance: 'Advanced'Deal.advance()Deal.advancing()Deal.advanced()Deal.advancedBy
close: 'Closed'Deal.close()Deal.closing()Deal.closed()Deal.closedBy
lose: 'Lost'Deal.lose()Deal.losing()Deal.lost()Deal.lostBy
reopen: 'Reopened'Deal.reopen()Deal.reopening()Deal.reopened()Deal.reopenedBy

BEFORE Hooks (Validation)

BEFORE hooks run synchronously before the verb executes. They receive the entity (or creation payload) and can:

  • Validate — throw to reject the operation
  • Transform — return a modified entity to change what gets written
  • Enrich — attach computed properties before persistence
import { Contact } from '@headlessly/crm'
import { Subscription } from '@headlessly/billing'

// Reject invalid operations
Contact.qualifying(contact => {
  if (!contact.email) throw new Error('Cannot qualify without email')
  if (!contact.organization) throw new Error('Contact must belong to an organization')
  return contact
})

// Transform before write
Subscription.creating(sub => {
  return { ...sub, startDate: new Date().toISOString() }
})

If a BEFORE hook throws, the verb does not execute and no event is emitted. The error propagates to the caller.

AFTER Hooks (Side Effects)

AFTER hooks run asynchronously after the verb succeeds and the event is written. They receive the entity and a $ context for cross-domain operations:

import { Deal } from '@headlessly/crm'

Deal.closed((deal, $) => {
  // Cross-domain: create a billing subscription
  $.Subscription.create({ plan: 'pro', contact: deal.contact })

  // Cross-domain: update the contact stage
  $.Contact.update(deal.contact, { stage: 'Customer' })

  // Cross-domain: create onboarding content
  $.Content.create({
    title: `Onboarding: ${deal.name}`,
    type: 'Article',
    status: 'Draft',
  })
})

The $ argument provides access to all 35 entities across all domains. Import it from @headlessly/sdk when used outside a hook:

import { $ } from '@headlessly/sdk'

Verb Execution Across Interfaces

The same verb works identically across all five interfaces:

SDK

import { Contact } from '@headlessly/crm'

await Contact.qualify({ id: 'contact_fX9bL5nRd' })

MCP

headless.ly/mcp#do
await $.Contact.qualify({ id: 'contact_fX9bL5nRd' })

CLI

npx @headlessly/cli do Contact.qualify contact_fX9bL5nRd

REST

POST https://crm.headless.ly/~my-startup/Contact/contact_fX9bL5nRd/qualify

Events

headless.ly/mcp#search
{ "type": "Event", "filter": { "type": "Contact.Qualified", "target": "contact_fX9bL5nRd" } }

All interfaces produce the same event, the same audit trail, and the same hook execution.

Code-as-Data Execution

Verb handlers (BEFORE and AFTER hooks) are serialized via fn.toString() and stored in the tenant's Durable Object. They execute inside the DO -- no separate function infrastructure, no cold starts, no network hops.

Handler registered → fn.toString() → stored in DO SQLite
Verb executed      → handler deserialized → runs in-DO → ~0ms latency

This means handlers have access to the full entity graph within the DO but cannot make external network calls. For external integrations, use WebSocket or Webhook subscription modes instead. See Events for subscription mode details.

Complete Verb Table

All custom verbs across the 35 core entities:

CRM

EntityVerbEventDescription
ContactqualifyQualifiedMove from Lead to Qualified
ContactcaptureCapturedCapture from form or import
ContactassignAssignedAssign to team member
ContactmergeMergedMerge duplicate records
ContactenrichEnrichedEnrich with external data
OrganizationenrichEnrichedEnrich with external data
OrganizationscoreScoredUpdate organization score
DealadvanceAdvancedMove to next pipeline stage
DealcloseClosedClose as won
DealloseLostClose as lost
DealreopenReopenedReopen a closed deal
ActivitylogLoggedLog an activity
ActivitycompleteCompletedMark activity complete

Projects

EntityVerbEventDescription
ProjectarchiveArchivedArchive a project
ProjectactivateActivatedActivate a project
IssueassignAssignedAssign to team member
IssuecloseClosedClose the issue
IssuereopenReopenedReopen a closed issue
CommentresolveResolvedResolve a comment thread

Content

EntityVerbEventDescription
ContentpublishPublishedPublish content
ContentarchiveArchivedArchive content
ContentscheduleScheduledSchedule for future publish
AssetprocessProcessedProcess uploaded asset

Billing

EntityVerbEventDescription
SubscriptionactivateActivatedActivate subscription
SubscriptionpausePausedPause subscription
SubscriptioncancelCanceledCancel subscription
SubscriptionrenewRenewedRenew subscription
InvoicefinalizeFinalizedFinalize for payment
InvoicevoidVoidedVoid an invoice
PaymentcaptureCapturedCapture payment
PaymentrefundRefundedIssue refund

Support

EntityVerbEventDescription
TicketassignAssignedAssign to agent
TicketescalateEscalatedEscalate priority
TicketresolveResolvedResolve the ticket
TicketreopenReopenedReopen a resolved ticket
TicketcloseClosedClose the ticket

Analytics

EntityVerbEventDescription
MetricsnapshotSnapshottedRecord a metric snapshot
FunnelactivateActivatedActivate a funnel
GoalachieveAchievedMark goal as achieved
GoalmissMissedMark goal as missed

Marketing

EntityVerbEventDescription
CampaignlaunchLaunchedLaunch a campaign
CampaignpausePausedPause a campaign
CampaigncompleteCompletedComplete a campaign
SegmentrefreshRefreshedRefresh segment membership
FormpublishPublishedPublish a form
FormsubmitSubmittedRecord form submission

Experimentation

EntityVerbEventDescription
ExperimentstartStartedStart an experiment
ExperimentstopStoppedStop an experiment
ExperimentconcludeConcludedConclude with results
FeatureFlagenableEnabledEnable a feature flag
FeatureFlagdisableDisabledDisable a feature flag
FeatureFlagrolloutRolledOutProgressive rollout

Platform

EntityVerbEventDescription
WorkflowactivateActivatedActivate a workflow
WorkflowdeactivateDeactivatedDeactivate a workflow
WorkflowtriggerTriggeredTrigger workflow execution
IntegrationconnectConnectedConnect an integration
IntegrationdisconnectDisconnectedDisconnect an integration
AgentdeployDeployedDeploy an agent
AgentpausePausedPause an agent
AgentinvokeInvokedInvoke agent execution

On this page