Skip to content

RAGs (knowledge collections)

A RAG (Retrieval-Augmented Generation collection) is a named, tenant-managed bundle of markdown documents your app pulls into a prompt at runtime — launch hooks, brand voice, customer profiles, FAQ snippets, anything your agent needs as context that the tenant edits without redeploying your app.

Apps don’t see RAG IDs directly. They declare slots in the manifest; the tenant binds each slot to one of their RAGs at activation; the platform resolves slot → rag_id on every call.

The pattern shines whenever content that’s app-private today wants to be tenant-shared tomorrow: the same “Launch 2026” hooks can feed your press-planner, your office-assistant’s email drafts, and your social-manager — all pointing the same launch_corpus slot at the same RAG.

When to reach for a RAG (vs. alternatives)

Section titled “When to reach for a RAG (vs. alternatives)”
NeedUse
Personality + tone rules of the agentagent.system_prompt in the manifest
Static config baked at build timeenv vars
Tenant-tunable scalars (digest_time, language)manifest install_settings
App-private structured state (cursors, kv settings)ctx.kv / bridge.kv
Tenant-curated textual content the app readsRAG slot
Binary artifacts (PDFs, images, generated reports)ctx.files / linkworld_files
linkworld.app.yaml
apiVersion: linkworld.ai/v2
app_id: press-planner
version: 0.2.0
rag_slots:
- name: launch_corpus
description: |
Hooks, bio, competitive framing for the current launch.
Pulled into pitch-mail drafts. Without this the agent
falls back to a generic message.
required: true
scope: read # default; "read_write" for ingest apps
suggested_tags: # hint shown in the tenant's RAG editor
- hook_a
- hook_b
- hook_c
- bio
- proof
- name: outlet_research
description: "Optional: per-journalist research dossiers"
required: false
scope: read

Field rules:

FieldRequiredNotes
nameyes^[a-z][a-z0-9_]{0,31}$ — slot key apps pass to rag_query. Unique within the manifest.
descriptionyes1-280 chars. Shown in the activation consent dialog.
requirednoDefault false. Required slots block activation unless bound.
scopeno"read" (default) or "read_write". Only read_write bindings can call rag_upsert_document / rag_delete_document.
suggested_tagsnoUp to 20 tags, max 32 chars each. Hint only — tenants can tag freely.

Up to 10 slots per app. Validation runs both in the SDK (local) and on the platform at publish.

All tools live on the platform’s rag skill. Apps invoke them through the standard tool bridge — same surface as every other skill. The platform resolves slot → RAG using linkworld_app_rag_bindings; apps never pass a rag_id.

Retrieve up to top_k documents from a bound RAG. Filter by tags (any-match) and/or full-text-search keyword match.

const res = await bridge.tools.call('rag_query', {
slot: 'launch_corpus',
tags: ['hook_c'], // optional, OR-semantics
q: 'Mittelstand', // optional, FTS keyword filter
top_k: 5, // default 5, max 20
})
// res.documents: Array of doc shape; res.rag_id, res.rag_name

v1 retrieval is filter only — results are ordered by updated_at DESC, not by FTS rank. Prefer precise tags for highest signal; use q as a keyword fallback. v2 will add hybrid ranking (FTS + embedding cosine) behind the same surface; the score field on each doc is reserved for that.

Same shape as rag_query minus FTS, paginated. Used by app UIs that need to enumerate the corpus.

res = await ctx.tools.call(
"rag_list_documents",
slot="launch_corpus",
tag="hook_c", # optional single-tag filter
limit=100, # default 100, max 200
offset=0,
)

Single doc by ID or by (slot, key) tuple.

res = await ctx.tools.call(
"rag_get_document",
slot="launch_corpus",
key="hook_c", # or id="<uuid>"
)
# res.document.body has the full markdown

Create or update a document. Either set body inline or pass a source_file_id (a linkworld_assistant_artifacts ID) to have the platform extract the text server-side and store the snapshot.

# Inline body
await ctx.tools.call("rag_upsert_document",
slot="outlet_research",
key="pacher-article-2026-04",
title="Pacher — Brutkasten Apr 2026 KI-Übersicht",
body="...markdown...",
tags=["pacher", "april"])
# From a workspace file (PDF, markdown, HTML, JSON, plaintext)
await ctx.tools.call("rag_upsert_document",
slot="outlet_research",
key="pacher-article-2026-04",
title="Pacher — Brutkasten Apr 2026 KI-Übersicht",
source_file_id="<artifact_id>",
tags=["pacher", "april"])

Read-scope bindings get error: "scope_denied: ...".

Hard delete by ID or (slot, key). v1 keeps it simple — bring a versioning layer in your app if you need history.

res = await ctx.tools.call(
"rag_delete_document",
slot="outlet_research",
key="stale-article",
)
# res.deleted, res.deleted_count

Every tool call resolves (tenant_id, app_id, slot) to one rag_id via the binding row. Failure modes apps should handle:

Error stringMeaningFix
rag_unbound: ...Manifest declares the slot but the user never bound it (or unbound it later).Tell the user to open the app’s settings; platform UI also reflects this.
rag_orphaned_binding: ...The binding exists but the RAG was deleted underneath it.User must rebind in /user/knowledge → app settings.
scope_denied: ...App requested a write tool on a read-scope binding.Manifest needs scope: read_write on the slot, then re-activation.
fts_unavailable: ...Backend driver missing the FTS path. Operational issue, not your code.Report; meanwhile fall back to tag-only filtering.

Apps cannot enumerate RAGs they aren’t bound to. Brute-forcing slot names returns rag_unbound with no info leak.

A RAG document can be sourced from a tenant’s workspace file (linkworld_assistant_artifacts) instead of typed inline. The mental model is source + cache:

  • The artifact stays the canonical source — whatever the tenant sees in the Files tab IS the file.
  • The RAG doc’s body is a cached extracted-text snapshot bound to a source_snapshot_at timestamp.
  • The link is one-way: edits in the workspace file don’t auto-flow into the RAG cache; the tenant clicks “Aktualisieren” in the doc editor to re-extract.

Why a cache instead of pure-reference: FTS index needs the text on the row, PDF extraction is non-deterministic across library versions (pinning gives stable retrieval), and quota math (“how big is this RAG?”) must be one DB query.

Supported source types (extraction at upsert time):

Content typeExtractorNotes
text/markdown, text/plainraw textUTF-8
text/htmltags stripped, entities decodedminimal — good enough for snippet retrieval
application/jsonpretty-printedpreserves Unicode
application/pdfpypdftext-only; layout discarded

Other content types reject with unsupported_content_type — your app can either pre-extract and pass body directly, or surface the error to the user.

Source freshness in responses: rag_query / rag_list_documents / rag_get_document decorate workspace-file docs with a source_status field:

ValueMeaning
freshCache snapshot ≥ source’s updated_at — content is current.
staleSource has been modified after the snapshot. UI prompts a refresh; apps can surface this in their own UI too.
orphanedSource file was deleted; the cache body persists as if it were an inline doc.
nullDoc is source_type='inline' or 'app_generated' — nothing to compare.

When your manifest declares rag_slots, the activation consent dialog gets a “Knowledge-Bindings” section after the scope grants. Each slot renders as a row: pick an existing RAG from the dropdown, or inline-create a new one with a name + optional description.

Required slots block activation. Optional slots can stay unbound — the app will get rag_unbound at runtime for those slots, which is expected.

Tenants manage their RAGs in /user/knowledge — a full CRUD UI with markdown editing, tag filtering, and a workspace-file picker. Bindings are visible per RAG (“Eingebunden in: press-planner · launch_corpus”) so the tenant always knows which apps are reading which content.

Post-install rebinding: when your app’s manifest declares chrome.panel (the right-side panel with a settings tab), the tenant gets a “Knowledge” sub-tab inside Settings that lists each slot with its current binding and a picker to change it — same UX as the consent dialog, no re-activation needed. Optional slots can be unbound from there; required slots can be re-pointed but never left empty. The endpoint behind it is PATCH /api/apps/{app_id}/rag_bindings, validated through the same helpers as activation.

ResourceSoft capHard cap
RAGs per tenant50200
Documents per RAG5005000
Body size per doc256 KB (enforced)256 KB
Total bytes per tenant50 MB500 MB
Slots per manifest10

The 256 KB body cap is enforced at the DB level via a CHECK constraint — passing body over the limit errors at upsert. Pre- extracted text from large PDFs typically lands well under the cap (PDFs that extract to >256 KB are rare for the use cases RAGs target).

For the design rationale (why slots not RAG IDs, why source+cache, how scaling looks at 10k users), read the in-repo RFC: docs/architecture/rags_platform.md.