Run your workspace
Tickets
Tickets are durable support records. Conversations expire after 24 hours; tickets persist forever. Use them for issues that need a human follow-up beyond the live chat window โ billing disputes, bug reports, account problems, feature complaints. Each ticket has a thread (replies to the customer and internal notes), an assignee, an activity timeline, and can be escalated or merged.
How tickets get created
-
The bot opens one mid-chat via the
open_ticketLLM tool. Auto-enabled for agents whose vertical ishelp_center(theHelpCenterPresetships theticketingcapability the tool requires). Other verticals can opt in by addingticketingto the agent'svertical_overrides.capabilities. The ticket is linked to the conversation and its timeline starts with Opened by the AI assistant during a chat. -
New ticket on
/app/ticketsโ subject, description, priority and an optional customer name + email. Replies on the ticket are emailed to that address. - Create ticket on a conversation โ the button in the conversation header opens the same form prefilled from the chat: the visitor's first question as the subject, the page URL plus the last 30 visitor / assistant / operator messages as the description (internal notes are left out), and the captured lead's name and email. The ticket is linked to the conversation and its agent.
Members whose role has the Manage tickets permission (built-in: Owner, Admin, Editor) can create and work tickets; every member can read them (see Members & roles). Every endpoint is workspace-scoped: a ticket, conversation or member from another workspace is never found, assigned, linked or merged.
Ticket lifecycle
Status moves through four values backed by constants on
App\Models\Ticket:
| Status | Meaning |
|---|---|
open | Created, not yet picked up. Default on insert. |
pending | Being worked. |
resolved | Done. resolved_at is stamped; moving the ticket back to open / pending clears it. |
closed | Archived. Merged tickets end up here too. |
Priority is one of low, normal, high, urgent โ set at create time, changed by the team, and forced to urgent by an escalation.
Replies and internal notes
The ticket page has a composer with two modes:
-
Reply โ written to the customer. When an email
address is known the reply is emailed with the queued
TicketReplyMail(subject Re: <ticket subject>, in the visitor's language when the linked conversation's language ships with PoriFyx Chat). The address comes from the Customer email typed on the ticket, else from the lead captured in the linked conversation(s). With no address the page says No customer email on file and the reply is still saved on the ticket โ it is just not emailed. A mail failure never loses the reply either. - Internal note โ team-only context, never emailed and never shown to the customer.
The email's Reply-To is the teammate who wrote the reply. Inbound email is not threaded back into the ticket, so a customer answering the email reaches that teammate's inbox โ not the ticket, and not the platform's sender address.
Assigning
The assignee picker lists the workspace's accepted members whose role
has Manage tickets โ Owners, Admins and Editors among the
built-in roles, plus any custom role that grants it. The server
enforces the same rule โ any other user id (someone outside the
workspace, a role without the permission, an invite that was never
accepted) is rejected. The new assignee gets a
TicketAssignedNotification (database + email) unless
they assigned the ticket to themselves.
Escalation
Escalate sets the priority to urgent,
stamps escalated_at, assigns the ticket to the
manager you pick (the workspace owner by default) and
sends them a TicketEscalatedNotification (database +
email). A manager is a member whose role can manage both tickets and
members โ Owner and Admin among the built-in roles, or a custom role
with both permissions.
Automatic escalation is a per-workspace rule, off by
default. A member with the Manage workspace settings
permission (built-in: Owner, Admin) turns it on from the
Auto-escalation button on /app/tickets and sets
the window (1โ720 hours, stored as
workspaces.settings.ticket_auto_escalate_hours). The
scheduled php artisan tickets:auto-escalate (every 15
minutes) escalates every open or pending ticket that is older than the
window, has no staff reply (internal notes don't count) and
was never escalated, to the workspace owner. It is idempotent: an
escalated ticket keeps its escalated_at and is never
picked again.
Merging duplicates
Merge on a ticket picks a target ticket of the same
workspace (search by subject or by the short #reference โ
the last eight characters of the ticket id). On merge:
- The source's replies and notes move to the target.
- The source's original request is added to the target as a note, dated when it was written.
- The source's conversation is recorded on the target (
metadata.linked_conversation_ids), so its lead email can receive replies; a customer email typed on the source carries over when the target has none. - The source is closed and marked
metadata.merged_into_ticket_id; it becomes read-only and links to the target. - Both tickets get a
mergedtimeline event.
Merging a ticket into itself, into another workspace's ticket, or into / out of an already-merged ticket is refused.
Activity timeline
Every change is appended to ticket_events and shown on the
ticket page: created, reply (with the address
it was emailed to, if any), note, assigned,
escalated (manual or automatic), merged,
status and priority. Names are snapshotted
into the event, so the history reads the same after someone leaves
the workspace.
Admin surface
GET /app/ticketsโ list, filter by status (open / pending / resolved / closed / all).POST /app/ticketsโ create (subject, body, priority, customer_name, customer_email, optional conversation_id).GET /app/tickets/{ticket}โ thread, timeline, assign / escalate / merge actions.PATCH /app/tickets/{ticket}โ status, priority, assignee.POST /app/tickets/{ticket}/messagesโ reply or internal note (type=reply|note).POST /app/tickets/{ticket}/escalateโ optionaltarget_user_id(a manager).GET /app/tickets/{ticket}/merge-candidates?q=โ JSON list for the merge picker.POST /app/tickets/{ticket}/mergeโtarget_ticket_id.PATCH /app/tickets/settingsโauto_escalate_after_hours(null turns the rule off). Needs Manage workspace settings.
Schema
The tickets table (migration 2026_05_13_092105_create_tickets_table.php):
workspace_id,agent_id,conversation_id(nullable โ tickets opened from scratch aren't tied to a chat).subject(โค 200 chars),body.status(open / pending / resolved / closed),priority(low / normal / high / urgent).assigned_to_user_idโ nullable.metadataJSON โsource(open_ticket_tool,manualorconversation),customer_name,customer_email,linked_conversation_ids,merged_ticket_ids,merged_into_ticket_id.resolved_at, andescalated_at(migration2026_10_02_082044_add_escalated_at_to_tickets_table.php).- Timestamps.
ticket_messages โ the thread: ticket_id, workspace_id, author_user_id (nullable), type (reply | note), body, metadata (emailed_to, or merged_from_ticket_id / merged_from_subject), timestamps.
ticket_events โ the timeline: ticket_id, workspace_id, actor_user_id (null for the AI tool and the automatic rule), type, data JSON, created_at.
How the open_ticket tool works
See Tools & rich messages for the registry shape. The tool's input schema:
subjectโ required, โค 120 chars.bodyโ required. The LLM is prompted to include the visitor's account / order references, what they tried, and what went wrong.priorityโ optional, defaults tonormal.
On invoke the tool inserts a Ticket row tied to the
current conversation, records its created timeline event,
and returns the ticket id the LLM weaves into its final reply.
Nobody is notified when the AI opens a ticket โ it appears on
/app/tickets; turn on automatic escalation if unanswered
tickets must reach someone by email.