Sprinter Docs

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_at descending.
  • relevance -- Sort by relevanceScore descending (from entity metadata), with recency as tiebreaker.
  • importance -- Sort by importanceScore descending (from entity metadata), with recency as tiebreaker.
  • updated -- Sort by updated_at descending.

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:

  1. Parse config -- Validate the raw config against FeedConfigSchema. Invalid configs silently fall back to defaults.

  2. Build entity query -- Start with tenant-scoped entities, then apply filters:

    • entityTypeSlugs filters via entity_type_slug (denormalized column).
    • parentEntityId filters via parent_id.
    • tags uses the PostgreSQL overlaps operator on the tags array.
    • search performs case-insensitive matching on title and description.
    • timeRange filters the sort column (created_at or updated_at) to the range start.
    • Tab-specific sorting: each tab maps to a sort column via getTabSortColumn().
  3. Fetch data -- Entity query and agent list execute in parallel for performance.

  4. Convert to feed items -- Each entity row is mapped to a FeedItem via rowToFeedItem(), which extracts preview fields, scores (from entity metadata), and builds the entity detail link.

  5. Apply score filters -- Items below minImportance or minRelevance thresholds are removed.

  6. 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.

  7. Agent slug filter -- If agentSlugs are specified, filter items to only those with recent activity from the specified agents. This checks the agent_slug field in activity metadata.

  8. Resolve actors -- User profiles for activity actors are fetched in parallel to populate display names.

  9. Rank items -- The ranking step depends on the tab:

    • for-you: computeForYouScores() queries user ownership and interaction history, then rankForYou() sorts by personalized scores.
    • Other tabs: rankFeedItems() sorts by the selected sort comparator.
  10. 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:

  1. 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).
  2. 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).
  3. 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).
  4. 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 >= 7 generates a "High importance (X.X)" reason.
  • relevanceScore >= 7 generates a "High relevance (X.X)" reason.
  • Created within 24 hours generates "Recently created."
  • Updated within 24 hours generates "Recently updated."
  • Non-null lastAgentSlug generates "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)

FunctionSignatureDescription
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)

FunctionSignatureDescription
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)

FunctionSignatureDescription
parseFeedUrlState(params) => FeedUrlStateParse URL params into client feed state with UI defaults.
serializeFeedUrlState(state) => URLSearchParamsSerialize feed state while omitting default/empty values.
feedConfigFromSearchParams(params) => FeedConfigParse URL params into a server resolver config.
buildFeedHref(config, basePath?) => stringBuild a shareable feed URL from a feed config.

Personalization (features/feed/lib/for-you-scoring.ts)

FunctionSignatureDescription
computeScores(items, ownedIds, interactedIds, preferredTypes) => Map<string, number>Pure personalization scoring.

Personalization DB Layer (features/feed/server/resolve-for-you.ts)

FunctionSignatureDescription
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, updateEntity tools), 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 agentSlugs filter lets users see entities touched by specific agents.
  • Importance and relevance scores -- Agents can set importanceScore and relevanceScore in entity metadata, which directly influences feed ranking.
  • Feed as a block -- The entity-feed block type renders a feed within views. Agents authoring views via manageView can 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.

  • 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/) -- The entity-feed block type renders a feed within views and dashboards.
  • Scoring -- Entity scoring criteria (from EntityTypeConfig.scoring) provide the score field; importanceScore and relevanceScore come from entity metadata.
  • Views System (features/views/) -- Feed blocks can be configured within workspace and list views.

On this page