Skip to content

Multi-KB Workflow

A knowledge base (KB) is a named namespace inside your Picora account that groups related Markdown documents. Use multiple KBs when your content belongs to distinct projects or audiences that should not mix.

Why multiple knowledge bases

Without KBs all documents share a flat namespace. Two MORAYA.md files from different projects collide. A KB solves this:

  • project-alpha / MORAYA.md
  • project-beta / MORAYA.md

Both coexist. Same filename, separate namespaces, separate sync histories.

Anatomy of a knowledge base

FieldRules
nameDisplay label, 1–120 characters
slugURL-friendly identifier, [a-z0-9-], 2–64 characters; auto-derived from name if omitted; immutable after creation
descriptionOptional free text, ≤ 500 characters
isDefaultOne KB per account may be the default; new docs without an explicit kbId are attributed to it by clients that choose to

Creating a knowledge base

Via Picora Center

Go to Library → Knowledge Bases → New KB and fill in name, slug, and optional description.

Via API

Terminal window
curl -X POST https://api.picora.me/v1/kbs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"name": "Project Alpha", "slug": "project-alpha"}'

Response:

{
"success": true,
"data": {
"id": "kb_V1StGXR8_Z5jdHi6B",
"name": "Project Alpha",
"slug": "project-alpha",
"docCount": 0,
"sizeBytes": 0,
"isDefault": false,
"createdAt": "2026-04-29T10:00:00Z"
}
}

Uploading documents into a KB

Extend the standard POST /v1/docs body with kbId and relativePath:

Terminal window
curl -X POST https://api.picora.me/v1/docs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"filename": "MORAYA.md",
"content": "# Project Alpha\n...",
"kbId": "kb_V1StGXR8_Z5jdHi6B",
"relativePath": "MORAYA.md"
}'

relativePath mirrors the file’s position inside your local KB directory. Subdirectories are allowed:

notes/2026/04/meeting.md
specs/api-design.md
MORAYA.md

Filtering documents by KB

Terminal window
GET /v1/docs?kbId=kb_V1StGXR8_Z5jdHi6B

Additional filters:

  • prefix=notes/2026/ — only documents whose path starts with this prefix
  • includeDeleted=true — include soft-deleted entries (useful for sync clients)

To list documents with no KB (legacy flat uploads): kbId=__legacy__

Syncing a local directory to a KB

The sync workflow uses two endpoints:

1. Pull the manifest

Terminal window
GET /v1/kbs/{kbId}/manifest?limit=500

Returns lightweight metadata (no content) for every active document:

{
"items": [
{
"relativePath": "MORAYA.md",
"sourceHash": "a3f2c1...",
"sizeBytes": 4321,
"updatedAt": "2026-04-29T10:00:00Z",
"deletedAt": null
}
],
"nextCursor": null,
"serverTime": "2026-04-29T10:05:00Z"
}

Save serverTime — use it as since on the next incremental pull.

2. Compute the diff locally

Compare each local file’s content hash against sourceHash from the manifest:

Local stateRemote stateAction
New fileNot in manifestPush (upsert)
Changed hashManifest has older hashPush (upsert)
Unchanged hashMatchSkip
Missing locallyIn manifestDecide: push delete or skip
In manifest deletedAt != nullSoft-deleted remotelyDelete locally (if desired)

3. Push changes via batch sync

Terminal window
POST /v1/kbs/{kbId}/sync
Content-Type: application/json
{
"ops": [
{
"op": "upsert",
"relativePath": "notes/new.md",
"content": "# New Note\n...",
"sourceHash": "b4e5f6...",
"baseUpdatedAt": null
},
{
"op": "delete",
"relativePath": "old/obsolete.md",
"baseUpdatedAt": "2026-04-28T08:00:00Z"
}
]
}

The server processes each op independently. Conflicts are reported per-op without blocking the rest:

{
"applied": [
{ "relativePath": "notes/new.md", "status": "created" }
],
"conflicts": [
{
"relativePath": "old/obsolete.md",
"reason": "REMOTE_NEWER",
"serverUpdatedAt": "2026-04-29T09:00:00Z"
}
]
}

Up to 100 ops per request. For larger batches, split into multiple requests.

Conflict resolution

ReasonMeaningSuggested client action
REMOTE_NEWERServer’s updatedAt is newer than your baseUpdatedAtShow conflict UI; let user choose keep-local or accept-remote
REMOTE_DELETEDDocument was deleted remotely; you tried to updateAccept remote deletion or force-push
BASE_MISSINGYou provided baseUpdatedAt but document doesn’t exist server-sideTreat as new upload (baseUpdatedAt: null)
LOCAL_HASH_MISMATCHsourceHash you sent doesn’t match SHA-256 of the contentRecalculate hash from content

Reading a document by path

Retrieve raw content without knowing the document ID:

Terminal window
GET /v1/kbs/{kbId}/raw?path=notes/2026/04/meeting.md

Returns the raw Markdown body. Useful for scripting workflows that work on a known path.

Incremental sync

After the first full sync, use since to pull only changes:

Terminal window
GET /v1/kbs/{kbId}/manifest?since=2026-04-29T10:05:00Z

Use the serverTime from the previous manifest response as the since value — never use your local clock, which may drift.

Typical multi-KB setup

Picora account
├── KB: project-alpha (slug: project-alpha)
│ ├── MORAYA.md
│ ├── notes/...
│ └── specs/...
├── KB: project-beta (slug: project-beta)
│ ├── MORAYA.md ← same filename, no collision
│ └── roadmap.md
└── KB: team-wiki (slug: team-wiki, isDefault: true)
└── onboarding.md

Each KB is a fully independent namespace. Slugs are unique per account and cannot be changed after creation.

Moraya integration

Moraya v0.35.0+ handles the full sync lifecycle automatically — manifest pull, diff, batch push, conflict UI — for each local KB you bind to a Picora KB. See Moraya integration for configuration steps.