Activity Log Logo
Essentials

Sources

The four built-in source types and how to register custom ones.

Sources own where entries come from. The Relaticle\ActivityLog\Timeline\TimelineBuilder composes one or more sources, asks each to resolve() inside a shared Window, and merges the streams. This page covers every built-in source plus the addSource() extension hook. For dedup mechanics, source-priority defaults, and the entry-type taxonomy, see /concepts/how-it-works.

fromActivityLog()

Registers Relaticle\ActivityLog\Timeline\Sources\ActivityLogSource — the subject's own spatie activity log.

public function fromActivityLog(?int $priority = null): self
$entries = $record->timeline()
    ->fromActivityLog()
    ->get();

Reads from the activity_log table where subject_type and subject_id match the subject. Each row becomes a TimelineEntry with type='activity_log'.

Subject must be persisted.ActivityLogSource::resolve() throws a DomainException when $subject->getKey() === null. Don't call timeline() from a creating model event or on a fresh, unsaved instance.

$priority overrides the default of 10 (see source_priorities.activity_log).

fromActivityLogOf(array $relations)

Registers Relaticle\ActivityLog\Timeline\Sources\RelatedActivityLogSource — spatie activity-log entries that belong to the subject's related models.

public function fromActivityLogOf(array $relations, ?int $priority = null): self
$entries = $opportunity->timeline()
    ->fromActivityLogOf(['comments', 'tasks'])
    ->get();

For each named relation, the source loads the related rows, then queries activity_log for matching (subject_type, subject_id) pairs.

Unbounded relation fetch.RelatedActivityLogSource calls $subject->{$relation}()->get() with no limit to discover IDs to look up. For relations with thousands of rows this is a real cost — every timeline render loads every related row into memory. Tracked by issue #14. Workaround: use addSource() with a custom-scoped source if the relation is large.
Entries from this source carry type='activity_log', not'related_activity_log'. The related_activity_log key only exists in config('activity-log.source_priorities') for priority configuration. See the type taxonomy in /concepts/how-it-works.

fromRelation(string $relation, Closure $configure)

Registers Relaticle\ActivityLog\Timeline\Sources\RelatedModelSource — synthetic events derived from timestamp columns on related rows. Use this when you don't log the related model with spatie but still want its lifecycle on the timeline.

public function fromRelation(string $relation, Closure $configure, ?int $priority = null): self
use Filament\Support\Icons\Heroicon;
use Relaticle\ActivityLog\Timeline\Sources\RelatedModelSource;

$record->timeline()
    ->fromRelation('invoices', function (RelatedModelSource $source): void {
        $source
            ->event('issued_at', 'invoice.issued', icon: Heroicon::DocumentText->value, color: 'info')
            ->event('paid_at', 'invoice.paid', icon: Heroicon::CheckCircle->value, color: 'success')
            ->event('voided_at', 'invoice.voided', color: 'danger', when: fn ($invoice): bool => $invoice->total > 0)
            ->with(['customer'])
            ->using(fn ($query) => $query->where('archived', false))
            ->title(fn ($invoice): string => "Invoice #{$invoice->number}")
            ->description(fn ($invoice): string => "{$invoice->total} {$invoice->currency}")
            ->causer('createdBy');
    });

RelatedModelSource API

MethodPurpose
event($column, $event, $icon = null, $color = null, $when = null)Register one event per timestamp column. $when is an optional row-level filter returning bool.
with(array $relations)Eager-loads relations on every event query. Prevents N+1 in renderers.
using(Closure $modifier)SQL-level query modifier — receives the query builder. Use for tenant scopes, soft-delete, archived flags.
title(Closure $resolver)Per-row resolver for the entry title. Receives the related row, returns string.
description(Closure $resolver)Per-row resolver for the entry description. Receives the related row, returns string.
causer(Closure|string $resolver)Relation name as a string, or a Closure returning Model|null.
event(when: ...) runs post-fetch in PHP. It filters yielded entries one-by-one after the SQL query has already loaded the rows — it does not reduce the row count fetched from the database. For SQL-level filtering (tenant scope, soft-delete, archived flag), use using() instead so the predicate becomes a WHERE clause.

fromCustom(Closure $resolver)

Registers Relaticle\ActivityLog\Timeline\Sources\CustomEventSource — yields Relaticle\ActivityLog\Timeline\TimelineEntry instances directly, with no assumptions about where they come from.

public function fromCustom(Closure $resolver, ?int $priority = null): self
use Carbon\CarbonImmutable;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;

$record->timeline()
    ->fromCustom(function ($subject, Window $window) {
        $query = $subject->stripeCharges()
            ->orderByDesc('created_at')
            ->limit($window->cap);

        if ($window->from instanceof CarbonImmutable) {
            $query->where('created_at', '>=', $window->from);
        }

        if ($window->to instanceof CarbonImmutable) {
            $query->where('created_at', '<=', $window->to);
        }

        foreach ($query->cursor() as $charge) {
            yield new TimelineEntry(
                id: "stripe:charge:{$charge->id}",
                type: 'custom',
                event: 'charge.succeeded',
                occurredAt: CarbonImmutable::parse($charge->created_at),
                dedupKey: "stripe:charge:{$charge->id}",
                sourcePriority: 30,
                subject: $subject,
                title: "Charge {$charge->amount_formatted}",
            );
        }
    });

The closure receives the subject and the active Window — respect cap, from, and to to keep memory bounded and honor ->between(...) filters.

CustomEventSource validates yield types. Yielding anything other than a TimelineEntry instance throws TypeError with the offending type name. Don't yield arrays, raw models, or DTOs.

addSource(TimelineSource $source)

For reusable source classes. Implement the Relaticle\ActivityLog\Contracts\TimelineSource contract and register the instance directly:

namespace App\Timeline\Sources;

use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Relaticle\ActivityLog\Contracts\TimelineSource;
use Relaticle\ActivityLog\Timeline\TimelineEntry;
use Relaticle\ActivityLog\Timeline\Window;

final class StripePaymentSource implements TimelineSource
{
    public function __construct(private readonly int $priority = 30) {}

    public function priority(): int
    {
        return $this->priority;
    }

    public function resolve(Model $subject, Window $window): iterable
    {
        $query = $subject->stripePayments()
            ->orderByDesc('created_at')
            ->limit($window->cap);

        if ($window->from instanceof CarbonImmutable) {
            $query->where('created_at', '>=', $window->from);
        }

        foreach ($query->cursor() as $payment) {
            yield new TimelineEntry(
                id: "stripe:payment:{$payment->id}",
                type: 'custom',
                event: "payment.{$payment->status}",
                occurredAt: CarbonImmutable::parse($payment->created_at),
                dedupKey: "stripe:payment:{$payment->id}",
                sourcePriority: $this->priority,
                subject: $subject,
                title: "Payment {$payment->amount_formatted}",
            );
        }
    }
}
$record->timeline()
    ->fromActivityLog()
    ->addSource(new StripePaymentSource());

Prefer addSource() over fromCustom() when the source is reusable across models, has its own constructor dependencies, or needs to be tested in isolation.

Per-call $priority override

Every from*() method accepts an optional $priority argument that overrides the default from config('activity-log.source_priorities.*'):

$record->timeline()
    ->fromActivityLog(priority: 50)
    ->fromActivityLogOf(['comments'], priority: 60)
    ->fromRelation('invoices', $configure, priority: 80)
    ->fromCustom($resolver, priority: 100);

Useful when one resource needs a non-default priority without touching config/activity-log.php. See /concepts/how-it-works for how priority resolves dedup ties.

Copyright © 2026