REST API
A small, deliberately narrow API for reading and editing maps and tasks from your own code. It is the same surface the MCP server is built on.
Looking for a worked example?
Connecting Google Sheets is the whole recipe step by step — code for Apps Script and for n8n, conflict handling (409) included.
- Works for the cloud and self-hosted alike — a hosted instance (
yourcompany.killbottleneck.com) exposes the same API as one on your server; only the address differs - Base path:
/api/kb/v1— the older/api/flowmap/v1prefix still resolves to the same handlers - Authentication: an API key in the
Authorizationheader. Never a session cookie or JWT - Format: JSON in, JSON out
- Language: English only, by design
Authentication
Create a key in the application, under your user menu. The plaintext token is shown once — only its SHA-256 hash is stored, so a lost key cannot be recovered, only rotated.
Authorization: Bearer kb_user_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXKeys issued before the rename start with fm_user_ and keep working.
Scopes
| Scope | May call |
|---|---|
read | every GET endpoint |
read_write | everything, including all POST endpoints |
A read key calling a write endpoint gets 403. The default when creating a key is read.
What a key can never do
- The owner comes from the key, never from the request body. A key only reaches maps and tasks belonging to the account that created it.
- A key cannot escalate. The server never establishes a session for it and never reads the account's role, so an API key cannot do administrator things.
- Someone else's map id returns 404, not 403 — the API does not confirm that an id exists.
Limits
| Limit | Value |
|---|---|
Requests per minute, read | 120 per key |
Requests per minute, read_write | 30 per key |
| Request body | 2 MB |
| Items per call (nodes) | 200 |
| Keys per account | 20 |
Read and write are counted in separate buckets, so bulk reading cannot starve your writes. Over the limit you get 429.
Optimistic locking
Every endpoint that changes an existing map requires base_updated — the updated value you got when you last read the map.
GET /api/kb/v1/maps/{id} → { "updated": "2026-07-30 08:12:44.031Z", … }
POST /api/kb/v1/maps/{id}/nodes ← { "base_updated": "2026-07-30 08:12:44.031Z", … }Omit it and you get 400. Send a stale one and you get 409 — someone changed the map in the meantime. Re-read the map, re-apply your change, try again.
This is deliberate: it makes "write without reading first" impossible, which is exactly the mistake an over-eager AI assistant makes.
Endpoints
GET /api/kb/v1/maps
Lists maps owned by the key's account.
| Query | Meaning |
|---|---|
archived=1 | list archived maps instead of active ones |
{
"maps": [
{ "id": "abc123", "title": "Website relaunch", "node_count": 14, "updated": "2026-07-30 08:12:44.031Z" }
]
}GET /api/kb/v1/maps/{id}
One map as a tree. No canvas positions — the shape, not the drawing.
{
"id": "abc123",
"title": "Website relaunch",
"description": "",
"archived": false,
"updated": "2026-07-30 08:12:44.031Z",
"tree": [ { "id": "n_1", "title": "Content", "status": "todo", "children": [] } ],
"notes": []
}Keep the updated value — you need it as base_updated for any write.
POST /api/kb/v1/maps
Creates a map from an outline. Requires read_write.
| Field | Required | Meaning |
|---|---|---|
title | yes | Map title, trimmed to 200 characters |
tree | no | Array of items (see below); max 200 nodes in total |
description | no | Free text |
apex_text | no | Text of the root goal; defaults to title |
Each item in tree may carry title, description, deadline, owner, status, wait_for_children and children. Layout is computed for you.
Returns { id, title, updated, tree }.
POST /api/kb/v1/maps/{id}/nodes
Adds a subtree. Requires read_write.
| Field | Required | Meaning |
|---|---|---|
base_updated | yes | Version you read (see optimistic locking) |
items | yes | Array of items, at least one, max 200 |
parent_id | no | Where to attach; omitted = under the apex |
Returns { updated, added_ids, tree }.
This re-computes the layout of the whole map
Adding nodes rearranges positions across the map. It is not a surgical insert.
POST /api/kb/v1/maps/{id}/nodes/{nodeId}
Updates one node. Requires read_write and base_updated.
Settable fields: title, status (todo / in_progress / done), description, deadline (YYYY-MM-DD, empty string clears), owner (e-mail, empty string clears), wait_for_children, color, executor_kind, executor_name, automation_wanted, automation_note.
Setting status to done has side effects on purpose: it may unblock waiting nodes, notify their owners, and trigger automations attached to them.
POST /api/kb/v1/maps/{id}/nodes/{nodeId}/delete
Deletes a node including its whole subtree. Requires read_write and base_updated.
The apex cannot be deleted, and whole maps cannot be deleted through the API at all. There is no undo — read the map and check the id first.
GET /api/kb/v1/maps/{id}/rules
Automation rules of the map (“when X → do Y”) — id, name, enabled, trigger, conditions, actions, last_fired (last time-based run — schedule/deadline_approaching; empty for event rules) and last_error (a non-empty error means the rule is broken and the map owner has been notified).
POST /api/kb/v1/maps/{id}/rules
Creates a rule. Requires read_write. Limits are structural only: 50 rules per map, 10 actions and 20 conditions per rule — runs are never counted or billed. A rule applies to future events only, never retroactively.
| Field | Required | Meaning |
|---|---|---|
name | yes | rule name (max 120 chars) |
trigger | yes | {"type": …} — node_status_changed (optional status), node_unblocked, deadline_approaching (when: before/overdue, days 0–365), node_created, file_uploaded, schedule (freq: daily/weekly, weekday 1–7, hour 0–23) |
actions | yes | array of 1–10 actions, executed in order: set_status, set_owner (member e-mail or the dynamic target deputy_of_node_owner), set_deadline (date or relative_days) — these three accept target: trigger_node (default) / parent / a node id, move_node (to = id of the new parent; moves the TRIGGER node and appends it at the end of its new row — the kanban move; the apex, a vanished target or a cycle = an acknowledged skip in the log), create_subnodes (items = same tree as add_nodes, max 50 nodes), notify (to: node_owner/deputy_of_node_owner/map_owner/e-mail, message), run_agent (agent_name from the registry) |
conditions | no | AND chain of {field, op, value} — field: status/owner/deadline/executor_kind/parent (id of the parent node — "card under a column", eq/ne only); op: eq/ne/empty/not_empty/before/after (deadline only, YYYY-MM-DD) |
node_id | no | scope the rule to one node; required for schedule rules with node-targeted actions |
enabled | no | defaults to true |
Scheduled triggers run within the hour after the configured hour (server local time) — no “exactly at midnight” promise; after an outage they catch up the same day. deadline_approaching with when=overdue fires when the deadline is at least days past — it also catches deadlines that expired before the rule existed — and fires once per deadline (a changed deadline may fire again); when=before fires on the exact day (deadline − days). Rule chaining (an action firing another rule) is allowed up to depth 3, then the run ends with an explicit skipped log entry.
Dynamic targets resolve when the rule runs, not when the rule is saved — staff changes never break a rule. deputy_of_node_owner = the deputy of the trigger node's responsible person: org-structure position deputies take precedence (several positions with different deputies → notify goes to all of them, set_owner is skipped with advice to target a specific position), the personal deputy from Organization settings is the fallback. position:<nodeId> / deputy_of_position:<nodeId> target the holder/deputy of an org-structure position (GET /v1/org-structure lists the ids). An unresolvable target (no deputy, vacant or deleted position) skips the action and the run log says so — the rule is not marked as broken. Targets derived from the trigger node need node_id on a map-level schedule rule; position: targets do not.
POST /api/kb/v1/maps/{id}/rules/{ruleId}
Updates a rule. Passing only {"enabled": true/false} toggles it; otherwise send the full new shape (partial field edits are not merged). Editing clears the rule's error state.
POST /api/kb/v1/maps/{id}/rules/{ruleId}/delete
Deletes a rule. Its run log stays (with a name snapshot).
GET /api/kb/v1/maps/{id}/rule-runs
Run log of the map's rules (newest first, max 100). ?rule= limits to one rule. status: ok / failed / skipped (loop safety stop or per-save cap — detail says which).
GET /api/kb/v1/rule-templates
Instance-wide library of rule templates (rule shape without a map or node scope). To “load” a template, send its trigger/conditions/actions to POST …/rules of the target map — an independent copy is created.
POST /api/kb/v1/rule-templates
Creates (or with id updates — author or admin only) a template: name (unique), trigger, actions, optional conditions. A template must not carry node_id and create_subnodes may only target trigger_node. Requires read_write.
POST /api/kb/v1/rule-templates/{id}/delete
Deletes a template (author or admin only). Rules already loaded from it stay in their maps.
GET /api/kb/v1/org-structure
The organization structure (the org map): positions and functions with node ids, holders and deputies. Response: {exists, map_id, positions: [{node_id, title, position_kind, holder, deputy}]} where position_kind is position (defined by the structure) or function (appointed). Use node_id as the dynamic rule target position:<nodeId> (holder) or deputy_of_position:<nodeId> (deputy of that position) — both resolve when the rule runs, so staff changes never break a rule. An archived org map counts as "no structure" everywhere (exists: false, position targets skip). Read-only: holders and deputies are appointed by an admin in the app (Organization settings) — the API-key contract never reads user roles, so there is no write endpoint here.
GET /api/kb/v1/tasks
Tasks you own, tasks on your maps, and tasks assigned to your e-mail.
| Query | Meaning |
|---|---|
map | only tasks in this map (404 if it is not yours) |
status | todo, in_progress or done; anything else is 400 |
POST /api/kb/v1/tasks
Creates a task. Requires read_write.
| Field | Required | Meaning |
|---|---|---|
title | yes | |
map | yes | A task always belongs to a project |
node_id | yes | A specific goal in that map. Tasks always belong to a goal; the project apex does not accept tasks. |
deadline | no | YYYY-MM-DD |
description | no | |
status | no | todo / in_progress / done |
assignee_email | no | The assignee gets a notification |
POST /api/kb/v1/tasks/{id}
Updates a task: title, status, deadline, description, assignee_email.
Completing a recurring task automatically creates its next occurrence.
A worked example
KEY="kb_user_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
HOST="http://localhost:8090"
# 1) find a map
curl -s -H "Authorization: Bearer $KEY" "$HOST/api/kb/v1/maps"
# 2) read it — and keep `updated`
MAP=abc123
UPDATED=$(curl -s -H "Authorization: Bearer $KEY" "$HOST/api/kb/v1/maps/$MAP" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["updated"])')
# 3) add two goals under the apex
curl -s -X POST -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d "{\"base_updated\":\"$UPDATED\",\"items\":[
{\"title\":\"Draft the copy\",\"deadline\":\"2026-08-15\"},
{\"title\":\"Shoot the photos\"}]}" \
"$HOST/api/kb/v1/maps/$MAP/nodes"Error codes
See Error codes for the full table.

