Skip to main content

Knowledge Pages

Living markdown documents, organized in a folder tree, that rewrite themselves as the bank learns.

A page is backed by a mental model but is configured as a document: it is built from the bank's observations only, it never reads other pages, and it refreshes incrementally after each consolidation. See Knowledge Pages for the concepts behind the API.

All endpoints below are relative to a bank:

/v1/default/banks/{bank_id}/knowledge-base

Get the Tree

Returns the whole knowledge base as a nested tree of folders and pages. Page bodies are not included — fetch a page to read its content.

# Fetch the whole knowledge base as a nested folder/page tree (no page bodies)
tree = client.get_knowledge_base_tree(BANK_ID)

for root in tree.roots:
print(f"{root.kind}: {root.name}")
for child in root.children:
print(f" {child.kind}: {child.name} (stale: {child.is_stale})")
{
"roots": [
{
"id": "kf-9f2c...",
"kind": "folder",
"name": "Operations",
"parent_id": null,
"mental_model_id": null,
"managed": false,
"description": null,
"tags": [],
"timestamp": "2026-08-01T11:04:02+00:00",
"is_stale": null,
"children": [
{
"id": "kp-2e85...",
"kind": "page",
"name": "Deploying the API",
"parent_id": "kf-9f2c...",
"mental_model_id": "mm-77ab...",
"managed": false,
"description": "How is the API deployed?",
"tags": ["ops"],
"timestamp": "2026-08-03T09:12:44+00:00",
"is_stale": true,
"children": []
}
]
}
]
}
FieldDescription
kindfolder or page
mental_model_idThe backing mental model (pages only)
descriptionThe page's source query — the question that rebuilds it
timestampLast refresh for a page, last update for a folder
is_stalePages only: true when a memory in this page's scope has been written since the page last read the memories (see below)
managedtrue when the node is flagged as system-owned rather than hand-authored

How is_stale is decided

Each page is answered against its own scope — its tags and its fact_types — using the same staleness check that decides whether a scheduled refresh does any work. A flagged page is one a refresh would actually rewrite; an unflagged one is a page a refresh would leave alone. Activity elsewhere in the bank does not flag a page whose own scope is quiet.

The whole tree is answered in one query, so the flag costs the same whether the bank has three pages or three hundred, and GET /mental-models/{id} returns the identical value for the page's backing model.

One thing it does not see: deletions. The check asks what has been written since the page last read the memories, and deleting an in-scope memory leaves no write behind — a page that cites a deleted fact keeps reporting itself up to date.


Create a Page

Creating a page stores it with placeholder content and schedules the first build in the background. Poll the returned operation_id via the operations API to know when the content is ready.

# Create a page — content is generated in the background
page = client.create_knowledge_page(
BANK_ID,
name="Deploying the API",
source_query="How is the API deployed?",
parent_id=folder.id,
tags=["ops", "type:runbook"],
)

# Poll the operation to know when the first build has finished
print(f"Page ID: {page.page_id}, operation: {page.operation_id}")
{
"page_id": "kp-2e85...",
"mental_model_id": "mm-77ab...",
"operation_id": "op-1d0f..."
}

Parameters

ParameterTypeRequiredDescription
namestringYesPage name. Must be unique within its folder, case-insensitively — a duplicate returns 409. (Enforced on PostgreSQL only.)
source_querystringYesThe question the page answers, re-asked on every refresh.
parent_idstringNoFolder to create the page in. null (or omitted) creates it at the root.
tagslistNoTags that scope which memories the page is built from — see Tags Are a Filter. A type:<x> tag also sets the page's rendered type, and still counts as part of the filter.
max_tokensintNoContent budget. Defaults to 4096 (a plain mental model defaults to 2048).
triggerobjectNoRefresh configuration — see below.

Tags Are a Filter

tags are not labels on the page. They are the scope the page is built from, and a tagged page matches memories with all_strict by default: a memory must carry every one of the page's tags, and untagged memories are excluded entirely.

So a page created like this:

{
"name": "Homelab Infrastructure",
"source_query": "NAS, ThinkPad, docker containers, jellyfin",
"tags": ["type:runbook", "homelab", "infrastructure"]
}

is built only from memories tagged type:runbook and homelab and infrastructure. If your memories were retained without those exact tags — which is the usual case when the tags are invented at page-creation time to describe the topic — the page matches nothing and generates as "I don't have information about this." A direct recall for the same query still returns everything, because recall was not given the same filter.

The type:<x> tag makes this easy to trip over: it is documented as setting the page's rendered type, but it narrows retrieval like any other tag.

Three ways to get this right:

You wantDo this
The page built from the whole bankOmit tags (or pass [])
The tags to scope the page, but untagged memories still includedKeep tags, add "trigger": {"tags_match": "all"}
Only memories carrying every tag (strict isolation, e.g. per-user pages)Keep tags and the all_strict default

To repair a page that already generated empty, PATCH it with {"tags": []} or with the widened tags_match, then refresh it — the tags are stored on the backing mental model, not baked into the content.

See tag matching modes for the full semantics of any, all, any_strict, all_strict, and exact.

Default Trigger

When trigger is omitted, the page is created with a document-oriented configuration:

{
"mode": "delta",
"fact_types": ["observation"],
"exclude_mental_models": true,
"refresh_after_consolidation": true
}

This makes the page a living document built from consolidated observations only, refreshed incrementally whenever consolidation produces new knowledge in its scope, and never influenced by other pages.

SettingWhy
fact_types: ["observation"]The page reads consolidated beliefs, not the raw conversational noise underneath them. Observations are already deduplicated and evidence-backed, so a page reads as a settled document instead of a transcript. Enforced structurally — with only observation in scope, the refresh agent isn't given the raw-memory recall tool at all.
exclude_mental_models: trueA page never reflects on sibling pages. Without this, pages would cite each other and drift into a feedback loop where one wrong claim propagates across the knowledge base.
mode: "delta"Each refresh edits the existing document with what is new since the last refresh instead of regenerating it, so hand-tuned structure and wording survive. See Refresh Mode.
refresh_after_consolidation: trueThe page rewrites itself whenever consolidation produces new knowledge in its scope — gated by the same staleness check as any mental model, so unrelated bank activity doesn't trigger rebuilds.

Page Lifecycle

  1. Create — the page is stored with placeholder content and a background refresh is submitted; the call returns immediately with an operation_id.
  2. First build — a full generation, since there is no prior document to edit.
  3. Consolidation — new memories are retained and consolidated into observations.
  4. Staleness check — pages whose trigger asks for it are checked against their own scope (tags and fact_types both apply).
  5. Delta refresh — stale pages are rewritten by editing the existing document with the new observations only.

Observations are what a page is built from, but it can still inspect the evidence underneath them: the refresh agent can expand a memory to its original chunk or document (unless store_document_text is disabled for the bank), and if HINDSIGHT_API_REFLECT_SOURCE_FACTS_MAX_TOKENS is enabled — off by default — observation search also returns each observation's grounding facts.

info

A supplied trigger is a patch: only the fields you actually send are applied, and the rest keep the defaults above. Sending {"trigger": {"tags_match": "all"}} widens the tag filter and leaves mode, fact_types, exclude_mental_models, and refresh_after_consolidation as they are. The one exception is the two refresh triggers, which stay mutually exclusive: setting refresh_cron clears refresh_after_consolidation, and vice versa.

Every mental model trigger setting is accepted here — including refresh_cron for scheduled rebuilds instead of consolidation-driven ones, and tag_groups for compound tag scoping.


Create a Folder

# Create a folder (omit parent_id, or pass None, to create it at the root)
folder = client.create_knowledge_folder(BANK_ID, name="Operations")

print(f"Folder ID: {folder.id}")
ParameterTypeRequiredDescription
namestringYesFolder name
parent_idstringNoParent folder. Omit or pass null for the root.

A parent_id that does not exist, or that points at a page rather than a folder, returns 400.


Read a Page

Returns the page rendered as a markdown document.

# Read a page as a markdown document
document = client.get_knowledge_page(BANK_ID, page.page_id)

print(document.type) # "runbook" — from the type:runbook tag
print(document.body) # the synthesized markdown body
print(document.markdown) # YAML frontmatter + body
{
"id": "kp-2e85...",
"name": "Deploying the API",
"type": "runbook",
"description": "How is the API deployed?",
"tags": ["ops"],
"timestamp": "2026-08-03T09:12:44+00:00",
"body": "# Deploying the API\n\n...",
"markdown": "---\nid: \"kp-2e85...\"\ntype: \"runbook\"\n...\n---\n\n# Deploying the API\n\n..."
}
  • body is the synthesized markdown body on its own.
  • markdown is the full document: a YAML frontmatter block (id, type, title, description, tags, timestamp) followed by the body.
  • type comes from a type:<x> tag and defaults to knowledge-page. The type: tag is removed from the returned tags.

Search Pages

Document-level hybrid search: a full-text (BM25) match and a vector-similarity match, fused with Reciprocal Rank Fusion. There is no reranking step, which keeps it fast enough to be an agent's first call.

# Hybrid search (full-text + vector) over whole pages
results = client.search_knowledge_base(BANK_ID, q="how do we deploy", limit=5)

for hit in results.results:
print(f"{hit.score:.3f} {hit.name}: {hit.snippet}")
{
"results": [
{
"id": "kp-2e85...",
"name": "Deploying the API",
"mental_model_id": "mm-77ab...",
"snippet": "The API is deployed via ...",
"score": 0.032,
"updated_at": "2026-08-03T09:12:44+00:00"
}
],
"total": 1
}
ParameterTypeDefaultDescription
qstringRequired. Search query (min length 1).
limitint10Maximum results, 1–50.

This searches whole pages. To search individual memories, use recall.


Update or Move a Node

One PATCH renames a node, moves it, and/or updates a page's options. Each field applies only when present in the body.

# Rename a node, move it, and/or update a page's options.
# Changing source_query rebuilds the page against the new question.
client.update_knowledge_node(
BANK_ID,
page.page_id,
name="Deploying the API (v2)",
tags=["ops", "type:runbook", "reviewed"],
)
ParameterTypeApplies toDescription
namestringBothNew name
parent_idstring | nullBothNew parent folder. Pass null explicitly to move to the root.
source_querystringPagesNew question. Changing it schedules an async refresh so the page rebuilds against the new question.
tagslistPagesReplaces the page's tags, and so the scope it is built from. Pass [] to clear them and rebuild the page from the whole bank.
max_tokensintPagesNew content budget
triggerobjectPagesRefresh settings to change, applied as a patch. {"tags_match": "all"} keeps the page's tags but stops excluding untagged memories.

Sending an empty body returns 400; an unknown node returns 404.


Delete a Node

Deletes a folder or page and its entire subtree. The mental models backing the deleted pages are removed too.

# Delete a folder or page — deleting a folder removes its whole subtree
client.delete_knowledge_node(BANK_ID, folder.id)
{"status": "deleted"}

Export as a Markdown Bundle

Returns the whole knowledge base as a flat set of portable markdown files: a nested index.md, one <page-id>.md per page, and a <page-id>.log.md refresh history for pages that have been rebuilt.

# Export the knowledge base as a portable markdown bundle
bundle = client.export_knowledge_base(BANK_ID)

for file in bundle.files:
print(file.path) # index.md, <page-id>.md, <page-id>.log.md
{
"files": [
{"path": "index.md", "content": "---\ntype: \"index\"\n..."},
{"path": "kp-2e85....md", "content": "---\nid: \"kp-2e85...\"\n..."},
{"path": "kp-2e85....log.md", "content": "---\ntype: \"log\"\n..."}
]
}
Mirror it to disk

hindsight fs mount --bank my-bank keeps a local folder in sync with this bundle via a background refresh loop, so ls, grep, rg, and your editor work against real files.


Storage

TableHolds
knowledge_pagesThe tree: folders and pages, their names, parents, and ordering. A page row references its backing mental model; a folder row has none.
mental_modelsThe content: the document body, its source query, tags, token budget, trigger, and refresh history.

The page layer owns only tree structure — everything about the content lives on the backing mental model, which is why every mental model capability applies to pages unchanged.


Endpoint Summary

MethodPathDescription
GET/knowledge-base/treeNested folder/page tree with staleness
POST/knowledge-base/foldersCreate a folder
POST/knowledge-base/pagesCreate a page (async first build)
GET/knowledge-base/pages/{page_id}Read a page as markdown
GET/knowledge-base/searchHybrid search over pages
PATCH/knowledge-base/nodes/{node_id}Rename, move, or reconfigure a node
DELETE/knowledge-base/nodes/{node_id}Delete a node and its subtree
GET/knowledge-base/exportExport the whole base as markdown files