Headlessly
Concepts

Verbs

Every action has a full lifecycle — execute, validate, react, audit.

Every verb has a full conjugation:

qualify
  ├── qualify()      → execute (imperative)
  ├── qualifying()   → BEFORE hook (gerund)
  ├── qualified()    → AFTER hook (past tense)
  └── qualifiedBy    → audit (reverse)

CRUD Is Default

Every Noun gets create, update, delete with full conjugation:

import { Contact } from '@headlessly/crm'

Contact.creating(contact => { /* validate */ })
await Contact.create({ name: 'Alice', stage: 'Lead' })
Contact.created(contact => { /* react */ })

Custom Verbs

Declared as properties on the Noun:

export const Contact = Noun('Contact', {
  qualify: 'Qualified',
  capture: 'Captured',
})

BEFORE Hooks

Validate or reject before an action executes:

import { Contact } from '@headlessly/crm'

Contact.qualifying(contact => {
  if (!contact.email) throw new Error('Cannot qualify without email')
  return contact
})

AFTER Hooks

React to what happened:

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

Contact.qualified((contact, $) => {
  $.Issue.create({
    type: 'Task',
    subject: `Follow up with ${contact.name}`,
    contact: contact.$id,
    source: 'crm',
  })
})

Deal.closed((deal, $) => {
  $.Subscription.create({ plan: 'pro', contact: deal.contact })
  $.Contact.update(deal.contact, { stage: 'Customer' })
})

Same Verb, All Interfaces

SDK
import { Contact } from '@headlessly/crm'

await Contact.qualify({ id: 'contact_uLoSfycy' })
headless.ly/mcp#do
await $.Contact.qualify({ id: 'contact_uLoSfycy' })
CLI
headlessly do Contact.qualify contact_uLoSfycy
REST
POST /~my-startup/Contact/contact_uLoSfycy/qualify

Code-as-Data

Handlers are serialized via fn.toString() and stored in the tenant's database. They execute inside the Durable Object — no separate infrastructure.

ModeLatencyUse Case
Code-as-data~0msDefault — runs inside the DO
WebSocket~10msReal-time streaming
Webhook~100msExternal integrations

On this page