← All seeds
Tickets ADL v0.3.1

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

ResourceFeatures
TicketCRUD, soft delete, audit, state machine, search, field-level permissions
CommentCRUD, soft delete, belongs to Ticket (cascade delete)
AttachmentCRUD, belongs to Ticket (cascade delete), file metadata
CategoryCRUD, 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 <──────────────┘
TransitionFromToGuardsNotes
assignopenassignedauthenticatedAny logged-in user can assign
startassignedin_progressowner, admin
resolvein_progressresolvedowner, adminRequires resolution field
blockin_progressblockedowner, admin
unblockblockedin_progressadminOnly admins can unblock
closeresolvedclosedowner, adminSets closedAt to current time
reopenclosedopenauthenticatedClears closedAt, resolution, assignee

Roles & Permissions

RoleCapabilities
adminFull access to all resources, can unblock tickets, manage categories
agentCan assign, update priority, resolve, and close tickets
customerCan create tickets, add comments, reopen closed tickets

Field-Level Restrictions

  • assigneeId / assigneeName: write restricted to agent, admin
  • priority: write restricted to agent, admin
  • resolution: 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]

Download bundle