Support Tickets
A helpdesk with a full ticket workflow state machine, threaded comments, file attachments, and role-based access for agents and customers.
- Resources
- Ticket, Comment, Attachment, Category
- Roles
- admin, agent, customer
- Features (5)
- soft_deleteauditstate_machinepermissionsrequired_fields_on_transition
A support ticket system with a full workflow state machine, role-based access, and file attachments.
Resources
| Resource | Features |
|---|---|
| Ticket | CRUD, soft delete, audit, state machine, search, field-level permissions |
| Comment | CRUD, soft delete, belongs to Ticket (cascade delete) |
| Attachment | CRUD, belongs to Ticket (cascade delete), file metadata |
| Category | CRUD, admin-only management |
Relationships
- Ticket hasMany Comment (cascade delete)
- Ticket hasMany Attachment (cascade delete)
- Comment belongsTo Ticket
- Attachment belongsTo Ticket
- Ticket belongsTo Category
State Machine
open ──assign──> assigned ──start──> in_progress ──resolve──> resolved ──close──> closed
│ │
├──block──> blocked ──unblock──┘ │
│ │
└────────────────── reopen <──────────────┘
| Transition | From | To | Guards | Notes |
|---|---|---|---|---|
| assign | open | assigned | authenticated | Any logged-in user can assign |
| start | assigned | in_progress | owner, admin | |
| resolve | in_progress | resolved | owner, admin | Requires resolution field |
| block | in_progress | blocked | owner, admin | |
| unblock | blocked | in_progress | admin | Only admins can unblock |
| close | resolved | closed | owner, admin | Sets closedAt to current time |
| reopen | closed | open | authenticated | Clears closedAt, resolution, assignee |
Roles & Permissions
| Role | Capabilities |
|---|---|
| admin | Full access to all resources, can unblock tickets, manage categories |
| agent | Can assign, update priority, resolve, and close tickets |
| customer | Can create tickets, add comments, reopen closed tickets |
Field-Level Restrictions
assigneeId/assigneeName: write restricted to agent, adminpriority: write restricted to agent, adminresolution: write restricted to agent, admin
Seed Data
4 categories (Account, Billing, Technical, Feature Request), 5 tickets across various states (2 open, 1 in_progress, 1 resolved, 1 closed), users: admin, sarah (agent), mike (agent), alice (customer), bob (customer).
Domain definition
View raw (472 lines)Show domain.yaml
# pocs/poc-02-tickets/adl-tickets.yaml
#
# Support Ticket System — ADL Domain Definition
# ADL version: 0.3.1
#
# Resources: Ticket, Comment, Attachment, Category
# Relations: Ticket hasMany Comment (cascade delete)
# Ticket hasMany Attachment (cascade delete)
# Comment belongsTo Ticket
# Attachment belongsTo Ticket
# Ticket belongsTo Category
#
# ADL features exercised:
# Phase 1 — CRUD generation, REST route mapping, RBAC permissions
# Phase 6 — hasMany, belongsTo, ?include=
# Phase 7 — filter, sort, paginate, search, count
# Phase 8 — Rich types: markdown, text, datetime, uuid
# Phase 9 — $merge (Timestamp, SoftDelete objects), $ref enums
# Phase 10 — UUID primary keys, custom indexes on FK fields
# Phase 11 — Full state machine: transitions, guards, requiredFields, sets
# Phase 12 — Soft delete on Ticket + Comment, audit trail on Ticket
# Phase 14 — Field-level permissions (assigneeId, priority, resolution)
#
# ─── FIX LOG ─────────────────────────────────────────────────────
#
# 2026-03-07 (v1): Converted stateMachine from flat transitions to
# states-based format. Fixed parser compatibility.
#
# 2026-03-07 (v2): Added requiredFields and sets to transitions.
# - resolve: requiredFields: [resolution]
# - close: sets: { closedAt: now() }
# - reopen: sets: { closedAt: null, resolution: null, ... }
# These require runtime patches in action_sm.cpp and adl_parser.cpp.
# ─────────────────────────────────────────────────────────────────
domain:
name: tickets
version: "1.0.0"
description: >
Support ticket system demonstrating the full ADL v0.3.1 feature set
including state machines with named transitions, guards, and on_enter
hooks. Tickets progress through a defined lifecycle with role-based
access controls at the operation, field, and transition level.
api: {}
# ─── Shared objects ────────────────────────────────────────────
objects:
Timestamp:
createdAt: { type: datetime, autoNow: create }
updatedAt: { type: datetime, autoNow: update }
SoftDelete:
deletedAt: { type: datetime }
# ─── Shared enums ──────────────────────────────────────────────
enums:
TicketStatus:
values:
open:
label: "Open"
description: "Newly created, awaiting assignment"
color: "#3b82f6"
assigned:
label: "Assigned"
description: "Assigned to an agent, not yet started"
color: "#8b5cf6"
in_progress:
label: "In Progress"
description: "Agent is actively working on this ticket"
color: "#f59e0b"
blocked:
label: "Blocked"
description: "Work is blocked by an external dependency"
color: "#ef4444"
resolved:
label: "Resolved"
description: "Solution provided, awaiting confirmation"
color: "#10b981"
closed:
label: "Closed"
description: "Ticket is closed and complete"
color: "#64748b"
final: true
TicketPriority:
values:
low:
label: "Low"
description: "Minor issue, no urgency"
color: "#94a3b8"
medium:
label: "Medium"
description: "Standard priority"
color: "#3b82f6"
high:
label: "High"
description: "Important issue requiring prompt attention"
color: "#f59e0b"
critical:
label: "Critical"
description: "Urgent issue requiring immediate attention"
color: "#ef4444"
# ─── Resources ─────────────────────────────────────────────────
resources:
# ╔══════════════════════════════════════════════════════════════╗
# ║ Category ║
# ╚══════════════════════════════════════════════════════════════╝
Category:
description: >
Admin-managed ticket categories for organizing support requests.
object:
id:
type: uuid
primaryKey: true
name:
type: string
required: true
unique: true
minLength: 2
maxLength: 100
description:
type: text
maxLength: 500
$merge:
- { $ref: '#/objects/Timestamp' }
permissions:
list: ["*"]
read: ["*"]
create: [admin]
update: [admin]
delete: [admin]
# ╔══════════════════════════════════════════════════════════════╗
# ║ Ticket ║
# ╚══════════════════════════════════════════════════════════════╝
Ticket:
description: >
A support ticket submitted by a customer. Tickets progress through
a state machine lifecycle (open -> assigned -> in_progress ->
resolved -> closed) with guards enforcing role and field
requirements on each transition. Soft-deleted tickets are hidden
from the default list scope but remain in the database. All
changes are tracked in the audit log.
object:
id:
type: uuid
primaryKey: true
title:
type: string
required: true
minLength: 5
maxLength: 200
description:
type: markdown
required: true
status:
$ref: '#/enums/TicketStatus'
default: open
priority:
$ref: '#/enums/TicketPriority'
default: medium
categoryId:
type: uuid
# Reporter is stored as username string for POC simplicity.
reporterId:
type: string
required: true
immutable: true
maxLength: 100
reporterName:
type: string
maxLength: 200
# Assignee — set during the assign transition via request body merge
assigneeId:
type: string
maxLength: 100
assigneeName:
type: string
maxLength: 200
resolution:
type: text
description: "Required for the resolve transition (via requiredFields)"
dueAt:
type: datetime
closedAt:
type: datetime
$merge:
- { $ref: '#/objects/Timestamp' }
- { $ref: '#/objects/SoftDelete' }
relationships:
comments:
type: hasMany
target: Comment
foreignKey: ticketId
onDelete: cascade
attachments:
type: hasMany
target: Attachment
foreignKey: ticketId
onDelete: cascade
category:
type: belongsTo
target: Category
foreignKey: categoryId
indexes:
- fields: [status]
description: "Filter tickets by status"
- fields: [priority]
description: "Filter tickets by priority"
- fields: [assigneeId]
description: "List tickets by assignee"
- fields: [reporterId]
description: "List tickets by reporter"
- fields: [categoryId]
description: "List tickets by category"
- fields: [status, priority]
description: "Composite: filter by status and priority"
- fields: [status, assigneeId]
description: "Composite: agent's tickets by status"
features:
softDelete: true
audit: true
search: true
audit:
track: [title, status, priority, assigneeId, resolution, closedAt]
exclude: [updatedAt, deletedAt]
validation:
dueDateFuture:
rule: "dueAt == null || dueAt > now()"
message: "Due date must be in the future"
when: create
# ─── State machine ──────────────────────────────────────────
#
# States-based format (required by adl_parser.cpp).
# Each state defines its outbound transitions.
#
# Features used:
# guards: Role-based access control on transitions
# requiredFields: Fields that must be non-empty in request or record
# sets: Declarative field mutations on transition
# "now()" -> ISO timestamp, null -> clear field
#
# NOTE: closed does NOT have final: true at the state level
# because the executor rejects ALL transitions from final states,
# which would block reopen. The enum's final: true is metadata only.
#
stateMachine:
field: status
initial: open
states:
open:
transitions:
assign:
to: assigned
guards: [authenticated]
assigned:
transitions:
start:
to: in_progress
guards: [owner, admin]
in_progress:
transitions:
resolve:
to: resolved
guards: [owner, admin]
requiredFields: [resolution]
block:
to: blocked
guards: [owner, admin]
blocked:
transitions:
unblock:
to: in_progress
guards: [admin]
resolved:
transitions:
close:
to: closed
guards: [owner, admin]
sets:
closedAt: now()
closed:
transitions:
reopen:
to: open
guards: [authenticated]
sets:
closedAt: null
resolution: null
assigneeId: null
assigneeName: null
# ─── Permissions ─────────────────────────────────────────────
permissions:
list: ["*"]
read: ["*"]
create: [authenticated]
update: [owner, agent, admin]
delete: [admin]
fields:
assigneeId:
read: ["*"]
write: [agent, admin]
assigneeName:
read: ["*"]
write: [agent, admin]
priority:
read: ["*"]
write: [agent, admin]
resolution:
read: ["*"]
write: [agent, admin]
# ╔══════════════════════════════════════════════════════════════╗
# ║ Comment ║
# ╚══════════════════════════════════════════════════════════════╝
Comment:
description: >
A comment on a support ticket. Any authenticated user can create
comments. Comments are soft-deleted to preserve discussion context.
object:
id:
type: uuid
primaryKey: true
body:
type: text
required: true
minLength: 1
maxLength: 5000
ticketId:
type: uuid
required: true
immutable: true
authorId:
type: string
required: true
immutable: true
maxLength: 100
authorName:
type: string
maxLength: 200
$merge:
- { $ref: '#/objects/Timestamp' }
- { $ref: '#/objects/SoftDelete' }
relationships:
ticket:
type: belongsTo
target: Ticket
foreignKey: ticketId
onDelete: cascade
indexes:
- fields: [ticketId]
description: "List all comments for a ticket"
- fields: [authorId]
description: "List all comments by an author"
features:
softDelete: true
permissions:
list: ["*"]
read: ["*"]
create: [authenticated]
update: [owner, admin]
delete: [owner, admin]
# ╔══════════════════════════════════════════════════════════════╗
# ║ Attachment ║
# ╚══════════════════════════════════════════════════════════════╝
Attachment:
description: >
A file attachment on a support ticket. Exercises the ADL file
type for binary uploads.
object:
id:
type: uuid
primaryKey: true
filename:
type: string
required: true
maxLength: 255
mimeType:
type: string
maxLength: 100
sizeBytes:
type: integer
url:
type: string
maxLength: 1024
description: "URL or path to the stored file"
ticketId:
type: uuid
required: true
immutable: true
uploadedBy:
type: string
required: true
immutable: true
maxLength: 100
$merge:
- { $ref: '#/objects/Timestamp' }
relationships:
ticket:
type: belongsTo
target: Ticket
foreignKey: ticketId
onDelete: cascade
indexes:
- fields: [ticketId]
description: "List all attachments for a ticket"
permissions:
list: [authenticated]
read: [authenticated]
create: [authenticated]
update: [owner, admin]
delete: [owner, admin]