Activity Log Logo
Essentials

Refining the timeline

Filter, sort, deduplicate, and paginate the merged stream.

Every method on this page is a chainable call on Relaticle\ActivityLog\Timeline\TimelineBuilder between source registration and result consumption. For source registration see /essentials/sources; for the lifecycle and dedup model see /concepts/how-it-works.

Filtering

Five filters are available — one window, two type filters (allow/deny), two event filters (allow/deny):

$record->timeline()
    ->between(now()->subMonth(), now())
    ->ofType(['activity_log'])
    ->exceptType(['custom'])
    ->ofEvent(['created', 'updated'])
    ->exceptEvent(['draft_saved'])
    ->get();
MethodPurpose
between(?CarbonInterface $from, ?CarbonInterface $to)Passes through to each source's Window. Either side may be null for an open-ended range.
ofType(array $types)Entry-type allow-list. Anything not in the list is dropped.
exceptType(array $types)Entry-type deny-list. Anything in the list is dropped.
ofEvent(array $events)Event-name allow-list.
exceptEvent(array $events)Event-name deny-list.

Type and event filters are mirrored onto every source's Window so sources can push them into SQL where possible. The builder also re-applies them post-yield as a safety net, so a source that ignores the Window filters still produces correct output.

There are only three entry types today: activity_log, related_model, custom. Entries from RelatedActivityLogSource carry type='activity_log' — calling ->ofType(['related_activity_log']) will silently match nothing. See the type taxonomy for the full breakdown.

Sorting

$record->timeline()->sortByDateDesc()->get(); // default — newest first

$record->timeline()->sortByDateAsc()->get();  // oldest first

Sorting happens after sources merge, so individual sources may yield in any order. The sort key is $entry->occurredAt->getTimestamp().

Deduplication

$record->timeline()->deduplicate(false)->get(); // disable dedup entirely

Override the dedup key with a closure that receives each TimelineEntry:

$record->timeline()
    ->dedupKeyUsing(fn ($entry) => "{$entry->type}:{$entry->event}:{$entry->occurredAt->toDateString()}");

The default dedup key is {class}:{id}:{occurredAt-iso}. See /concepts/how-it-works#dedup-behavior for what wins when keys collide (highest sourcePriority; ties broken by registration order).

Running the query

MethodReturnsNotes
get()Collection<int, TimelineEntry>Caps at 10000 entries internally.
paginate(?int $perPage = null, int $page = 1)LengthAwarePaginator<int, TimelineEntry>$perPage = null falls back to default_per_page config (20).
count()intRuns get() internally — no separate COUNT query. Use sparingly on large datasets.
$entries = $record->timeline()->fromActivityLog()->get();

$page = $record->timeline()->fromActivityLog()->paginate(perPage: 25, page: 2);

$total = $record->timeline()->fromActivityLog()->count();
For in-Filament rendering you almost never call these directly — the infolist component, relation manager, and action all mount the Livewire timeline component, which calls paginate() for you. Direct calls are useful for tests, queue workers, exports, and other non-UI consumers.

Trait helper: paginateTimeline()

The Relaticle\ActivityLog\Concerns\InteractsWithTimeline trait exposes paginateTimeline(?int $perPage = null, int $page = 1) as a pass-through to $this->timeline()->paginate(...). It's required by the Relaticle\ActivityLog\Contracts\HasTimeline interface, so consumers can paginate without going through the builder explicitly:

$page = $opportunity->paginateTimeline(perPage: 20, page: 1);

This is the entry point the Livewire timeline component uses internally. Override timeline() on the model to customize source registration once and have every caller — including paginateTimeline() — pick it up.

Pagination buffer mechanics

Pagination uses an over-fetch strategy controlled by the pagination_buffer config key (default 2):

cap = perPage × (page + buffer)

Each source is asked for at most cap entries via Window::cap. After dedup and post-source filtering drop entries, the remaining set is sliced down to the requested page. Over-fetching keeps deeper pages from coming up short when entries are removed during the pipeline.

Trade-off:

  • Higher buffer — more memory, more rows scanned per source, but exact pagination at deep pages even when sources collide heavily.
  • Lower buffer — less memory, fewer rows scanned, but risk of a deep page returning fewer than perPage entries when dedup removes a lot.

When to lower it: sources rarely collide (no overlapping dedupKeys) and your filters are permissive. The default 2 is overkill for single-source timelines.

When to raise it: sources overlap heavily — for example, a custom source that re-emits events also covered by spatie activity log — and you need exact page sizes at high page numbers.

See /essentials/configuration for the config key.

Copyright © 2026