---
title: "Querying and the activity timeline — Tickets for Laravel | Roundly"
description: "Eloquent scopes shared by PHP and the API — status, priority, category, assignee, fields, dates and search — plus the append-only activity timeline."
url: https://roundly-consulting.com/open-source/docs/tickets-for-laravel/querying
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)

# Querying and the activity timeline

Plain Eloquent scopes on Ticket. The API’s list filters are built on them, so PHP and HTTP filter identically:

```php
use RoundlyConsulting\Tickets\Enums\StatusCategory;
use RoundlyConsulting\Tickets\Models\Ticket;

Tickets::visibleTo($user)->unresolved()->orderByPriority()->paginate();
Ticket::query()->assignedTo($agent)->withStatusCategory(StatusCategory::Waiting)->get();
Ticket::query()->inCategory($hardware, descendants: true)->overdue()->count();
Ticket::query()->taggedWith('printer', 'vip')->createdFrom(now()->subWeek())->get();
Ticket::query()->search('TCK-42')->first();
```

In host code that must follow a model swap, query through Support\\TicketModels::ticket()::query().

| Scope | Meaning |
| --- | --- |
| `visibleTo(Model $user)` | The tickets TicketPolicy::view allows (Tickets::visibleTo()). |
| `assignedTo(Model ...$assignees) / unassigned()` | Assignee morph. |
| `requestedBy(Model $requester)` | Requester morph (a user or a TicketGuest). |
| `inProject(?Model $project)` | null = tickets without a project. |
| `inCategory(TicketCategory\|int $category, bool $descendants = false)` | With its subcategories (bounded by categories.max\_depth). |
| `withStatus(BackedEnum\|string ...$states)` | Exact state values. |
| `withStatusCategory(StatusCategory ...$categories)` | The states of the configured enum in those categories. |
| `unresolved()` | Open, Active or Waiting. |
| `withPriority(BackedEnum\|string ...$priorities)` | Any of the priorities. |
| `taggedWith(string\|TicketTag ...$tags)` | Any of the tags, by slug or model. |
| `watchedBy(Model $watcher)` | Tickets the model watches. |
| `whereField(string $key, mixed $value) / whereFieldBetween(string $key, mixed $from, mixed $to)` | Custom field values, cast to the field’s type; a null bound of the range is open. An unknown, encrypted or multiselect key throws UnknownTicketFieldException; the range takes number, decimal, date and datetime fields only. |
| `dueBefore / dueAfter(CarbonInterface), dueFrom / dueTo, overdue()` | Before / after are exclusive, from / to inclusive; overdue = past due and unresolved. |
| `createdFrom / createdTo(CarbonInterface)` | Inclusive. |
| `viaChannel(string ...$channels)` | Intake channel. |
| `search(string $term)` | The reference exactly, or subject/description LIKE (wildcards matched literally). |
| `merged(bool $merged = true)` | merged\_into\_id set / not set. |
| `orderByPriority(string $direction = 'desc')` | By the enum weights; an unknown stored value sorts as 0. |

## The activity timeline

Every change — status, priority, owner, due date, tags, fields, merges, edits and deletions of messages — goes into an append-only timeline, written inside each action’s transaction (status changes inside lifecycle’s transition transaction):

```php
Tickets::for($ticket)->activity();                 // public entries, oldest first — safe for a requester-facing page
Tickets::for($ticket)->activity(internal: true);   // every entry, for agents
```

| Type | Visibility |
| --- | --- |
| `created, status_changed, priority_changed, category_changed, assigned, unassigned, due_changed, attachment_added, attachment_removed, requester_changed, overdue` | public |
| `merged, merged_from` | public, counting the published messages only (comments), no reason — plus an internal entry of the same type with every message counted and the reason, when those differ |
| `replied` | public — internal for a reply the host’s moderation held (properties.held: true, plus properties.transition when it asked for one it did not get) |
| `comment_edited, comment_deleted` | as visible as the message: public for a published public reply, internal for a note or a held reply; properties comment (id) and role — never a body |
| `updated` | public for subject / description; a change of the host meta is a separate internal entry |
| `note_added, fields_changed, tagged, untagged, linked, unlinked, watcher_added, watcher_removed, guest_access_revoked, deleted, restored` | internal (agent-only) |

Entries carry the actor, an is\_system flag, the visibility and properties — ids, keys, from/to; never a note body or an encrypted value. An update or delete throws ActivityIsAppendOnlyException, and a force-deleted ticket cascades. tickets.activity.enabled off writes nothing and the endpoint returns \[\]. The API filters entries by role.

## 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)
