Activity Log Logo
Concepts

How it works

The mental model and vocabulary behind the timeline pipeline.

This page is the canonical reference for the package's mental model: the pipeline, the building blocks, the type taxonomy, and the dedup/priority rules. Read it once before configuring sources, writing renderers, or debugging missing entries.

The pipeline

$record->timeline()             [TimelineBuilder]
    ↓ resolves
TimelineSource(s) per source    [resolve(subject, Window)]
    ↓ yields
TimelineEntry stream            [filter → dedup → sort]
    ↓ paginated
LengthAwarePaginator → Renderer → Blade view

A call to $record->timeline() returns a fluent TimelineBuilder. Each registered source is asked to resolve() entries inside a shared Window. The combined stream is filtered, deduplicated, sorted, and either paginated or returned whole. Renderers turn each TimelineEntry into HTML at view time.

Core building blocks

Subject. Any Eloquent model implementing Relaticle\ActivityLog\Contracts\HasTimeline. In practice you get this for free by using the Relaticle\ActivityLog\Concerns\InteractsWithTimeline trait, which provides timeline(), paginateTimeline(), and forgetTimelineCache(). The subject is passed into every source's resolve() call.

Source. A class implementing Relaticle\ActivityLog\Contracts\TimelineSource. Sources own the data: they query the database (or any other store) and yield TimelineEntry instances. The package ships four built-ins, registered via the builder's helpers:

$record->timeline()
    ->fromActivityLog()                                    // ActivityLogSource
    ->fromActivityLogOf(['comments'])                      // RelatedActivityLogSource
    ->fromRelation('invoices', fn ($s) => $s->title(...))  // RelatedModelSource
    ->fromCustom(fn ($subject, $window) => yield ...);     // CustomEventSource

See /essentials/sources for full source configuration.

Entry. Relaticle\ActivityLog\Timeline\TimelineEntry — an immutable readonly value object. It carries id, type, event, occurredAt, dedupKey, sourcePriority, optional subject/causer/relatedModel, plus presentation hints (title, description, icon, color, renderer, properties). Sources construct entries; the builder, dedup, sort, and renderer only consume them.

Renderer. A class implementing Relaticle\ActivityLog\Contracts\TimelineRenderer. Given a TimelineEntry, it returns a View or HtmlString. Renderers are looked up by the entry's renderer field (explicit), then its event, then its type, falling back to DefaultRenderer. See /essentials/customization.

The Window value object

Relaticle\ActivityLog\Timeline\Window is the read-only context passed to every source's resolve(). It carries:

  • from / to — the date range set via ->between($from, $to) (both nullable).
  • cap — the per-source over-fetch limit. The builder computes cap = perPage * (page + buffer) for paginated reads, and uses a hard 10000 ceiling for ->get().
  • typeAllow / typeDeny / eventAllow / eventDeny — the active filters, mirrored so sources can push them down into queries (otherwise the builder applies them post-yield).

Sources should respect cap to keep memory bounded — RelatedActivityLogSource, for example, calls ->limit($window->cap) on its underlying query. The builder constructs the Window via makeWindow($cap).

Type taxonomy

Two distinct "type" axes exist. Do not confuse them.The entry-type axis ($entry->type) has 3 values today:
  • activity_log
  • related_model
  • custom
The source-priority config axis (source_priorities config keys) has 4 keys:
  • activity_log
  • related_activity_log
  • related_model
  • custom
Critically: RelatedActivityLogSource emits entries with type='activity_log' (not'related_activity_log'). Filtering with ->ofType(['related_activity_log']) will never match anything. To distinguish own-log entries from related-log entries today, inspect $entry->relatedModel (it's null for ActivityLogSource entries and an Eloquent model for RelatedActivityLogSource entries) after calling ->get() — there is no builder-level filter for source-of-origin.See /troubleshooting ("Type filter doesn't match anything") and issue #11.

Source priorities

Defaults live in config/activity-log.php under source_priorities. Higher priority wins on dedup ties.

SourceDefault priorityConfig key
ActivityLogSource10source_priorities.activity_log
RelatedActivityLogSource10source_priorities.related_activity_log
RelatedModelSource20source_priorities.related_model
CustomEventSource30source_priorities.custom

Override per-call by passing the second argument to any builder helper:

$record->timeline()->fromCustom($resolver, priority: 100);

Dedup behavior

Entries sharing a dedupKey collapse to a single entry: the one with the highest sourcePriority wins. On equal priority, first-seen wins (sources are resolved in the order they were registered).

The default dedupKey is generated by AbstractTimelineSource::dedupKeyFor():

{class}:{id}:{occurredAt-iso}

Override per builder:

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

Disable dedup entirely with ->deduplicate(false).

Lifecycle

  1. The builder calls each registered source's resolve($subject, $window).
  2. Yielded entries pass through passesFilters()typeAllow/typeDeny/eventAllow/eventDeny.
  3. Dedup is applied if enabled (default: true, overridable in config via deduplicate_by_default).
  4. The collection is sorted — sortByDateDesc() is the default; sortByDateAsc() flips it.
  5. Results are returned via paginate(perPage, page) (the standard Filament/Livewire path) or get() (capped at 10000 entries).
Copyright © 2026