SemiLayerDocs

Include Syntax

include is the parameter that tells SemiLayer which relations to attach to each parent row. Same shape on every read op: query, search, similar, feed, and their streaming variants.

interface IncludeOptions {
  where?:   Record<string, unknown>                                 // extra filter on the child lens
  select?:  '*' | string[]                                          // column projection
  orderBy?: { field: string; dir?: 'asc' | 'desc' } | Array<...>    // sort within parent
  limit?:   number                                                  // per-parent cap, 0 < N ≤ 1000
}

type IncludeSpec = Record<string, IncludeOptions | true>
queryrecipes
// Assume recipes.relations.{reviews, ingredientRows} already declared
// — see /joins/declaring-relations

Two forms

Shorthand — true

Use relation defaults: all declared fields, no extra filter, defaultIncludeLimit rows.

include: { reviews: true }

Object — per-call overrides

Customize any of the four knobs:

include: {
  reviews: {
    where:   { rating: { $gte: 4 } },
    orderBy: { field: 'helpful_count', dir: 'desc' },
    limit:   3,
    select:  ['author', 'rating'],
  },
}

Mix forms freely — one relation can be shorthand, another fully customized.

The four knobs

where

Same operator vocabulary as query predicates: $eq, $in, $gt, $gte, $lt, $lte. Top-level keys AND together. The foreign key filter (recipe_id IN (...)) is added automatically by the planner — you don't write it.

include: {
  reviews: { where: { rating: { $gte: 4 }, status: 'published' } },
}

select

Project specific fields. Smaller payload over the wire, slightly cheaper compute. Defaults to all fields on the lens ('*'). Selected fields must be declared on the target lens or you get a 422.

include: {
  reviews: { select: ['author', 'rating'] },     // only these
  reviews: { select: '*' },                       // default
}

orderBy

Single rule or array (applied in order). Used for the per-parent slice — "top 3 reviews by helpful_count" means sort per recipe, take the first 3.

include: {
  reviews: {
    orderBy: [
      { field: 'helpful_count', dir: 'desc' },
      { field: 'created_at',    dir: 'desc' },
    ],
    limit: 3,
  },
}

limit

Per-parent cap. The planner caps this at 1000; the relation's defaultIncludeLimit applies when the caller omits it.

include: {
  reviews: { limit: 5 },          // 5 reviews per recipe
  ingredientRows: true,           // uses declared defaultIncludeLimit
}

Across the read ops

Same include spec, four places it shows up:

query

Relations land directly on each row:

const { rows } = await beam.recipes.query({
  include: { reviews: true },
})
rows[0].reviews[0].rating   // typed

search / similar

Relations land on metadata:

const { results } = await beam.recipes.search({
  query: 'comforting pasta',
  include: { reviews: true },
})
results[0].metadata.reviews[0].rating   // typed

feed

Same place as search — on each item's metadata:

const page = await beam.recipes.feed.discover({
  context:  { liked_titles: ['Lasagna'] },
  include:  { reviews: { limit: 3 } },
})
page.items[0].metadata.reviews   // typed array

Streaming

stream.query and stream.search both accept include. Relations are attached per-row before the row yields — you never see a row without its relations:

for await (const row of beam.recipes.stream.query({
  where:   { cuisine: 'italian' },
  include: { reviews: true },
})) {
  row.reviews   // always present — resolved per batch
}

subscribe() and observe() do not support include — those stream single records as they change. If you need joined data there, re-fetch via query({ where: { id: event.recordId }, include }) on each event.

CLI

# semilayer query
semilayer query recipes.query \
  --where '{"cuisine":"italian"}' \
  --include '{"reviews":{"limit":3}}' \
  --limit 10

The --include flag takes JSON — same shape as the parameter.

Partial failures

When one relation fails (access denied, bridge down, missing capability), the primary rows still land. The failed relation returns empty on those rows, and meta.includeErrors names it:

const { rows, meta } = await beam.recipes.query({
  include: { reviews: true, ingredientRows: true },
})

if (meta.includeErrors) {
  // [{ relation: 'reviews', reason: 'bridge_unreachable' }]
  showPartialDataBanner(meta.includeErrors)
}

Failure reasons: unknown_relation, access_denied, capabilities.batchRead, source_query_failed, source_unreachable, query_timeout. See Access Rules and Bridges Without Support for the causes.

What include doesn't do

  • Nested includes — you can't include: { reviews: { include: { author: true } } }. Include is one level deep in v1. For depth, chain calls.
  • Parent-side filters on child values — "recipes with any 5-star review" can't be expressed in include. Do a query on the child lens first, then where: { id: { $in: ids } } on the parent.
  • Aggregatesavg(rating), count(reviews), etc. Aggregations are a separate primitive, not a flavour of include.
  • Subscribe / observe — see above.

Next: Cross-source joins.