GitHub
Concept

Tickets

Customer-support tickets with messages, internal notes, attachments, tags, SLA tracking, canned responses, and a full audit trail — separate from internal-engineering issues.

Tickets are ProxifAI’s customer-support workspace. They’re a separate primitive from issues — issues track internal engineering work, tickets track external requests from customers (or anyone outside the team). Both share the same RBAC, audit, and @-mention plumbing, but tickets carry a richer surface around messaging, SLAs, and channel intake.

Anatomy of a ticket

FieldNotes
ticketNumberPer-org auto-incrementing integer (#42); shown in the UI
subjectShort headline, like an email subject
descriptionLong-form description; renders as Markdown
statusOne of 5 values (see Status)
prioritylow / medium / high / urgent
categorygeneral / bug / feature / billing / account / technical
sourceWhere the ticket came from — web / email / portal / api
teamIdOptional — scope the ticket to one team
assigneeIdPerson responsible for the next response
creatorIdInternal author, if created from inside (otherwise the contact info applies)
contactEmail, contactNameExternal requester — the user the ticket is for (not necessarily a ProxifAI user)
slaDeadline, firstResponseAt, resolvedAt, closedAtSLA-tracking timestamps (see SLA)
tags[]Many-to-many via TicketTag
messageCountCached count of TicketMessage rows

Status

Five values from TicketStatus. The flow generally moves left-to-right but can revisit pending or on_hold at any time:

StatusMeaning
openNew; no agent has worked it yet
pendingAwaiting customer reply — firstResponseAt is set, but they need to respond before we can continue
on_holdWaiting on something internal (a fix, an external dep, a decision) — pauses SLA accrual
resolvedAgent considers it done; sets resolvedAt. Customer has a window to push back
closedFinal state; sets closedAt. Reopening creates a follow-up ticket

Status changes are logged as TicketActivity entries.

Messages and internal notes

A ticket is a thread. Every reply is a TicketMessage row with one bit that changes everything:

  • Public message (isInternalNote: false) — visible to the contact, included in any email/portal response
  • Internal note (isInternalNote: true) — only visible to org members, used for “FYI: this customer also reported X last month” or “@alice can you look at this?”

Internal notes still mention-trigger agents, so you can write @triage-agent please categorize this inside an internal note and the agent runs without the customer ever seeing it. Both message types support @-mentions, file attachments via TicketAttachment, and code blocks.

# Public reply — goes to the customer
pfai api POST /api/v1/tickets/{id}/messages \
  -d '{"content": "Hi! We just shipped a fix in v1.4.3 — please try again.", "isInternalNote": false}'

# Internal note — team-only
pfai api POST /api/v1/tickets/{id}/messages \
  -d '{"content": "@bob this is the third report this week — file an issue?", "isInternalNote": true}'

Activity timeline

Every change appends a TicketActivity row. Nine activity types tracked (models.go):

created · status_changed · priority_changed · assigned
category_changed · tag_added · tag_removed
message_added · note_added

Activities carry oldValue / newValue so the UI can render diffs (“priority mediumurgent”) without needing to reconstruct from older rows.

Tags

TicketTag is a per-team taxonomy: a name + a color. Useful for slicing the ticket list by ad-hoc dimensions the built-in category enum can’t capture — e.g. regression, enterprise-customer, tracking-1.5-release. Tags are managed independently of tickets; create them once, reuse forever.

pfai api POST /api/v1/ticket-tags -d '{"name": "regression", "color": "#ef4444"}'

SLA policies

SLAPolicy rows define response/resolution targets per priority (and optionally per team):

FieldNotes
priorityWhich TicketPriority this policy applies to
firstResponseMinutesTarget time to first response
resolutionMinutesTarget time to resolved
teamIdOptional — scope policy to a single team’s tickets

When a ticket is created (or its priority changes), the matching policy stamps slaDeadline. The first message a non-creator posts sets firstResponseAt; SLA breach badges appear in the UI when now > slaDeadline and the ticket isn’t yet resolved.

Canned responses

CannedResponse is a saved snippet of message content (title + body) for the replies your team writes 50 times a week. Insert one from the message composer; canned responses can be team-scoped (just support’s) or org-wide.

CLI

pfai ticket is a thin wrapper over the REST surface:

pfai ticket list                                # list tickets in the current org
pfai ticket list --status open --priority high  # filter
pfai ticket view <id>                           # full ticket including thread
pfai ticket create --subject "Login broken" --priority high --category technical
pfai ticket update <id> --status resolved --assignee alice

Subcommands inherit the standard kubectl-style verbs (ls/list, view/get, delete/rm) and the global --output/--json/--query flags.

REST endpoints

Method · PathPurpose
GET /api/v1/ticketsList with filters (status, priority, category, assigneeId, teamId, tag)
POST /api/v1/ticketsCreate — minimum is {subject}, everything else has defaults
GET /api/v1/tickets/statsAggregate counts (per-status, per-priority, SLA-breach count) for dashboards
GET /api/v1/tickets/{id}Read with embedded assignee, creator, tags
PATCH /api/v1/tickets/{id}Update fields
DELETE /api/v1/tickets/{id}Soft-delete
POST /api/v1/tickets/{id}/assignReassign — separate endpoint for proper activity logging
GET /api/v1/tickets/{id}/messagesPaginated thread
POST /api/v1/tickets/{id}/messagesPost a message; isInternalNote toggles visibility
PATCH /api/v1/tickets/{id}/messages/{msgId}Edit your own message
DELETE /api/v1/tickets/{id}/messages/{msgId}Delete your own message
GET /api/v1/tickets/{id}/activitiesAudit log
* /api/v1/ticket-tagsTag CRUD
* /api/v1/canned-responsesCanned-response CRUD
* /api/v1/sla-policiesSLA-policy CRUD

How tickets relate to issues

QuestionAnswer
Is a ticket an issue?No — separate table, separate model, separate UI route.
Can a ticket spawn an issue?Yes — convert “this is a real bug we need to fix” via the Convert to issue action, which creates an Issue linked back to the ticket via the issue’s metadata.
Do they share assignees, mentions, audit?Yes — same User table, same @-mention parser, same RBAC permission checks.
Do they share status enums?No — IssueStatus (6) and TicketStatus (5) are independent.
Where do they show up in the rail?Issues under Plan; tickets under their own UI tab (or surfaced in the customer-facing portal at /portal).

The split is intentional: issues are unbounded engineering work, tickets are bounded support requests with response targets. Mixing them dilutes both — you don’t want a backlog grooming session blocked because somebody filed an SLA-driven ticket overnight.

See also