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: 'Prospecting | Qualification | Proposal | Negotiation | Closed | Won | 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:

Identity

EntityVerbEventDescription
UserinviteInvitedSend an invite
UsersuspendSuspendedSuspend a user
UseractivateActivatedActivate a user
ApiKeyrevokeRevokedRevoke an API key

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
LeadconvertConvertedConvert lead to a deal
LeadloseLostMark lead as lost
DealwinWonMark deal as won
ActivitylogLoggedLog an activity
ActivitycompleteCompletedMark activity complete
ActivitycancelCancelledCancel an activity

Projects

EntityVerbEventDescription
ProjectarchiveArchivedArchive a project
ProjectcompleteCompletedComplete 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
SubscriptioncancelCancelledCancel subscription
SubscriptionreactivateReactivatedRestart a cancelled subscription
SubscriptionupgradeUpgradedMove to a higher-value plan
SubscriptiondowngradeDowngradedMove to a lower-value plan
SubscriptionrenewRenewedRenew subscription
InvoicepayPaidMark invoice as paid
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
MetricrecordRecordedRecord a metric value
MetricresetResetReset a metric
FunnelanalyzeAnalyzedAnalyze funnel data
GoalachieveAchievedMark goal as achieved
GoalcompleteCompletedComplete a goal
GoalmissMissedMark goal as missed
GoalresetResetReset goal progress

Marketing

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

Experimentation

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

Platform

EntityVerbEventDescription
WorkflowactivateActivatedActivate a workflow
WorkflowpausePausedPause a workflow
WorkflowtriggerTriggeredTrigger workflow execution
WorkflowarchiveArchivedArchive a workflow
IntegrationconnectConnectedConnect an integration
IntegrationdisconnectDisconnectedDisconnect an integration
IntegrationsyncSyncedSync integration data
AgentdoDoneExecute an action
AgentaskAskedAsk for information
AgentdecideDecidedMake a decision
AgentapproveApprovedApprove an action
AgentnotifyNotifiedSend a notification
AgentdelegateDelegatedDelegate to another agent
AgentescalateEscalatedEscalate to a human
AgentlearnLearnedLearn from interaction
AgentreflectReflectedReflect on outcomes
AgentinvokeInvokedInvoke agent execution
AgentdeployDeployedDeploy an agent
AgentpausePausedPause an agent
AgentstopStoppedStop an agent
AgentretireRetiredRetire an agent

Communication

EntityVerbEventDescription
MessagesendSentSend a message
MessagedeliverDeliveredMark as delivered
MessagereadReadMark as read

On this page