rails-activity-timeline
Use when adding polymorphic activity timelines with live Turbo Stream updates to any Rails model. Covers migration, model, concern, shared partials, broadcasting, and optional AI-generated change summaries. Triggers on activity feed, audit trail, timeline, field change tracking, or Turbo Stream broadcasting patterns.
From majestic-railsnpx claudepluginhub majesticlabs-dev/majestic-marketplace --plugin majestic-railsThis skill uses the workspace's default tool permissions.
references/ai-summaries.mdreferences/broadcasting.mdreferences/display.mdreferences/setup.mdRails Activity Timeline
Add a polymorphic activity timeline with live Turbo Stream updates to any Rails model. Track field changes, status transitions, comments, attachments, and more with configurable icons and colors per action type.
When to Use
- Adding an activity/audit timeline to any Rails model
- Tracking field changes, status transitions, comments, or attachments
- Displaying live-updating activity feeds via Turbo Streams
- Auto-logging lifecycle events on child/associated models
- Building activity feeds with configurable icons and colors
Architecture
Four components:
ActivityEventmodel (polymorphic) — the core event recordActivityTrackableconcern — auto-logs child model lifecycle events on a parent's timeline- Shared timeline partial — vertical timeline with Turbo Stream live updates
- Display configuration — icon SVG path + accent color per action type, kept in the model
Key Associations (on ActivityEvent)
belongs_to :trackable, polymorphic: true # parent entity whose timeline this belongs to
belongs_to :subject, polymorphic: true, optional: true # related entity being acted upon
belongs_to :user, optional: true # who performed the action
Generic Action Set
ACTIONS = %w[
created updated destroyed
field_updated status_changed
comment_added
attachment_added attachment_removed
assigned unassigned
relationship_added relationship_removed
tag_added tag_removed
].freeze
Add domain-specific actions (e.g., published, approved) to ACTIONS and the DISPLAY hash.
Core Patterns
Setup from Scratch
Migration, full ActivityEvent model, ActivityTrackable concern, and prerequisites.
See: references/setup.md
Turbo Stream Broadcasting
Stream naming convention, broadcast methods, partial routing, shared timeline partial, and event partial.
See: references/broadcasting.md
Display Configuration
The DISPLAY hash, adding custom actions with icons, display methods, customizing the event partial.
See: references/display.md
AI Summaries (Optional)
AI-generated change summaries for field_updated events with long text changes.
See: references/ai-summaries.md
Adding Timeline to a Model
Step 1: Add the association
class Project < ApplicationRecord
has_many :activity_events, as: :trackable, dependent: :destroy
end
Step 2: Add model type to broadcast routing
In ActivityEvent#broadcast_partial, add your model to the case statement:
def broadcast_partial
case trackable_type
when "Project", "Article" then "activity_events/activity_event"
else "activity_events/activity_event"
end
end
Step 3: Render the shared partial
<%= render "shared/activity_timeline", record: @project %>
Step 4: Create events
# Status change
ActivityEvent.create!(
trackable: @project,
user: current_user,
action: "status_changed",
details: { from_status: "draft", to_status: "active" }
)
# Field change
if @project.saved_change_to_status?
ActivityEvent.create!(
trackable: @project,
user: current_user,
action: "field_updated",
details: { field: "status", from: @project.status_previously_was, to: @project.status }
)
end
ActivityTrackable Concern
For child models that auto-log on a parent's timeline:
class Comment < ApplicationRecord
include ActivityTrackable
def activity_trackable = post
def activity_action_created = "comment_added"
def activity_action_destroyed = "comment_removed"
def activity_label = body.truncate(60)
def activity_user = user
end
Override activity_created_details or activity_destroyed_details for additional metadata:
def activity_created_details
{ file_size: byte_size, content_type: content_type }
end
Querying
@project.activity_events.timeline # recent, includes user, limit 50
@project.activity_events.by_type("status_changed")
ActivityEvent.where(subject: @task).recent
Common Pitfalls
-
Wrong trackable — events appear on wrong timeline.
activity_trackablemust return the parent, notself. -
Missing broadcast routing for new types — add new model types to the
broadcast_partialcase statement; without this broadcasts silently fail. -
Missing actions in ACTIONS + DISPLAY — validation rejects unrecognized actions. Always add both.
-
N+1 on timeline — always use
.timelinescope (includes:user). Add associations to the scope, not the partial. -
Turbo Stream not updating —
turbo_stream_fromtag in view must match stream name in broadcast method. Convention:"#{record_type}_{id}_activity".
Prerequisites
- Rails 7+ with
turbo-rails - Action Cable or Solid Cable configured
- A
Usermodel (optional, for attribution) - Tailwind CSS for default styling