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.mdproject-beta / MORAYA.md
Both coexist. Same filename, separate namespaces, separate sync histories.
Anatomy of a knowledge base
| Field | Rules |
|---|---|
| name | Display label, 1–120 characters |
| slug | URL-friendly identifier, [a-z0-9-], 2–64 characters; auto-derived from name if omitted; immutable after creation |
| description | Optional free text, ≤ 500 characters |
| isDefault | One 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
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:
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.mdspecs/api-design.mdMORAYA.mdFiltering documents by KB
GET /v1/docs?kbId=kb_V1StGXR8_Z5jdHi6BAdditional filters:
prefix=notes/2026/— only documents whose path starts with this prefixincludeDeleted=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
GET /v1/kbs/{kbId}/manifest?limit=500Returns 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 state | Remote state | Action |
|---|---|---|
| New file | Not in manifest | Push (upsert) |
| Changed hash | Manifest has older hash | Push (upsert) |
| Unchanged hash | Match | Skip |
| Missing locally | In manifest | Decide: push delete or skip |
In manifest deletedAt != null | Soft-deleted remotely | Delete locally (if desired) |
3. Push changes via batch sync
POST /v1/kbs/{kbId}/syncContent-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
| Reason | Meaning | Suggested client action |
|---|---|---|
REMOTE_NEWER | Server’s updatedAt is newer than your baseUpdatedAt | Show conflict UI; let user choose keep-local or accept-remote |
REMOTE_DELETED | Document was deleted remotely; you tried to update | Accept remote deletion or force-push |
BASE_MISSING | You provided baseUpdatedAt but document doesn’t exist server-side | Treat as new upload (baseUpdatedAt: null) |
LOCAL_HASH_MISMATCH | sourceHash you sent doesn’t match SHA-256 of the content | Recalculate hash from content |
Reading a document by path
Retrieve raw content without knowing the document ID:
GET /v1/kbs/{kbId}/raw?path=notes/2026/04/meeting.mdReturns 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:
GET /v1/kbs/{kbId}/manifest?since=2026-04-29T10:05:00ZUse 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.mdEach 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.
Related
- KB API reference — full endpoint and schema documentation
- Moraya integration — automated KB sync via Moraya desktop app
- Hosting Markdown documents — single-document upload patterns