The helpdesk.* family (4 verbs). Every verb enters through the one front door: it is capability-checked, permission-checked, scope-checked and audited before it runs (see Security & permissions). Each entry below lists its capability, risk class (which fixes the minimum autonomy level), the minimum caller permission, its availability tier, whether it is idempotent, and any outbound egress.
Createhelpdesk.cannedResponse.create
| Property | Value |
|---|---|
| Capability | helpdesk.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Admin |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
A plain insert; created_by_id is the acting user’s FRONTEND users.id (mapped via the actor link) - REQUIRED (NOT NULL), so a SYSTEM/service actor with no linked user is refused (BadInput). No unique constraint on shortcut today, so no 23505 handling.
Input { title, content, category?, shortcut? } (fields ending in ? are optional)
Call POST /v/helpdesk.cannedResponse.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool helpdesk.cannedResponse.create.
Createhelpdesk.ticket.create
| Property | Value |
|---|---|
| Capability | helpdesk.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
Opens a ticket in the caller’s workspace. The kernel OWNS the per-workspace ticket_number counter: a tx-scoped advisory lock (salt 42, distinct from the audit-chain lock) serialises the COALESCE(MAX(ticket_number),0)+1 mint so two concurrent opens cannot collide (replacing the frontend withTicketNumberRetry). matterId/teamId/assigneeId are each fenced within the workspace (BadInput on miss).
Input { subject, description, priority?, category?, clientEmail, clientName, matterId?, teamId?, assigneeId? } (fields ending in ? are optional)
Call POST /v/helpdesk.ticket.create with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool helpdesk.ticket.create.
Replyhelpdesk.ticket.reply
| Property | Value |
|---|---|
| Capability | helpdesk.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Member |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
The single-writer collapse of the old prisma.$transaction: ticket_messages.create + support_tickets.update on ONE ctx.tx. (1) resolve
- FOR UPDATE the ticket in the workspace (BadInput on miss); (2) REFUSE a CLOSED ticket (Conflict, the route’s old 422); (3) INSERT the AGENT message (author_id mapped from the acting user, nullable); (4) transition: status -> AWAITING_CLIENT unless internal, and first_reply_at stamped once (COALESCE).
Input { ticketId, content, isInternal?, authorName?, authorEmail? } (fields ending in ? are optional)
Call POST /v/helpdesk.ticket.reply with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool helpdesk.ticket.reply.
Updatehelpdesk.ticket.update
| Property | Value |
|---|---|
| Capability | helpdesk.write |
| Risk class | Write - autonomy L1 |
| Min. permission | Admin |
| Availability | On demand (MCP tier 2) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
A partial PATCH with present-vs-null discrimination on the four nullable FKs (an omitted field is left unchanged; an explicit JSON null un-sets it). resolved_at/closed_at are derived off the FOR UPDATE current-status read (transition INTO RESOLVED/CLOSED stamps the timestamp once). Non-null cross-refs (assignee/team/matter) are fenced within the workspace.
Input { ticketId, status?, priority?, category?|null, assigneeId?|null, teamId?|null, matterId?|null } (fields ending in ? are optional)
Call POST /v/helpdesk.ticket.update with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool helpdesk.ticket.update.