Headlessly
Support

Ticket

Customer support requests with priority, channel tracking, escalation, and satisfaction scoring.

Schema

import { Noun } from 'digital-objects'

export const Ticket = Noun('Ticket', {
  subject: 'string!',
  description: 'string',
  status: 'Open | Pending | InProgress | Resolved | Escalated | Closed | Reopened',
  priority: 'Low | Medium | High | Urgent',
  category: 'string',
  assignee: '-> Contact',
  requester: '-> Contact',
  organization: '-> Organization',
  deal: '-> Deal',
  channel: 'Email | Chat | Phone | Web | API',
  tags: 'string',
  firstResponseAt: 'datetime',
  resolvedAt: 'datetime',
  satisfaction: 'number',
  resolve: 'Resolved',
  escalate: 'Escalated',
  close: 'Closed',
  reopen: 'Reopened',
})

Fields

FieldTypeRequiredDescription
subjectstringYesShort summary of the support request
descriptionstringNoDetailed description of the issue
statusenumNoOpen, Pending, InProgress, Resolved, Escalated, Closed, or Reopened
priorityenumNoLow, Medium, High, or Urgent
categorystringNoSupport category (e.g., billing, technical, account)
assignee-> ContactNoSupport agent handling the ticket
requester-> ContactNoCustomer who submitted the request
organization-> OrganizationNoOrganization the requester belongs to
deal-> DealNoAssociated deal for sales-context support
channelenumNoEmail, Chat, Phone, Web, or API
tagsstringNoComma-separated tags for categorization
firstResponseAtdatetimeNoTimestamp of the first agent response
resolvedAtdatetimeNoTimestamp when the ticket was resolved
satisfactionnumberNoCustomer satisfaction score (1-5)

Relationships

FieldDirectionTargetDescription
assignee->ContactThe support agent working on this ticket
requester->ContactThe customer who submitted the ticket
organization->OrganizationThe customer's organization for account context
deal->DealAssociated sales deal for pre-sale or renewal support

Verbs

VerbEventDescription
createCreatedOpen a new support ticket
updateUpdatedUpdate ticket fields
deleteDeletedSoft-delete the ticket
resolveResolvedMark the ticket as resolved
escalateEscalatedEscalate to a higher support tier or engineering
closeClosedClose the ticket after resolution or inactivity
reopenReopenedReopen a closed or resolved ticket

Verb Lifecycle

import { Ticket } from '@headlessly/support'

// BEFORE hook -- validate before escalation
Ticket.escalating(ticket => {
  if (ticket.status === 'Closed') {
    throw new Error('Cannot escalate a closed ticket -- reopen it first')
  }
})

// Execute
await Ticket.escalate('ticket_nV4xKpQm')

// AFTER hook -- create an engineering issue on escalation
Ticket.escalated((ticket, $) => {
  $.Issue.create({
    title: `Escalation: ${ticket.subject}`,
    description: ticket.description,
    type: 'Bug',
    priority: ticket.priority,
    reporter: ticket.requester,
  })
  $.Activity.create({
    subject: `Ticket escalated: ${ticket.subject}`,
    type: 'Task',
    contact: ticket.assignee,
  })
})

Status State Machine

              create()
(none) ──────────────→ Open

            update()     │     escalate()
              ┌──────────┤──────────┐
              ▼          │          ▼
           Pending    InProgress  Escalated
              │          │          │
              └────┬─────┘          │
                   │     resolve()  │
                   ▼                │
               Resolved ←──────────┘
                   │          resolve()
           close() │

                Closed ←── close()
                   │          ▲
         reopen()  │          │
                   ▼          │
               Reopened ──────┘
                   │      close()
                   └──→ Open (via update)

Valid transitions:

FromVerbTo
--createOpen
OpenupdatePending
OpenupdateInProgress
OpenescalateEscalated
OpenresolveResolved
PendingresolveResolved
InProgressresolveResolved
InProgressescalateEscalated
EscalatedresolveResolved
ResolvedcloseClosed
OpencloseClosed
ClosedreopenReopened
ReopenedcloseClosed
ReopenedescalateEscalated

Cross-Domain Patterns

Ticket connects to CRM, Projects, Analytics, and Billing for full account-aware support:

  • CRM: Requesters and assignees are Contacts. Organization provides account context -- tier, health score, lifetime value -- for priority routing. Deals link support to active sales.
  • Projects: Escalated tickets create Issues for engineering follow-up. When the issue is resolved, the ticket can be auto-closed.
  • Analytics: Ticket lifecycle events feed into Metrics -- first response time, resolution time, CSAT, escalation rate.
  • Billing: Subscription status determines SLA tier. Enterprise customers route to dedicated support.
import { Ticket } from '@headlessly/support'

// Auto-prioritize tickets from enterprise customers
Ticket.creating(async (ticket, $) => {
  if (ticket.organization) {
    const org = await $.Organization.get(ticket.organization)
    if (org.tier === 'Enterprise') {
      ticket.priority = 'High'
    }
  }
})

// Track support metrics
Ticket.resolved((ticket, $) => {
  const resolutionTime = Date.now() - new Date(ticket.$createdAt).getTime()
  $.Metric.record({
    name: 'ticket.resolution_time',
    value: resolutionTime,
    dimensions: {
      priority: ticket.priority,
      channel: ticket.channel,
      category: ticket.category,
    },
  })
})

Query Examples

SDK

import { Ticket } from '@headlessly/support'

// Find all urgent open tickets
const urgent = await Ticket.find({
  priority: 'Urgent',
  status: 'Open',
})

// Get a specific ticket with context
const ticket = await Ticket.get('ticket_nV4xKpQm', {
  include: ['requester', 'assignee', 'organization', 'deal'],
})

// Create a ticket from an API integration
await Ticket.create({
  subject: 'API rate limit exceeded',
  description: 'Receiving 429 errors on the /api/contacts endpoint since 2pm.',
  priority: 'High',
  channel: 'API',
  requester: 'contact_fX9bL5nRd',
  organization: 'org_Nw8rTxJv',
  category: 'technical',
})

// Resolve and close
await Ticket.resolve('ticket_nV4xKpQm')
await Ticket.close('ticket_nV4xKpQm')

MCP

headless.ly/mcp#search
{
  "type": "Ticket",
  "filter": { "status": "Open", "priority": "Urgent" },
  "sort": { "$createdAt": "asc" },
  "limit": 50
}
headless.ly/mcp#fetch
{ "type": "Ticket", "id": "ticket_nV4xKpQm", "include": ["requester", "organization"] }
headless.ly/mcp#do
const open = await $.Ticket.find({ status: 'Open' })
await $.Ticket.escalate('ticket_nV4xKpQm')
await $.Ticket.resolve('ticket_nV4xKpQm')

REST

# List open urgent tickets
curl https://headless.ly/~acme/tickets?status=Open&priority=Urgent

# Get a specific ticket
curl https://headless.ly/~acme/tickets/ticket_nV4xKpQm

# Create a ticket
curl -X POST https://headless.ly/~acme/tickets \
  -H 'Content-Type: application/json' \
  -d '{"subject": "API rate limit exceeded", "priority": "High", "channel": "API", "requester": "contact_fX9bL5nRd"}'

# Escalate a ticket
curl -X POST https://headless.ly/~acme/tickets/ticket_nV4xKpQm/escalate

# Resolve a ticket
curl -X POST https://headless.ly/~acme/tickets/ticket_nV4xKpQm/resolve

# Close a ticket
curl -X POST https://headless.ly/~acme/tickets/ticket_nV4xKpQm/close

On this page