---
title: "Replies and internal notes — Tickets for Laravel | Roundly"
description: "Public replies and agent-only notes on a comments-for-laravel thread — author roles, reply automations, first response, moderation, mentions and edits."
url: https://roundly-consulting.com/open-source/docs/tickets-for-laravel/conversation
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Tickets for Laravel](https://roundly-consulting.com/open-source/docs/tickets-for-laravel.md)

# Replies and internal notes

A ticket’s conversation is a comments-for-laravel thread on the ticket. Tickets writes every message through comments and adds a namespaced meta block — your own meta keys are kept next to it, and a host key can never spoof the tickets namespace:

```
comment.meta.tickets = { internal: bool, role: 'agent'|'requester'|'participant'|'system', channel: string }
```

Messages are root comments (no threaded replies): comments hides everything under an invisible comment from the public thread, so a public reply under a note would vanish.

## Replies and notes

```php
// A public reply — with an optional transition applied in the same transaction
$comment = Tickets::for($ticket)->by($agent)->reply('A technician is on the way.', transition: 'wait');
Tickets::for($ticket)->by($customer)->reply('Here is a photo.', attachments: [$request->file('photo')], channel: 'web');

// An internal note — agents only, never shown to the requester
Tickets::for($ticket)->by($agent)->note('Roller worn out, order a spare.');

// Model conveniences, through the manager
$ticket->reply('Thanks!', $customer);
$ticket->note('Escalated to vendor.', $agent);

// Edit and delete (scoped to the ticket; each left in the timeline)
Tickets::for($ticket)->by($agent)->editComment($comment, 'A technician is on the way (ETA 2 pm).');
Tickets::for($ticket)->by($agent)->deleteComment($comment);

// Read
Tickets::for($ticket)->thread();                 // public: published replies, oldest first
Tickets::for($ticket)->thread(internal: true);   // every message: notes and held replies too
```

reply() takes body, transition, attachments, channel, reason and payload (for the transition) and meta; note() takes body, attachments, channel and meta. Both return the comments Comment. The channel must be one of tickets.channels (default tickets.default\_channel).

| Aspect | Public reply | Internal note |
| --- | --- | --- |
| comments visible | `true` | false — comments’ own public reads never show it |
| `meta.tickets.internal` | `false` | true — defence in depth: a comment marked internal is treated as internal even if a host made it visible |
| who | requester, agent, participant, system | agents and the system |
| activity | replied (public) | note\_added (internal; the comment id, never the body) |
| event | TicketReplied (with the role) | TicketNoteAdded |
| notification | replied / requester\_replied | note\_added (agents only) |

## Edits and deletes

editComment() and deleteComment() act as the handle’s actor and record a comment\_edited / comment\_deleted activity entry — the comment id and the role of the message’s author, never a body — as visible as the message: public for a published public reply, internal for a note or a held reply. TicketCommentUpdated / TicketCommentDeleted fire after the commit. Who may edit or delete is the API policy’s call (updateComment / deleteComment); the PHP API does not ask.

## Author roles

Computed when the message is posted:

| Author | Role |
| --- | --- |
| the ticket’s requester | `requester` |
| an agent for this ticket (the agent ability, narrowed by the AgentScope) | `agent` |
| a watcher (with tickets.watchers.can\_view) | `participant` |
| nobody (asSystem()) | `system` |
| anyone else | refused — TicketAccessDeniedException |

An agent the agent scope keeps off this ticket is no agent here: they reply as the watcher they are (a participant — no first-response stamp, and their reply resumes a waiting ticket) or are refused. A note by anyone but an agent for this ticket throws TicketAccessDeniedException. A Closed ticket takes no message at all — replies, notes, edits and deletes throw TicketClosedException (409 ticket\_closed in the API); open a follow-up ticket instead.

## What a reply sets off

- An explicit transition: is applied atomically — a denied transition leaves no reply — and replaces the reply automation. It applies only when the reply publishes; on a held reply its name is not even checked.
- Reply automations (requester or participant replies only): a Waiting ticket resumes (RequesterReplied); a Resolved ticket reopens (RequesterReopened) when tickets.comments.reply\_reopens is on.
- First response: the first published public agent reply stamps first\_responded\_at once and fires TicketFirstResponded. Notes, requester replies and held replies never stamp it.
- Auto-watch: an agent who writes starts watching (tickets.watchers.auto.agent\_repliers).
- last\_activity\_at moves on every reply, note, transition and assignment. A note moves it too, so it is agent-only in API resources and sorts, and the bump never touches updated\_at.

## Host moderation

| Host setting | Effect on a ticket reply |
| --- | --- |
| comments.require\_approval on | Approved at once (tickets.comments.auto\_approve, on by default). |
| comments.require\_approval on and tickets.comments.auto\_approve off | Held for a moderator. |
| blocklist action pending / hidden matched | Held — never auto-approved (that would bypass the host’s filter). |
| blocklist action reject matched | comments throws CommentRejectedException and the whole reply rolls back (API 422 comment\_rejected). |

A held reply changes nothing but the thread. It stays agent-visible only, its replied activity is internal (held: true, plus the transition it asked for), and it applies no transition, runs no automation, stamps no first response, fires no TicketReplied and notifies nobody — so a requester never sees a status change caused by a message nobody can read. Approving it later through comments publishes the message only; apply the transition yourself if you still want it.

Manual moderation stalls the waiting → resume flow: with comments.require\_approval on and tickets.comments.auto\_approve off, every reply is held and approving one later does not catch up — a pending ticket stays pending, a resolved one is not reopened, nobody is notified and the replied activity stays internal. Comments still delivers the reply’s @mentions on approval. If you moderate by hand, listen to comments’ CommentApproved for ticket comments and run what you need (Tickets::for($ticket)->resume(), a notification to the assignee).

## comments.authorization

With comments.authorization on (off by default), comments checks its own Gate abilities against the authenticated user on every post, approval, lock and unlock — not against the comment’s author. Guest replies, system notes, merge notes, the auto-approval and the scheduler’s auto-close lock then depend on your CommentPolicy, and tickets:doctor warns. Tickets’ own policy has already decided who may write on a ticket, so let ticket commentables through:

```php
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Comments\Models\Comment;
use RoundlyConsulting\Comments\Policies\CommentPolicy;
use RoundlyConsulting\Tickets\Models\Ticket;

final class AppCommentPolicy extends CommentPolicy
{
    public function create(?Model $user, ?Model $commentable = null): bool
    {
        return $commentable instanceof Ticket || parent::create($user, $commentable);
    }

    public function moderate(?Model $user, Comment $comment): bool
    {
        return $comment->commentable instanceof Ticket || parent::moderate($user, $comment);
    }

    public function lock(?Model $user, Model $lockable): bool
    {
        return $lockable instanceof Ticket || parent::lock($user, $lockable);
    }

    public function unlock(?Model $user, Model $lockable): bool
    {
        return $lockable instanceof Ticket || parent::unlock($user, $lockable);
    }

    // update() / delete() the same way, if you edit ticket messages through comments
}
```

## Mentions

Comments parses @handle and fires CommentMentioned — only when the host sets comments.mention\_resolver, only for an approved comment, once per person. Tickets turns a mention on a ticket comment into TicketMentioned only for someone who may read that comment: a note’s mention reaches agents the agent scope lets work on the ticket, a reply’s mention needs view access. The mentioned notification goes out only when the message’s own notification does not already reach that person; with tickets.watchers.auto.mentioned on, a mentioned reader starts watching. Your own CommentMentioned / CommentCreated listeners see internal notes too — filter on meta.tickets.internal there.

## Attachments on messages

Reply and note attachments are comment attachments: comments’ private attachments bucket on comments.media.private\_disk, through media-library. The API serves them with short-lived signed URLs, and a guest can download only the files of the ticket and of its public thread.

## Querying the thread through comments

```php
use RoundlyConsulting\Comments\Facades\Comments;

Comments::for($ticket)->whereMeta('tickets.role', 'requester')->get();
Comments::for($ticket)->whereMeta('tickets.internal', true)->get();   // the notes
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/tickets-for-laravel.md)
