P PoriFyx - Create AI-powered chat docs

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

  1. The bot opens one mid-chat via the open_ticket LLM tool. Auto-enabled for agents whose vertical is help_center (the HelpCenterPreset ships the ticketing capability the tool requires). Other verticals can opt in by adding ticketing to the agent's vertical_overrides.capabilities. The ticket is linked to the conversation and its timeline starts with Opened by the AI assistant during a chat.
  2. New ticket on /app/tickets โ€” subject, description, priority and an optional customer name + email. Replies on the ticket are emailed to that address.
  3. 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:

StatusMeaning
openCreated, not yet picked up. Default on insert.
pendingBeing worked.
resolvedDone. resolved_at is stamped; moving the ticket back to open / pending clears it.
closedArchived. 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 merged timeline 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 โ€” optional target_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.
  • metadata JSON โ€” source (open_ticket_tool, manual or conversation), customer_name, customer_email, linked_conversation_ids, merged_ticket_ids, merged_into_ticket_id.
  • resolved_at, and escalated_at (migration 2026_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 to normal.

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.