SemiLayerDocs

Feeds — Quickstart

Declare a feed, push, render with useFeed. Five moves, no ingest re-run needed if you already have vectors.

feedarticles
articles: {
  source: 'main',
  table: 'articles',
  fields: {
    id:         { type: 'number', primaryKey: true },
    title:      { type: 'text',   searchable: { weight: 2 } },
    body:       { type: 'text',   searchable: true },
    tags:       { type: 'text' },
    updated_at: { type: 'date' },
  },
  feeds: {
    foryou: {
      candidates: { from: 'embeddings', limit: 300 },
      rank: {
        similarity: { weight: 0.7, against: 'topics' },
        recency:    { weight: 0.3, halfLife: '7d' },
      },
      pagination: { pageSize: 10 },
    },
  },
  grants: {
    feed: { foryou: 'public' },
  },
}

Five moves

Add a feed to your lens config

Open sl.config.ts. Declare the feed under feeds.<name>, then grant it access under grants.feed.<name>. The smallest useful feed has candidates.from, a rank block, and an access rule:

articles: {
  // ... fields, sources, etc.
  feeds: {
    foryou: {
      candidates: { from: 'embeddings', limit: 300 },
      rank: {
        similarity: { weight: 0.7, against: 'topics' },
        recency:    { weight: 0.3, halfLife: '7d' },
      },
      pagination: { pageSize: 10 },
    },
  },
  grants: {
    feed: { foryou: 'public' },   // ← REQUIRED: per-named-feed opt-in
  },
}

Push

semilayer push

This registers the new feed without re-ingesting. Feeds read from the same vectors the search operation uses — so if your lens already has searchable fields indexed, you're ready.

Inspect from the CLI

semilayer feed list articles
# Lists every feed on the lens with rule / pageSize / scorers

semilayer feed view articles.foryou --context '{"topics":["security"]}'
# Fetches one page and prints a table

Generate Beam

semilayer generate

Beam emits a feed namespace on each lens. Every declared feed gets a typed method with a matching .next(), .subscribe(), and .explain():

import { beam } from './beam'

const page = await beam.articles.feed.foryou({
  context:  { topics: ['security', 'auth'] },
  pageSize: 10,
})

// page.items, page.cursor (pass to .next), page.evolved, page.meta

Render with useFeed

import { useFeed } from '@semilayer/react'
import { beam } from '@/lib/beam'

const feed = beam.articles.feed.foryou

export function ForYouFeed() {
  const { items, fetchMore, cursor, isLoading } = useFeed(feed, {
    context:      { topics: ['security', 'auth'] },
    liveUpdates:  true,   // WS subscribe; auto-fetch on tick
    pageSize:     10,
  })

  return (
    <div>
      {items.map((it) => (
        <article key={it.id}>
          <h3>{it.metadata.title}</h3>
          <small>rank #{it.rank} · score {it.score.toFixed(2)}</small>
        </article>
      ))}
      {cursor && <button onClick={fetchMore} disabled={isLoading}>Load more</button>}
    </div>
  )
}

Done. items[i].metadata is fully typed from the lens's field declarations.

What you just built

  • A typed Beam method: beam.articles.feed.foryou(params)
  • A WebSocket subscription: feed.foryou.subscribe() yields feed.tick events
  • A debug endpoint: feed.foryou.explain({ recordId }) (sk-only)
  • A React hook integration that owns pagination + evolution

All from one config block. Next up: Ranking — every scorer with worked math.