--- title: Knowledge API description: How a gem reads and writes the user's knowledge through ctx.knowledge — concepts, permission strings, the full API, write rules, and uninstall behavior. section: dev order: 45 updated: 2026-11-01 translation_of: zh source_sha: c978d1463d1e --- # Knowledge API The user's long-term knowledge lives in the knowledge base: **facts** about the user and the people they know, and **documents** such as diaries and keepsakes. A gem accesses it through `ctx.knowledge`, and how much it can access depends entirely on which permissions the user has turned on in the gem's page — none at all when it is first installed. A few boundaries to know first: - **What is written into knowledge belongs to the user, not to the gem.** When a gem is uninstalled, its private storage is deleted completely; the records it wrote into knowledge are kept by default, and the user can choose to delete them as well at uninstall. - **You never get internal ids.** All entities, facts, documents, and collections appear as opaque `ref`s, and different gems get different refs. - **Email is not in knowledge.** To read email, request the `email` capability (`ctx.email`); knowledge stores only the background information distilled from email. - **One set of data per organization.** Personal data and each organization's data are stored physically apart; a gem sees only the one it lives in, and there is no cross-organization access. ## Concepts | Concept | Description | | --- | --- | | Entity | A person, an organization, a place… and the user themselves (`self`). An entity has a `kind` (`person`, `org`, `place`, etc.), a display name, and identifiers (email, phone, domain, URL, social account). | | Fact | "Subject · predicate · value", for example `self · pref.food.dislike · cilantro`. The value can be any JSON; it can also point to another entity (for example a colleague relationship). A fact can expire (replaced by a new value or forgotten) but is never physically deleted. | | Predicate | The type of a fact, dot-separated with at least two levels, for example `location.home.city`. The first level is fixed to: `identity`, `contact`, `location`, `relation`, `pref`, `schedule`, `work`, `health`, `finance`, `x` (other). Below that it is not fixed: the app has a set of common predicates built in (see [Core predicates](#core-predicates)), and users and gems can add more under any level. | | Collection | A container for documents and also the unit of authorization. `diary` (the diary, one entry per day) and `keep` (keepsakes) are built in; users and gems can also create new ones. Collections can be nested (a folder inside a folder, except under `diary`); a document belongs to exactly one collection. | | Document | A title + body (Markdown) + optional `meta`. After it is written, it is automatically chunked, full-text and semantic indexes are built, and the contacts it mentions are recognized. | ## Requesting in the manifest In a Gem Studio draft `manifest.json`, knowledge permissions are a gem-level field, and every tool can use them by default (if a tool does not need them, leave `knowledge` out of its `permissions`). You can also add them one by one in the "Knowledge" section of the Gem Studio overview; mistakes are listed right away, the same as the checks at install time: ```json { "name": "recipes", "description": "Save recipes the user has rated", "kind": "tool", "entry": "tools/rate.js", "knowledge": { "permissions": ["facts.read:self:pref.food.*", "facts.write:self:pref.food.*", "collections.create"], "predicates": [ { "predicate": "pref.food.rated", "cardinality": "many", "description": "Dishes you have rated highly, used to recommend similar dishes. Example: braised pork", "aliases": ["top-rated dishes"] } ], "reason": "Remember the dishes you like, and save recipes into your knowledge base" } } ``` In the packaged `notochord.json`, the same content is written under `permissions.knowledge`, and a tool that needs `ctx.knowledge` writes `"knowledge": true` in its own `permissions` (Studio does this automatically when it generates the manifest): ```json { "permissions": { "knowledge": { "permissions": ["facts.read:self:location.home.*"] } }, "tools": [{ "name": "home", "entry": "tools/home.mjs", "permissions": { "knowledge": true }, "...": "..." }] } ``` - `permissions`: a list of permission strings, see the next section. - `predicates`: predicates this gem adds when none of the [core predicates](#core-predicates) fit; they are written into the global vocabulary at install time. They are **not permissions**, only a declaration of vocabulary. `cardinality` is `one` (only one value at a time, for example the city of residence) or `many` (there can be several values, for example favorite dishes). - `description` is **required**, at most 200 characters, and is **written for the user**: what it is, why the gem needs it, and an example. The gem's page shows it next to the user's value, and the user fills in or changes the value there. - `reason`: optional, shown on the install screen. ## Permission strings The format is always `data type.action:scope`. The gem's page ("Settings › Knowledge") turns each one into a sentence for the user, **all off by default**, and the user turns them on one by one; turning one on or off takes effect on the next call. A call that needs a permission that is not on is refused. | Permission | Shown on the install screen as | Description | | --- | --- | --- | | `facts.read:<subject>:<prefix>` | Read where you live | The subject is `self` or an entity kind (such as `person`). The prefix is `a.b.*` (all predicates under that prefix) or a full predicate `a.b.c`. **The prefix must have at least two levels**; `self:*` and `self:location.*` are both refused. | | `facts.write:<subject>:<prefix>` | Record your food preferences | Must be covered by some `facts.read`. | | `entities.resolve:<kind>` | Look up a contact's name by email and the like | A point lookup: exchange a known email or name for a ref and display name; it cannot list everything. | | `entities.read:<kind>` | View your contact list | Lists all entities of that kind and their identifiers. | | `entities.write:<kind>` | Add and edit contacts | Create entities and add identifiers. Includes `entities.resolve:<kind>`. | | `collections.read:<target>` / `collections.write:<target>` | Read your diary | The target is `diary`, `keep`, or `user-selected` (after installing, the user ticks specific collections in the gem's page). A grant on a collection includes all its subfolders, including ones created later. | | `collections.create` | Create its own collections | A newly created collection is automatically readable and writable by its creator, and so are the subfolders inside it. To create one inside another collection (`parent`), write permission on that collection is also required. | | `changes.manage` | View and undo all knowledge written by apps | Can view, undo, and modify records written by other writers (still limited to the scope authorized above). | | `origin.override` | Write data on behalf of other apps | When writing, it can specify `origin` and `source` itself; for "organizer" gems. | Request as narrowly as possible: to read only the city needed for weather, request only `facts.read:self:location.home.*`. Distinguish the two ways of failing: - **Query methods** (`profile.get`, `facts.lookup`, `search`, `collections.list`, `changes.list`) **do not raise an error** for parts that are not authorized; they just don't return them. You cannot tell from this whether a given piece of data exists. - **Methods that target a single object** (writes, reads by ref, `entities.*`, the collection named in `documents.list`, creation) **throw directly** when the permission is missing, and the error message contains `lacks knowledge permission <the missing permission>`. ## API reference All methods are asynchronous. The `collection` parameter can be the built-in name `'diary'` or `'keep'`, or a ref returned by `collections.list()` / `collections.create()`. ### Reading facts ```js await ctx.knowledge.profile.get('location.home') // → [{ predicate: 'location.home.city', value: 'Utrecht' }] ``` The standing profile: the facts about the user that are pinned, intersected with your `facts.read` permissions. The prefix can be omitted. ```js await ctx.knowledge.facts.lookup('self', 'pref.food') // or lookup(entityRef, 'relation') // → [{ ref, predicate, value, object: entityRef | null, validFrom }] ``` The facts currently valid for a subject. Requires `facts.read` for the corresponding subject kind. It **does not return** the source, the writer, or confidence, and it does not return facts the system inferred. ### Writing facts ```js await ctx.knowledge.facts.put({ subject: 'self', // defaults to 'self'; can also be an entity ref predicateHint: 'pref.food.rated',// best to write the full predicate; an alias or a one-line description is also accepted description: 'Dishes the user rated highly', // optional, useful for a new predicate value: 'Tomato beef stew', object: entityRef, // optional: the value points to another entity sourceRef: 'record/123', // optional: points back to a record in your own storage runId: 'nightly-2026-09-29' // optional: shared by a batch of writes, so the whole batch can be undone }) // → { status: 'applied' | 'duplicate' | 'conflict', ref, predicate } ``` - **The predicate is resolved first, then the permission is checked.** `predicateHint` is first normalized to a predicate in the vocabulary by "full predicate → alias → description", and `facts.write` is then checked against the normalized result. A vague hint may land under a prefix you have no permission for and be refused; if nothing matches at all, it lands under `x.<hint>`, which also needs permission. So use the full predicate name directly. - `duplicate`: the same subject, predicate, and value already exist. - `conflict`: a `cardinality: one` predicate already has a value **the user said themselves**; your value is not written and the original value is unchanged; the user sees this conflict in "View changes". - `cardinality: one` and the original value was not said by the user: the old value expires automatically and the new value takes effect. - `origin` and `source` are filled in by the system by default (`gem:<your id>` / `extracted`); specifying them yourself requires `origin.override`. ```js await ctx.knowledge.facts.retract(factRef) // → { status: 'retracted' | 'already_retracted' } ``` Makes a fact expire (no physical deletion). You can retract only what you wrote; retracting what someone else wrote requires `changes.manage`. ### Entities ```js await ctx.knowledge.entities.resolve({ email: 'zhangsan@acme.nl' }) // kind defaults to 'person' await ctx.knowledge.entities.resolve({ name: 'Acme' }, { kind: 'org' }) await ctx.knowledge.entities.resolve({ email, name }, { create: true }) // creates one if none is found; requires entities.write // → { ref, kind, displayName, created } | null ``` Available identifiers: `email`, `phone`, `domain`, `url`, `handle` (written as `platform:account`, for example `github:zhangsan`), and `name`. ```js await ctx.knowledge.entities.get(ref) // → { ref, kind, displayName, identifiers }, requires entities.read await ctx.knowledge.entities.list('person', { query }) // requires entities.read await ctx.knowledge.entities.addIdentifier(ref, 'phone', '+31 6 1234 5678') // → { status: 'added' | 'exists' }, requires entities.write ``` Entities can be deleted or merged only by the user (on the "Knowledge" page) or the assistant, not by a gem. If you store an entity ref in your own storage, expect it to go stale: - **Merged**: the old ref still works and automatically points to the entity that was kept. - **Deleted**: calling with this ref again throws a "not found" error (the message contains `no entity`). Discard the ref, and if needed look it up again with `entities.resolve`. ### Search and documents ```js await ctx.knowledge.search('tomato beef stew', { collections: ['diary'], timeRange: { from: '2026-09-22', to: '2026-09-22' }, limit: 10 }) // → [{ document: ref, title, collection: ref, text, locator, meta }] ``` Hybrid full-text + semantic search, only within collections you can read. A collection named in `collections` is searched together with all its subfolders. `timeRange` accepts milliseconds or ISO dates (a date alone means the whole day) and filters diary entries by `meta.date`. `query` can be empty, in which case results are listed in reverse chronological order. `entities: [entityRef]` returns only passages that mention those people. ```js await ctx.knowledge.documents.upsert({ collection: 'diary', key: '2026-09-29', // with a key: writing the same key again overwrites and does not produce a duplicate document title: '2026-09-29', body: '# Today\n\n…', meta: { date: '2026-09-29' } }) // → { status: 'created' | 'updated' | 'unchanged', ref } ``` - Passing `key` gives a deterministic document, suited to idempotent writes such as "one per day" or "one per record"; when the content has not changed it returns `unchanged` and does not rebuild the index. - Pass `ref` to update an existing document; pass neither to create a new one. - `text` + `sourceUri` is for imported external content: only the index is built, and the body is not stored. `sourceUri` is the URL where the original lives and the user can click it open, so it can only be an `http(s)://` URL, and its host must be in your [`permissions.network.hosts`](/en/dev/permissions.md) (a gem that has not requested network access cannot write `sourceUri`); other addresses (`file:` and so on) are refused. - Overwriting or deleting a document someone else wrote requires `changes.manage`. ```js await ctx.knowledge.documents.get(ref, { range: { from: 0, to: 3 } }) // the full text, or a fragment by chunk await ctx.knowledge.documents.getByKey('diary', '2026-09-29') // → document | null await ctx.knowledge.documents.list('diary', { timeRange: { from: '2026-09-01' } }) await ctx.knowledge.documents.delete(ref) ``` ### Collections ```js await ctx.knowledge.collections.list() // → [{ ref, name, builtin: 'diary' | 'keep' | null, kind, canWrite, parent }], lists only those you can read or write await ctx.knowledge.collections.create('Recipes') // → { ref, name, parent: null }, requires collections.create await ctx.knowledge.collections.create('Sichuan', { parent: ref }) // creates it inside the collection ref; write permission on it is also required ``` - Collections can be nested. `list()` returns a flat list, and `parent` is the ref of the collection it is in; for top-level ones it is `null`. A parent you cannot see (when only one of its subfolders was granted) is also `null`. - `parent` can be `'keep'` or a collection ref; nothing can be created inside `'diary'`. At most 5 levels (the top level counts as level 1); creating deeper raises an error. - `documents.list(collection)` lists only the documents placed directly in that collection, not those in subfolders; to search subfolders too, use `search`. - **The user can move collections and documents** (in the app's knowledge page; a gem cannot move them). What you can read and write follows location: content moved into a collection you have permission on becomes readable to you from then on; content moved out becomes unreadable. Moving does not change refs. Documents written with a `key` (the diary, documents you manage by key) cannot be moved by the user, so `getByKey` and writing by key always find them in the original collection. ### Changes and undo ```js await ctx.knowledge.changes.list({ since: '2026-09-28', status: 'conflict' }) // → [{ ref, runId, actor, op, summary, status, proposed, createdAt }] await ctx.knowledge.changes.revert({ runId: 'nightly-2026-09-29' }) // or revert(changeRef) ``` Without `changes.manage` you can see and undo only your own changes. Every write is recorded as a change; if you do not pass `runId`, the write is attributed to the `runId` of the current run, and you can also pass one yourself to group a batch of writes together. ## Write rules - **Each gem can make at most 500 writes per day** (by local date), and the text written totals at most 50 million characters per day (200 million for gems with `collections.bulk`). This is a system safeguard and the user cannot adjust it. Beyond it, writes throw an error containing `quota`. - **Limits on a single write**: a document body is at most 5 million characters, a title at most 1000 characters, and `meta` and a fact's value are each at most 64 KB when serialized as JSON. An entity's display name, an identifier's value, and a collection's name are also limited to 1000 characters, like a title. - **All writes can be undone**, and the user can undo them one at a time or by batch in "View changes". - **Don't use knowledge as your database.** Run state, caches, and drafts go in the gem's own data ([`ctx.data`](/en/dev/storage.md)); write into knowledge only content that is of long-term value to the user and still meaningful outside your gem. ## Uninstall and data ownership When the user uninstalls a gem: 1. A confirmation dialog shows how many facts, documents, and collections you contributed, and the user chooses "Keep" (the default) or "Delete them too". 2. Your private storage and authorization records are deleted. 3. Keep: the knowledge stays as it is, and the source marking still points to you. Delete: the documents you wrote are deleted, the facts expire, and the collections you created are deleted together with everything in them (including what the user later put or moved in); the user can also undo this step. 4. The predicates you declared stay in the vocabulary, because existing facts may still reference them. After reinstalling, you do not get your earlier private data back; but with read permission, you can read through `ctx.knowledge` what was kept before. ## Core predicates Common predicates are built in. Prefer them — the user may already have filled them in, and the gem's page shows them under the app's own names — and declare a new one in the manifest only when none fit. Here are all the built-in predicates: | Predicate | Cardinality | Meaning | | --- | --- | --- | | `identity.name` / `identity.nickname` / `identity.birthday` / `identity.language` | one / many / one / many | Name, forms of address, birthday, languages spoken | | `contact.email` / `contact.phone` / `contact.address` | many | Contact details | | `location.home.city` / `location.home.address` / `location.work.city` / `location.timezone` | one | City of residence, home address, work city, time zone | | `relation.family` / `relation.spouse` / `relation.friend` / `relation.colleague` / `relation.member_of` | many (spouse is one) | Relationships between people; the value usually points to another entity with `object` | | `work.company` / `work.title` / `work.project` | one / one / many | Company, job title, projects | | `pref.food.like` / `pref.food.dislike` / `pref.food.diet` / `pref.food.cuisine` | many | Food preferences | | `pref.communication.style` / `pref.communication.language` | one | Communication style, reply language | | `schedule.routine` / `schedule.event` | many | Daily routine, schedule | | `health.allergy` / `health.condition` | many | Allergies, health conditions | | `finance.currency` | one | Usual currency | ## What the user sees On the gem's page under "Settings › Knowledge", each permission has a switch, off by default. Below it are the facts about the user themselves that your `facts.read|write:self:…` covers, along with the user's current values: - Built-in predicates are shown under the app's own names (for example "City of residence"); - A predicate you added shows the `description` you wrote, labeled "Added by this gem"; - The user can edit, fill in, or delete directly. A value written this way counts as said by the user: on a `cardinality: one` predicate, your writes do not overwrite it (`conflict`). Facts about contacts (`facts.read:person:…`) are kept one per person and edited on the contacts page in the Vault; they are not listed here. ## Examples A read-only weather gem: ```js // Permission: facts.read:self:location.home.* export default async function (input, ctx) { const [home] = await ctx.knowledge.profile.get('location.home') if (!home) return { error: 'I do not know where you live' } return { city: home.value } } ``` A read-write recipe gem: ```js // Permissions: facts.read:self:pref.food.*, facts.write:self:pref.food.*, collections.create export default async function (input, ctx) { const cols = await ctx.knowledge.collections.list() const col = cols.find((c) => c.name === 'Recipes') ?? (await ctx.knowledge.collections.create('Recipes')) const fact = await ctx.knowledge.facts.put({ predicateHint: 'pref.food.rated', value: input.dish, sourceRef: `record/${input.id}` }) const doc = await ctx.knowledge.documents.upsert({ collection: col.ref, key: input.id, title: input.dish, body: input.recipe }) return { fact: fact.status, doc: doc.status } } ``` ## Not supported yet - Changes in knowledge are not announced to gems (this version publishes no knowledge events, see [Events and Triggers](/en/dev/events.md#available-events)). To notice changes, use a scheduled trigger to read; each item of `documents.list` carries `updatedAt`. - Reading another gem's private storage: will not be opened. Information to be shared between gems goes into knowledge.
Notochord
简体中文English