Feed System
Personalized entity feed with configurable sorting, filtering, card and compact views, for-you personalization, activity enrichment, digest summarization, and block integration.
Overview
The feed system provides a personalized, ranked view of entities across all types in a tenant. It serves as the central discovery surface -- users see what matters most to them, ranked by recency, relevance, importance, or personalized scoring. Each feed item carries explainability: reasons why it was surfaced (high importance, recently updated, agent activity, etc.).
The feed has evolved through two versions: v1 provides basic entity listing with sorting and filtering; v2 enriches items with recent activity context, actor information, and raw entity data for card rendering. Both versions are available through the resolver, with v2 powering the current UI.
The feed also functions as a block type (entity-feed), enabling it to be embedded in dashboards, workspace views, and entity detail pages alongside other blocks.
Key Concepts
Feed Tabs
const FEED_TABS = ["for-you", "new", "updated", "gallery"] as const;- for-you -- Personalized ranking based on the user's ownership, interaction history, and preferred entity types.
- new -- Sorted by creation date, showing the most recently created entities.
- updated -- Sorted by last update time, showing entities with the most recent changes.
- gallery -- Visual grid layout emphasizing entity images and card presentation.
Tab labels are mapped via FEED_TAB_LABELS:
const FEED_TAB_LABELS: Record<FeedTab, string> = {
"for-you": "For You",
new: "New",
updated: "Updated",
gallery: "Gallery",
};Sort Options
const FEED_SORT_OPTIONS = ["recency", "relevance", "importance", "updated"] as const;- recency -- Sort by
created_atdescending. - relevance -- Sort by
relevanceScoredescending (from entity metadata), with recency as tiebreaker. - importance -- Sort by
importanceScoredescending (from entity metadata), with recency as tiebreaker. - updated -- Sort by
updated_atdescending.
Show Modes
const FEED_SHOW_MODES = ["cards", "compact"] as const;- cards -- Full entity cards with preview fields, tags, scores, and activity context. Uses the entity type's card configuration for rendering.
- compact -- Condensed list items showing title, type, and key metadata. Higher information density for scanning.
Time Ranges
const FEED_TIME_RANGES = ["today", "week", "month", "all"] as const;Time ranges filter entities based on the active sort column (created_at for "new" tab, updated_at for "updated" tab).
URL Query Codec
features/feed/lib/feed-url-state.ts is the shared boundary between feed URL
state and server-side FeedConfig. The feed hook uses it to keep the current
route shareable, and GET /api/feed uses it to resolve the same filtered,
sorted feed from URL search parameters.
The codec supports tab, types, agents, time, q, tags, sort,
mode, limit, importance, and relevance. Empty client state serializes
to no query string; invalid values are ignored or defaulted before reaching the
resolver.
Feed Configuration
The FeedConfig schema (validated with Zod) controls all aspects of the feed:
interface FeedConfig {
entityTypeSlugs?: string[]; // Filter to specific types
parentEntityId?: string; // Scope to children of an entity
sortBy?: FeedSortBy; // recency, relevance, importance, updated
showMode?: FeedShowMode; // cards or compact
limit?: number; // Max items (default: 20)
tags?: string[]; // Tag filter (entities with any of these tags)
minImportance?: number; // Score threshold (0-10)
minRelevance?: number; // Score threshold (0-10)
tab?: FeedTab; // Active tab
agentSlugs?: string[]; // Filter to entities updated by specific agents
timeRange?: FeedTimeRange; // today, week, month, all
offset?: number; // Pagination offset (default: 0)
search?: string; // Text search on title/description
}Feed Items
Each feed item represents an entity surfaced in the feed:
interface FeedItem {
id: string;
entityId: string;
title: string;
description: string | null;
entityTypeSlug: string;
entityTypeName: string | null;
href: string; // Link to entity detail page
fields: Array<{ label: string; value: string }>; // Preview fields
tags: string[];
score: number | null; // Weighted scoring system score
importanceScore: number | null; // From metadata (0-10)
relevanceScore: number | null; // From metadata (0-10)
reasons: FeedItemReason[]; // Why this item was surfaced
updatedAt: string;
createdAt: string;
lastAgentSlug: string | null; // Agent that last modified this entity
}Feed Item Reasons (Explainability)
Every feed item carries reasons explaining why it was surfaced:
interface FeedItemReason {
type:
| "high_importance" // importanceScore >= 7
| "high_relevance" // relevanceScore >= 7
| "recently_updated" // Updated in the last 24 hours
| "recently_created" // Created in the last 24 hours
| "matched_tag" // Matched a tag filter
| "matched_filter" // Matched another filter
| "agent_updated"; // Updated by an AI agent
label: string; // Human-readable explanation
}Reasons are derived from item metadata by deriveReasons() when not pre-populated. The thresholds are configurable constants: importance >= 7, relevance >= 7, created/updated within 24 hours.
Feed V2 Items (Activity-Enriched)
The v2 feed extends items with activity context:
interface FeedItemV2 extends FeedItem {
recentActivity: FeedActivityItem[]; // Last N activity entries
lastActor: FeedActor | null; // Who last touched this entity
activityCount: number; // Total activity entries
entityData: FeedEntityData; // Raw entity data for card rendering
entityTypeData: FeedEntityTypeData; // Raw type data for card rendering
}Activity items include the action, actor name and type (user or agent), agent slug and icon, description, and timestamp. The lastActor field provides quick attribution -- who last modified this entity and whether it was a human or agent.
Feed Digest
The digest provides a high-level summary for the feed header:
interface FeedDigest {
stats: {
updatedToday: number;
createdToday: number;
workflowsCompleted: number;
pendingReviews: number;
};
topAgents: FeedDigestAgent[]; // Most active agents
needsAttention: FeedDigestAttentionItem[]; // Items requiring human action
aiSummaries?: string[]; // AI-generated summaries
}Attention items highlight entities with pending responses awaiting review or sessions/actions waiting on human input.
How It Works
Feed Resolution (V2)
The resolveFeedV2() function is the primary resolver:
-
Parse config -- Validate the raw config against
FeedConfigSchema. Invalid configs silently fall back to defaults. -
Build entity query -- Start with tenant-scoped entities, then apply filters:
entityTypeSlugsfilters viaentity_type_slug(denormalized column).parentEntityIdfilters viaparent_id.tagsuses the PostgreSQLoverlapsoperator on the tags array.searchperforms case-insensitive matching on title and description.timeRangefilters the sort column (created_at or updated_at) to the range start.- Tab-specific sorting: each tab maps to a sort column via
getTabSortColumn().
-
Fetch data -- Entity query and agent list execute in parallel for performance.
-
Convert to feed items -- Each entity row is mapped to a
FeedItemviarowToFeedItem(), which extracts preview fields, scores (from entity metadata), and builds the entity detail link. -
Apply score filters -- Items below
minImportanceorminRelevancethresholds are removed. -
Fetch activities -- Recent activities for all returned entity IDs are fetched in a single query (limited to avoid excessive data). Activities are grouped by entity.
-
Agent slug filter -- If
agentSlugsare specified, filter items to only those with recent activity from the specified agents. This checks theagent_slugfield in activity metadata. -
Resolve actors -- User profiles for activity actors are fetched in parallel to populate display names.
-
Rank items -- The ranking step depends on the tab:
- for-you:
computeForYouScores()queries user ownership and interaction history, thenrankForYou()sorts by personalized scores. - Other tabs:
rankFeedItems()sorts by the selected sort comparator.
- for-you:
-
Enrich with V2 data -- Each ranked item is augmented with recent activity entries, last actor info, activity count, and raw entity/type data for card rendering.
For-You Personalization
The "for-you" tab uses a multi-signal scoring model:
-
Fetch user context -- In parallel, query:
- User's owned entities (
entities.owner_id = userId). - User's recent activities (last 30 days, up to 200 entries).
- User's owned entities (
-
Build preference signals -- From the activity history:
ownedEntityIds-- Entities the user owns (score: +3).interactedEntityIds-- Entities the user has interacted with (score: +2).preferredTypeSlugs-- Top 3 entity types by interaction count (score: +1).
-
Compute scores -- For each feed item, the pure scoring function
computeScores()adds:- +3 if the user owns this entity.
- +2 if the user has interacted with this entity.
- +1 if the entity type is in the user's top 3 preferred types.
- +importanceScore/10 (normalized to 0-1 range).
- +relevanceScore/10 (normalized to 0-1 range).
-
Sort by score -- Items are sorted by their personalized score descending, with recency as tiebreaker.
The scoring function is pure (no database access) and fully testable. The database layer (computeForYouScores) handles data fetching and delegates to the pure function.
Ranking and Explainability
The rankFeedItems() function sorts items and attaches reasons:
function rankFeedItems(items: FeedItem[], sortBy: FeedSortBy): FeedItem[];Each comparator handles ties: importance and relevance comparators fall back to recency when scores are equal, ensuring a stable sort order.
After sorting, deriveReasons() generates explanation labels for items that do not already have reasons. Reasons are computed from the item's metadata:
importanceScore >= 7generates a "High importance (X.X)" reason.relevanceScore >= 7generates a "High relevance (X.X)" reason.- Created within 24 hours generates "Recently created."
- Updated within 24 hours generates "Recently updated."
- Non-null
lastAgentSluggenerates "Updated by {agentSlug}."
URL State Persistence
Feed configuration (tab, sort, show mode, filters) is persisted in URL search parameters. This enables sharing feed views via URL and preserving state across navigation. The feed client component reads from and writes to URL state through the shared query codec, and the feed API parses the same URL shape for direct server-side resolution.
API Reference
Server Actions (features/feed/server/resolve.ts)
| Function | Signature | Description |
|---|---|---|
resolveFeed | (rawConfig?: unknown) => Promise<FeedResult> | V1 resolver: entities with sorting and filtering. |
resolveFeedV2 | (rawConfig?: unknown) => Promise<FeedResultV2> | V2 resolver: activity-enriched items with card data. |
Pure Functions (features/feed/lib/rank.ts)
| Function | Signature | Description |
|---|---|---|
rankFeedItems | (items: FeedItem[], sortBy?: FeedSortBy) => FeedItem[] | Sort items and attach reasons. |
rankForYou | (items: FeedItem[], scores: Map<string, number>) => FeedItem[] | Sort by personalized scores with recency tiebreaker. |
deriveReasons | (item: FeedItem) => FeedItemReason[] | Compute explainability reasons from item metadata. |
URL Helpers (features/feed/lib/feed-url-state.ts)
| Function | Signature | Description |
|---|---|---|
parseFeedUrlState | (params) => FeedUrlState | Parse URL params into client feed state with UI defaults. |
serializeFeedUrlState | (state) => URLSearchParams | Serialize feed state while omitting default/empty values. |
feedConfigFromSearchParams | (params) => FeedConfig | Parse URL params into a server resolver config. |
buildFeedHref | (config, basePath?) => string | Build a shareable feed URL from a feed config. |
Personalization (features/feed/lib/for-you-scoring.ts)
| Function | Signature | Description |
|---|---|---|
computeScores | (items, ownedIds, interactedIds, preferredTypes) => Map<string, number> | Pure personalization scoring. |
Personalization DB Layer (features/feed/server/resolve-for-you.ts)
| Function | Signature | Description |
|---|---|---|
computeForYouScores | (items, userId, tenantId) => Promise<Map<string, number>> | Fetches user context and delegates to pure scoring. |
Result Types
interface FeedResult {
items: FeedItem[];
total: number;
config: FeedConfig;
hasMore: boolean;
}
interface FeedResultV2 {
items: FeedItemV2[];
total: number;
config: FeedConfig;
hasMore: boolean;
}For Agents
The feed system does not expose direct agent tools. However, agents influence the feed indirectly:
- Entity updates by agents -- When agents create or update entities (via
createEntity,updateEntitytools), those entities appear in the feed with "Updated by {agentSlug}" attribution. - Activity tracking -- All agent actions that produce activities affect the feed's activity enrichment layer. The
agentSlugsfilter lets users see entities touched by specific agents. - Importance and relevance scores -- Agents can set
importanceScoreandrelevanceScorein entity metadata, which directly influences feed ranking. - Feed as a block -- The
entity-feedblock type renders a feed within views. Agents authoring views viamanageViewcan configure feed blocks with specific filters and sort orders.
Design Decisions
Two-phase resolution. Entities are fetched first with broad filtering, then activities are fetched in a second pass scoped to the returned entity IDs. This avoids complex joins between entities and activities while keeping query performance predictable. The activity query limits total rows to prevent runaway fetches.
UUID suppression in preview fields. rowToFeedItem() skips any content field whose string value matches the UUID regex (/^[0-9a-f]{8}-...-[0-9a-f]{12}$/i) when building the card's fields preview array. Entity schemas frequently place relation reference fields (e.g. company_id) early in the property order; without this filter, those fields would occupy one of the three preview slots, showing a meaningless ID instead of an actual attribute. The loop now scans the full property list and stops after collecting three non-UUID, non-empty values.
Pure scoring function. The for-you personalization scoring is implemented as a pure function (computeScores) with no database access. This enables comprehensive unit testing of the scoring model without mocking. The DB layer (computeForYouScores) handles data fetching and delegates to the pure function.
Explainability as a first-class concept. Every feed item carries reasons explaining why it was surfaced. Rather than being a debugging afterthought, reasons are part of the item schema and rendered in the UI. This builds user trust in the ranking system and helps users understand what the platform considers important.
Denormalized entity_type_slug. Feed queries filter by entity_type_slug directly on the entities table instead of joining through entity_types. This denormalized column (synced via trigger) eliminates a join and enables index-only filtering.
Activity-enriched V2. The V2 resolver adds activity context because the feed UI evolved from "list of entities" to "what is happening with entities." The V1 resolver is maintained for backward compatibility and simpler use cases (like block embedding).
URL state over local state. Feed configuration is stored in URL search parameters rather than component state or localStorage. This makes feed views shareable and bookmarkable, and ensures the feed state survives navigation without additional persistence logic.
Agent slug filtering on activity metadata. Rather than adding an agent_id column to entities, agent attribution is tracked in the activities table's metadata. The agentSlugs filter checks metadata.agent_slug on activities, which is more flexible (an entity can be touched by multiple agents) but requires a secondary query.
Related Modules
- Entity System (
features/entities/) -- The feed queries and displays entities with their types, content, and metadata. - Activity System -- Activities provide the enrichment layer for V2 feed items and drive the "updated" tab sorting.
- Agent System (
features/agents/) -- Agent information (name, icon, slug) is resolved for activity attribution. - Block System (
features/blocks/) -- Theentity-feedblock type renders a feed within views and dashboards. - Scoring -- Entity scoring criteria (from
EntityTypeConfig.scoring) provide thescorefield;importanceScoreandrelevanceScorecome from entity metadata. - Views System (
features/views/) -- Feed blocks can be configured within workspace and list views.
Document Processing
End-to-end document lifecycle from upload through parsing, chunking, and embedding, with hybrid search, entity linking, signed URL access, and integration as extraction sources.
Quick Capture
AI-powered natural language input that parses free text into structured entities with automatic type detection, field extraction, and suggested relations.