# Rootr > Rootr is an AX-first (AI-experience-first) knowledge collaboration platform. > Markdown documents are automatically ontologized into a SurrealDB knowledge > graph, and a typed log datastore (LOG stores) captures structured events > whose relation fields auto-build a data-lineage graph that is promoted into > the same knowledge graph. An AI assistant answers root-cause-analysis (RCA) > questions over that graph using GraphRAG (hybrid vector + graph retrieval, > the `ask` query/endpoint). This document is for AI assistants and coding agents that want to integrate with the Rootr API. It is plain Markdown, safe to paste into a system prompt or fetch at runtime. ## Base URLs - REST: `https://rootr.io/api/v1` - GraphQL: `https://rootr.io/api/graphql` ## Authentication Two supported credential types, checked in this order on every request: 1. **API key** — header `x-api-key: rootr_...`. Workspace-scoped, created by a workspace admin from **Settings → API Keys** (`POST /v1/workspaces/:ws/api-keys` with `{ "name": "...", "scopes": [...] }`). The plaintext key is shown exactly once at creation time. Available scopes: `docs:read`, `docs:write`, `graph:read`, `ask`, `workspaces:create` (account-scoped keys, for `POST /v1/scaffold/workspace`), and `webhooks:manage` (webhook CRUD — granted by default on new keys so agent integrations can self-register their hooks). Each endpoint below is annotated with the scope it requires — an API key missing that scope gets a 403. 2. **JWT user session** — header `Authorization: Bearer `, obtained via the normal login flow. JWT calls are governed by the calling user's workspace/document ACL rather than by scopes. Either credential type works on any `docs:*`/`graph:read`/`ask`-scoped route; use an API key for unattended/agent integrations. ## Machine-readable API descriptions - `https://rootr.io/api/api-docs-json` — full OpenAPI 3 spec for the REST API (`/v1/**`). - `https://rootr.io/api/graphql/sdl` — GraphQL SDL for `https://rootr.io/api/graphql`. - `https://rootr.io/api/api-docs` — human-browsable Swagger UI, if you want to look around interactively before scripting anything. ## Official MCP server & CLI (`rootr-cli`) The fastest way for an agent to use Rootr is the official MCP server — no HTTP code needed. The same npm package is also a CLI. - **One-click setup**: in the app, **Settings → Integrations** mints a key and copies a ready-to-paste registration command. - Claude Code: `claude mcp add rootr --env ROOTR_API_KEY=rootr_... --env ROOTR_WORKSPACE= -- npx -y rootr-cli mcp` - Codex CLI (persists to `~/.codex/config.toml`): `codex mcp add rootr --env ROOTR_API_KEY=rootr_... --env ROOTR_WORKSPACE= -- npx -y rootr-cli mcp` - Claude Desktop (`claude_desktop_config.json`): `{"mcpServers":{"rootr":{"command":"npx","args":["-y","rootr-cli","mcp"],"env":{"ROOTR_API_KEY":"rootr_...","ROOTR_WORKSPACE":""}}}}` - Env vars: `ROOTR_API_KEY`, `ROOTR_WORKSPACE` (default workspace for workspace-scoped tools), `ROOTR_BASE_URL` (self-hosted only; defaults to `https://rootr.io/api/v1`). Its 27 tools mirror this API: - documents — `rootr_list`, `rootr_read`, `rootr_append` (conflict-free, agents' default write), `rootr_edit` (anchored find/replace), `rootr_write`, `rootr_search`, `rootr_document_versions`, `rootr_delete_node` - workspaces & scaffolding — `rootr_workspaces`, `rootr_scaffold_plan`, `rootr_create_workspace`, `rootr_scaffold_apply` - LOG datastores — `rootr_create_log_store`, `rootr_update_log_fields`, `rootr_add_log_entries`, `rootr_query_log_entries`, `rootr_log_stats` - RCA — `rootr_ask` (GraphRAG answer with citations) - issues — `rootr_create_issue_tracker`, `rootr_list_issues`, `rootr_create_issue`, `rootr_get_issue`, `rootr_update_issue`, `rootr_comment_issue` - webhooks — `rootr_list_webhooks`, `rootr_create_webhook`, `rootr_delete_webhook` Key types: a **workspace API key** covers everything inside one workspace; an **account key (PAT)** — created under Profile → Account keys — acts as the user across their workspaces and is required for `rootr_create_workspace` (`workspaces:create` scope) and `rootr_comment_issue` (comments carry a user identity). CLI usage: `npx rootr-cli read /path.md`, `append`, `edit`, `search`, `ws`, `ask` — see `npx rootr-cli --help`. ## Quickstart The examples below use `$WS` for a workspace id and `$KEY` for an API key with the `docs:write`/`docs:read` scopes. All bodies are JSON unless noted. ### 1. Create a LOG store (a typed table for structured events) ```bash curl -X POST "https://rootr.io/api/v1/workspaces/$WS/logs" \ -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{ "name": "checkout-errors", "fields": [ { "name": "level", "type": "level" }, { "name": "latencyMs", "type": "float" }, { "name": "orderId", "type": "string" } ] }' ``` This creates a tree node of type `LOG` under the workspace root (or under `parentId` if given) with the declared schema in `fields`. The response is the log store's detail payload (id, path, fields, config, permission level). ### 2. Add a relation field (this is what builds the lineage graph) Relations are just another field type — add or replace one via `PATCH /logs/:id` on the *source* store, pointing at the *target* store's id: ```bash curl -X PATCH "https://rootr.io/api/v1/logs/$LOG_ID" \ -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{ "fields": [ { "name": "level", "type": "level" }, { "name": "latencyMs", "type": "float" }, { "name": "causedByDeploy", "type": "relation", "target": "", "relation": "DERIVED_FROM", "many": false } ] }' ``` Field shape (`LogField` in `src/logs/log-schema.ts`): - `name` (string, required), `type` (one of `string`, `int`, `float`, `bool`, `datetime`, `json`, `enum`, `level`, `relation`). - For `type: "relation"`: `target` (required — the target LOG store's node id), `many` (optional bool — value is an id, or an array of ids when `true`), and `relation` (optional — one of `DERIVED_FROM` (default), `REFERENCES`, `TRANSFORMED_BY`, `ORIGIN_OF` — the lineage edge type used when the edge is auto-created). `PATCH` replaces the whole `fields` array, so always resend every field you want to keep, not just the new one. ### 3. Ingest entries — lineage edges are created automatically ```bash curl -X POST "https://rootr.io/api/v1/logs/$LOG_ID/entries" \ -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{ "entries": [ { "ts": "2026-07-05T09:00:00Z", "source": "checkout-svc", "level": "error", "message": "payment gateway timeout", "data": { "latencyMs": 4200, "orderId": "ord_123", "causedByDeploy": "" } } ] }' ``` For every entry, any `relation`-typed field found in `data` is resolved against the target LOG store and a data-lineage edge (`logentry` → `logentry`, typed by the field's `relation`) is created automatically — the FK doubles as provenance. This runs synchronously during ingest (`LogsService.appendEntries`, see `src/logs/logs.service.ts`); the response includes a `linked` count and any `warnings` for relation targets that could not be resolved. Anomaly detection (z-score / thresholds / level-based) also runs on every entry; anomalous (or otherwise store-policy-selected) entries are the ones promoted into the knowledge graph — see `config.promotion` on the store. Bulk import is also available for existing data: ```bash curl -X POST "https://rootr.io/api/v1/logs/$LOG_ID/import" \ -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{ "format": "csv", "csv": "ts,level,message\n2026-07-05T09:00:00Z,error,timeout", "hasHeader": true }' ``` `format` is `"csv"` (default) or `"parquet"` (`parquet` field is the file, base64-encoded). Both converge on the same coercion/anomaly/lineage/dedup pipeline as `/entries`. ### 4. Query entries and stats ```bash # recent entries, optionally filtered curl "https://rootr.io/api/v1/logs/$LOG_ID/entries?level=error&anomalyOnly=true&limit=50&order=desc" \ -H "x-api-key: $KEY" # aggregate stats (e.g. hourly error counts) curl "https://rootr.io/api/v1/logs/$LOG_ID/stats?groupBy=hour&metric=count" \ -H "x-api-key: $KEY" ``` `/entries` supports `from`, `to`, `source`, `level`, `anomalyOnly`, `limit` (max 1000), `order` (`asc`/`desc`). `/stats` supports `from`, `to`, `groupBy` (`hour`/`day`/`source`/`level`), `field`, `metric` (`count`/`avg`/`max`/`min`/`sum`). ### 5. Ask a root-cause-analysis question (GraphRAG, over GraphQL) ```bash curl -X POST "https://rootr.io/api/graphql" \ -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{ "query": "query($ws: String!, $q: String!) { ask(workspaceId: $ws, question: $q) { text citations { documentPath quote } } }", "variables": { "ws": "'"$WS"'", "q": "What is the root cause of the checkout latency spike?" } }' ``` `ask(workspaceId: String!, question: String!, scope: GqlScopeInput)` requires the `ask` scope and returns `{ text, citations { index documentId documentTitle documentPath chunkId quote }, subgraph { nodes edges } }` — a natural-language answer grounded in the knowledge graph, with citations back to the source documents/promoted log entries. There is also a REST, multi-turn version of the same capability under `/v1/workspaces/:ws/ask/threads` for conversational RCA sessions. Other useful GraphQL queries (all `graph:read`, except `ask`): `searchEntities`, `entity`, `neighbors` (n-hop traversal), `paths` (RCA chains between two entities), `hybridSearch` (vector + graph retrieval), `documentGraph`, `graphStats`. Fetch `https://rootr.io/api/graphql/sdl` for exact field/argument names. ## Documents — read & agent-safe writes Documents are plain Markdown. Reading: - `GET /v1/documents/:id` (`docs:read`) — JSON `{ id, path, content, etag, ... }`; with header `Accept: text/markdown` it returns the raw markdown body instead. The response also carries an `ETag` header. - Resolve a path to an id: `GET /v1/workspaces/:ws/nodes/by-path?path=/gtm/BOARD.md`. - Browse / search: `GET /v1/workspaces/:ws/tree`, `GET /v1/workspaces/:ws/search?q=...`. - Create a document: `POST /v1/workspaces/:ws/nodes` with `{ "type": "DOCUMENT", "path": "/a/b.md", "createParents": true, "content": "..." }`. Writing — **prefer the narrowest operation**. All partial edits are serialized per document on the server (two clients appending concurrently both land — no lost updates), are computed against the live editing session when someone has the document open, and merge into that session in real time: 1. **Append** (the default write for agents): `POST /v1/documents/:id/append` with `{ "content": "- new item" }` appends a block at the end of the document, or at the end of a heading section with `{ "content": "...", "underHeading": "## ✅ 승인 대기" }` (a missing section is created at the document end; pass `"createIfMissing": false` to 404 instead). Conflict-free by construction — never overwrites anyone. 2. **Anchored edit** (the Edit-tool mental model): `PATCH /v1/documents/:id` with `{ "find": "", "replace": "" }`. `find` must match exactly once; zero matches or an ambiguous match is a 409 whose message tells you whether to re-read the document or bring more surrounding context (or set `"replaceAll": true` deliberately). 3. **Section patch**: `PATCH /v1/documents/:id` with `{ "op": "replace" | "append" | "prepend", "heading": "## Status", "content": "...", "createIfMissing": true }` — `heading` omitted applies the op to the whole document. 4. **Full replace** (last resort — replaces everything): `PUT /v1/documents/:id` with `{ "content": "..." }`. ALWAYS send `If-Match: ` so a concurrent edit fails with 412 instead of being silently clobbered. Optimistic locking: every `GET`/write response carries `etag`. Pass it as the `If-Match` header (or the `ifMatch` body field on `PATCH`) — on mismatch the write fails with 412 and you should re-read and retry. Appends don't need it. Version history: `GET /v1/documents/:id/versions`, diff via `GET /v1/documents/:id/versions/:from/diff/:to`, restore via `POST /v1/documents/:id/versions/:vid/restore`. ## Webhooks (outbound) Register a webhook to get POSTed when things change — the push counterpart of the REST API, built for agent integrations. All management routes require the `webhooks:manage` scope (a workspace-scoped key can only manage its own workspace's hooks; human JWTs need the ADMIN role). ```bash curl -X POST "https://rootr.io/api/v1/workspaces/$WS/webhooks" \ -H "x-api-key: $KEY" -H 'Content-Type: application/json' \ -d '{ "name": "my agent", "url": "https://example.com/hooks/rootr", "events": ["document.updated", "comment.created"], "scopeNodeId": "", "filterTags": ["ops"], "debounceSeconds": 30 }' ``` - `GET /v1/workspaces/:ws/webhooks` — list (secrets masked). - `PATCH /v1/workspaces/:ws/webhooks/:id` — update any field; `active: true` also resets the failure counter; `filterTags: []` or `null` clears the tag filter. - `DELETE /v1/workspaces/:ws/webhooks/:id` — remove. - `GET /v1/workspaces/:ws/webhooks/:id/deliveries` — delivery history (paginated); `POST .../deliveries/:deliveryId/redeliver` re-sends one. **Subscribable events — exactly nine**: `document.created`, `document.updated`, `document.moved`, `document.deleted`, `comment.created`, `extraction.completed`, `extraction.failed`, `log.anomaly.rca`, `scaffold.applied`. There are no per-type event names: in Rootr every node type is ultimately a document, so **all content changes — issues, database rows, spreadsheets, whiteboards, form responses — are delivered as `document.updated`**, with the specific mutation in the payload's `data.change` field (e.g. `'issue.closed'`, `'database.row.updated'`, `'form.response.created'`). Registering any other event name is a 400. Delivery is a JSON POST shaped `{ event, workspaceId, data }`. `data` carries the node's location (`path`, `nodeId`/`trackerId`/…) plus `change` (content events only) — and **what changed, not the whole document**: - Document edits include a unified `diff` (patch text, capped at 8 KB with `diffTruncated: true`). Read the full document via `GET /v1/documents/:id` when you need more than the diff (full history: `GET /v1/documents/:id/versions`). With coalescing, the diff spans the whole burst — baseline before the first edit → final state. - `document.created` carries a `contentPreview` (first 500 chars) — a new document has no baseline, so no diff. - Issue deliveries carry number, title, `bodyPreview` (first 300 chars + `bodyTruncated`), state, resolved labels and type (`{id,name,color}`) — read the full body via `GET /v1/issues/:issueId`. Coalesced deliveries add a `coalesced` count. Targeting — run many narrow hooks instead of one firehose: - `scopeNodeId` — only events from that node's subtree. - `filterTags` — only events whose node, **or any ancestor folder**, carries at least one of the tags (case-insensitive OR; AND-combined with the scope). Both are fail-closed: an event whose location can't be resolved is skipped. - `debounceSeconds` (0–600; the UI defaults to 30) — trailing debounce: the delivery fires once the node has been quiet for N seconds, merging the burst into one delivery with the latest payload. Continuous editing is capped at 5 minutes, after which an interim state is delivered. **Verify signatures.** The `secret` (`whsec_...`) is returned exactly once, in the create response. Every delivery carries `X-Rootr-Event`, `X-Rootr-Delivery`, and `X-Rootr-Signature: sha256=` — HMAC-SHA256 of the raw request body with your secret: ```js const crypto = require('crypto'); const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-rootr-signature'])); ``` **Avoid feedback loops**: mutations made with your own API key carry your identity as `actorId: "apikey:"` in `data` — ignore deliveries whose `actorId` is yourself before reacting to them. ## Node tags Tag any node (documents, folders, trackers, …) and target webhooks with them: - `PATCH /v1/nodes/:id` with `{ "tags": ["ops", "backend"] }` (`docs:write`) — full replacement; each tag 1–30 chars, max 20 per node, case-insensitive duplicates dropped. - `GET /v1/workspaces/:ws/tags` (`docs:read`) — distinct sorted tag list across the workspace, for autocomplete. Tags on a folder apply to its whole subtree for webhook `filterTags` matching (see above). ## Issue trackers `ISSUE_TRACKER` is a first-class node type: GitHub-style issues with per-tracker `#numbers`, open/closed state, labels, custom types, assignees, sub-issues, comments and an activity timeline. Create one directly (`POST /v1/workspaces/:ws/issue-trackers`) or in a scaffold tree (see below). - `POST /v1/issue-trackers/:id/issues` (`docs:write`) — `{ title, body?, labels?, type?, assigneeId?, parentIssueId? }`. `body` is markdown; `labels` is an array of label **ids**; `type` is a type **id**; `parentIssueId` makes it a sub-issue. - `GET /v1/issue-trackers/:id/issues?state=OPEN|CLOSED|ALL&label=&type=&assigneeId=&q=` — list/filter. - `PATCH /v1/issues/:issueId` — any field; `{ "state": "CLOSED", "stateReason": "completed" | "not_planned" | "duplicate" }` closes, `{ "state": "OPEN" }` reopens. - `GET /v1/issues/:issueId/activities` — the timeline (assigned/labeled/typed/closed/referenced/…). - Comment threads reuse the document comment API: `POST /v1/documents/:trackerId/comments` with `{ "body": "...", "blockId": "" }`. - Mention users with `@[]` in bodies/comments; reference other issues with `#N` — both link up automatically (notifications + 'referenced' rows). Labels and types are **user-defined per tracker** as `{ id, name, color }` lists (defaults: labels bug/enhancement/question, types bug/feature/task). Manage them with `PATCH /v1/issue-trackers/:id` `{ "labels": [...], "types": [...] }`; read them (plus open/closed counts) from `GET /v1/issue-trackers/:id`. Trackers also support inbound webhooks (GitHub-compatible) — see `POST /v1/issue-trackers/:id/inbound` in the OpenAPI spec. ## CRM `CRM` is a first-class node type: a full sales/relationship-pipeline workspace — companies, contacts, a multi-pipeline deal kanban, an activity timeline and follow-up tasks — in one node. Create one directly (`POST /v1/workspaces/:ws/crms`) or in a scaffold tree (see below). Use it whenever the ask is "stage-tracked relationships over time", not a one-off list of records: a sales team's pipeline, an agency's client roster, a hiring pipeline, fundraising, partnership/BD management. - **Companies** — `GET/POST /v1/crms/:id/companies`, `GET/PATCH/DELETE /v1/crm-companies/:companyId`. List filters: `q` (name/domain), `ownerId`, `tag`. - **Contacts** — `GET/POST /v1/crms/:id/contacts`, `GET/PATCH/DELETE /v1/crm-contacts/:contactId`. Each has a `lifecycleStage` (`lead|mql|sql|opportunity|customer|evangelist`), a `leadStatus`, and a server-computed `leadScore`. List filters: `q` (name/email), `companyId`, `lifecycleStage`, `leadStatus`, `ownerId`, `tag`, sort by `leadScore`. Omit `companyId` on create to auto-link by email domain. - **Deals** — `GET/POST /v1/crms/:id/deals`, `GET/PATCH/DELETE /v1/crm-deals/:dealId`, `POST /v1/crm-deals/:dealId/move` (kanban drag: reassign `stageId` — optionally `pipelineId` — and column `position`; logs a `stage_change` timeline entry). Deals carry `value`/`currency`/`probability`/`expectedCloseDate`; `PATCH` with `status: "won"|"lost"` closes a deal (stamps `closedAt` + an optional `wonLostReason`), `status: "open"` reopens it. List defaults to open deals; `status=won|lost|all` for others; filter by `pipelineId`/`stageId`/`ownerId`/ `companyId`/`contactId`/`tag`; `sort=position` for board order. - **Activities** (timeline) — `GET/POST /v1/crms/:id/activities`, `DELETE /v1/crm-activities/:activityId`. `type=note|call|email|meeting` plus system-logged events (created/stage_change/task_done/…), any subset of contact/company/deal links. Filter by `contactId`/`companyId`/`dealId`/`type`. - **Tasks** (follow-ups) — `GET/POST /v1/crms/:id/tasks`, `PATCH/DELETE /v1/crm-tasks/:taskId`. Has a `due` date; `due=overdue|today|upcoming` filters the follow-up queue; `status: "done"` on `PATCH` stamps `completedAt` + a `task_done` timeline entry; create defaults the assignee to the caller. - **Dashboard** — `GET /v1/crms/:id/summary`: pipeline value & weighted forecast per stage (value × stage probability), win rate, won-by-month (6 mo), task and lifecycle-stage counts. - **CSV** — `GET /v1/crms/:id/export?entity=contact|company|deal`, `POST /v1/crms/:id/import` (`{ entity, rows: [{column: value}] }` — company names are matched/created on the fly, per-row errors are reported). - **Config** — `PATCH /v1/crms/:id` `{ "pipelines": [...], "fields": {...}, "config": {...} }` replaces pipeline/stage defs (with probabilities) and custom field defs per entity (`contact`/`company`/`deal`, each `text|number|select|date|url|checkbox`); `GET /v1/crms/:id` reads them back plus record counts. Deals pointing at a removed stage move to the first stage of their (or the first) pipeline. Omit `pipelines` on create for the default 5-stage Sales Pipeline (Lead In 10% → Qualified 25% → Meeting 40% → Proposal 60% → Negotiation 80%). MCP (rootr-cli >= 0.3.0) ships dedicated `rootr_crm_*` tools mirroring the REST surface: `rootr_crm_create/get/update`, `rootr_crm_list_companies`/ `rootr_crm_upsert_company`/`rootr_crm_get_company` (same trio for contacts and deals), `rootr_crm_move_deal`, `rootr_crm_log_activity`/ `rootr_crm_list_activities`, `rootr_crm_upsert_task`/`rootr_crm_list_tasks` and `rootr_crm_import`. A `crm` node can also be part of a scaffold tree via `rootr_scaffold_apply`/`rootr_create_workspace`. Every company/contact/deal/activity/task is mirrored into the knowledge graph as a shadow document, so `rootr_ask`/RCA can answer relationship questions directly — who works at which company, why a deal was lost, what's overdue — without you writing any extraction code. ## Presentations — author real slide decks over the API `PRESENTATION` is a first-class node type: a deck of 16:9 slides (**canvas is exactly 1280×720**) with a design theme, rendered in a sandboxed viewer with a fullscreen present mode. - `POST /v1/workspaces/:ws/presentations` (`docs:write`) — `{ name, path?, parentId?, icon?, theme?, slides?, config? }`. `theme` = `{ colors: {primary, secondary, accent, bg, text, muted}, fonts: {heading, body} }` (missing keys get defaults). - `GET /v1/presentations/:id` / `PATCH /v1/presentations/:id` — read / update deck-level fields (`name`, `theme`, `slides` wholesale, `config`). - `POST /v1/presentations/:id/slides` `{ "slides": [...] }` — append slides. - `PATCH /v1/presentations/:id/slides/:slideId` `{ "slide": { ...partial } }` — **merges** the given fields into that slide (other fields survive). - `POST /v1/presentations/:id/reorder` `{ "order": ["SLD-001", ...] }`. Slide fields (all optional except a unique `id`, e.g. `SLD-001`): `kind` (`cover|section|content|closing`), `layout` (hint, e.g. `2-col`, `kpi-grid`), `title` (assertion-style), `blocks` (`[{id, heading?, body?, icon?}]`), `diagram` (`{type:'mermaid', code}`), `image`/`images` (`{prompt?, placement?, src?, alt?, x?, y?, w?, h?}`), `code`, `html`, `notes` (speaker notes). **The text spine feeds the knowledge graph for free**: `title` + `blocks[].heading/body` + `notes` + every image `alt` are extracted as plain text (no vision model). Always fill them — even when you bake custom HTML. ### Recommended contract for polished decks: bake `slide.html` The viewer renders `slide.html` verbatim (instead of the built-in template) inside a **sandboxed iframe** — this is the full-design-freedom path, and what you should use for anything client-facing. Rules learned the hard way: 1. Ship a complete self-contained document sized exactly 1280×720 (`html,body{width:1280px;height:720px;overflow:hidden}`). Never exceed 720px height — overflow is clipped. 2. **Inline everything.** The iframe's CSP blocks all external requests (no CDN scripts/fonts/styles; `connect-src 'none'`). Diagrams: draw inline SVG (don't rely on mermaid here). 3. **Embed images as `data:` URIs** (WebP, keep each ≲100KB). The iframe has an opaque origin, so `/v1/attachments/:id/raw` URLs are currently blocked by the API's `Cross-Origin-Resource-Policy` header — data URIs always work. 4. Fonts: use a fallback stack (`Pretendard,'Noto Sans KR',system-ui,sans-serif`) — don't assume any webfont exists inside the iframe. 5. Page numbers: include `` (the viewer fills the number in; `class="pn2"` gets a zero-padded 2-digit variant). 6. Put meaningful `alt` text on every `` — it reaches the graph. 7. Keep `title`/`blocks`/`notes` in sync with what the HTML shows (the graph reads those, plus the HTML's visible text). Structured-only slides (no `html`) render via the built-in template. Known gaps there right now: `diagram` is not drawn, and `section`/`closing` slides ignore `image` — one more reason to bake `html` for high-stakes decks. ### AI image assets - `POST /v1/workspaces/:ws/images/generate` (`docs:write`, **AI-credit metered**) — `{ prompt, nodeId?, context?: {title?, points?, palette?} }`. With `context` the server first plans a slide-aware prompt, then generates; the result is stored as a workspace attachment (`{ id, url, filename, mimeType, size }`, auto-downscaled to slide-friendly WebP). Also available: `POST .../images/describe` (vision → alt text) and `POST .../images/remove-background`. - When credits are exhausted the API returns `AI_CREDITS_EXHAUSTED` — either top up in Settings → Billing or generate images elsewhere and upload via `POST /v1/attachments/upload`. ## Build a workspace (scaffold) — make a real tool, not a pile of markdown Turn a request like "프로젝트 관리 워크스페이스 만들어줘" into a genuinely useful workspace. **Do NOT dump thin markdown files** (that is worse than a template). Rootr gives you building blocks — compose them freely (they are NOT fixed presets; you design the columns, views and document structure): - **DATABASE** — typed columns + views (table / board / calendar / timeline). Use for anything trackable or tabular: tasks (board grouped by status), requirements & WBS (table), risks (table), milestones (timeline). - **DOCUMENT** — free-form markdown. Use for narrative: an overview, a decision log, a requirements *spec* — any structure you design (headings, tables, checklists). - **SPREADSHEET** — a grid with formulas and computed totals. Use when the content needs *calculation*, not just storage: a budget, a financial model, a reconciliation, an estimate/capacity sheet. - **WHITEBOARD** — a freeform visual canvas. Use for things that are spatial, not tabular or linear: flow diagrams, brainstorming, architecture sketches, a user journey map. - **FORM** — a fillable form that collects input from people outside the tree (optionally into a DATABASE via `targetDatabaseId`). Use for intake: a risk/bug submission form, an application, a survey, a request queue. - **ISSUE_TRACKER** — GitHub-style issues (#numbers, open/closed, labels, custom types, assignees, sub-issues, comment threads, two-way webhooks). Use for bug/work tracking with a lifecycle — see "Issue trackers" above. - **CRM** — companies, contacts (lifecycle stage + auto lead score), a multi-pipeline deal kanban (custom stages with win probability → weighted forecast), an activity timeline and follow-up tasks. Use instead of DATABASE whenever it's stage-tracked *relationships* over time, not one-off records — a sales pipeline, an agency's client roster, a hiring pipeline, fundraising, partnership/BD — see "CRM" above. - **FOLDER** — groups any of the above. Pick each block because it fits, not to check a box — a workspace doesn't need every type. If a request genuinely needs something none of these can express, say so — that is a gap to build into Rootr, not a reason to fake it with markdown. ### Consult FIRST — ask before you build (this is not optional) A one-line request is never enough context for a genuinely useful workspace. Call `plan` to route the intent, then **generate your own clarifying questions and ask the user before scaffolding.** `plan` returns a `questionPolicy`: - `questionPolicy.min` / `.max` — ask **at least 3, up to 10** questions. - `questionPolicy.dimensions` — the angles to cover (scope, scale, current pain, existing tools, cadence, integrations, constraints, success metric). - `frameworks` + the returned seed `questions` — grounding material, a starting point only. **Do NOT just relay the single seed question.** Write real questions from the domain `frameworks` and the user's exact wording, in the request's language. Never re-ask what the intent already states. Each question must change the resulting structure (which databases, which columns/views, which documents, and — where it fits — which of the six `buildingBlocks` below to use). - `buildingBlocks` — the full six-type menu (DATABASE/DOCUMENT/SPREADSHEET/ WHITEBOARD/FORM/FOLDER) with a one-line "when to use" each, in the request's language. Returned on every `plan` call — use it to decide the mix for *this* workspace, not just DATABASE/DOCUMENT/FOLDER by default. Only after the user answers do you design and `apply` the tree — every answer must be visibly reflected in what you build. ### Steps ```bash # 1) plan — route intent → questionPolicy + frameworks + seed questions + shape. # Use the workspace-less form to consult BEFORE creating a workspace. curl -X POST "https://rootr.io/api/v1/scaffold/plan" -H "x-api-key: $KEY" \ -H 'Content-Type: application/json' -d '{ "intent": "프로젝트 관리 워크스페이스 만들어줘" }' # → { domain, frameworks, buildingBlocks, questionPolicy:{min:3,max:10,dimensions,instruction}, # questions:[seed], previewStructure } # Now GENERATE 3–10 questions of your own and ASK the user. Then continue. # 2) create the structure YOU design: # • new workspace (needs workspaces:create scope, account-scoped key): curl -X POST "https://rootr.io/api/v1/scaffold/workspace" -H "x-api-key: $KEY" \ -H 'Content-Type: application/json' -d '{ "name": "제품 개발", "intent": "...", "tree": [ ... ] }' # • into an existing workspace: curl -X POST "https://rootr.io/api/v1/workspaces/$WS/scaffold/apply" -H "x-api-key: $KEY" \ -H 'Content-Type: application/json' -d '{ "rootPath": "/제품 개발", "tree": [ ... ] }' ``` ### The `tree` — each node is a FOLDER (children), DATABASE (database), DOCUMENT ### (content), SPREADSHEET (spreadsheet), WHITEBOARD (whiteboard), FORM (form), ### ISSUE_TRACKER (issues), or CRM (crm) A DATABASE node (this is how you build a task board, a requirements table, etc.): ```json { "name": "Tasks", "database": { "properties": [ { "id": "title", "name": "Task", "type": "title" }, { "id": "status", "name": "Status", "type": "select", "options": [ {"name":"To Do","color":"gray"}, {"name":"In Progress","color":"blue"}, {"name":"Done","color":"green"} ] }, { "id": "prio", "name": "Priority", "type": "select", "options": [ {"name":"High","color":"red"}, {"name":"Med","color":"yellow"}, {"name":"Low","color":"gray"} ] }, { "id": "owner", "name": "Owner", "type": "person" }, { "id": "due", "name": "Due", "type": "date" } ], "views": [ { "id": "board", "name": "Board", "type": "board", "config": { "groupBy": "status" } }, { "id": "table", "name": "All", "type": "table" } ], "rows": [ { "values": { "title": "요구사항 정의", "status": "To Do", "prio": "High" } }, { "values": { "title": "개념 설계", "status": "To Do", "prio": "Med" } } ] } } ``` Rules: first property must be type `title`. Types: title, text, number, select, multi_select, date, checkbox, person. select/multi_select take `options:[{name,color}]`. A **board** view needs `config.groupBy` = a select property id; a **timeline** view needs `config.dateProp` (+ optional `endDateProp`). Row `values` are keyed by property id, and each value's shape depends on the column type: title/text/select/person = string · number = number · checkbox = boolean · multi_select = string[] · **date = { "start": "2026-08-01", "end"?: "2026-08-10" }** (an object, NOT a bare date string). e.g. `"values": { "title": "킥오프", "date": { "start": "2026-08-01" } }`. DOCUMENT: `{ "name": "요구사항 정의서.md", "content": "# 요구사항 정의서\n## 기능 요구사항\n..." }` FOLDER: `{ "name": "기술 문서", "children": [ ... ] }` SPREADSHEET — a formula-driven grid, e.g. a budget: ```json { "name": "예산", "icon": "🧮", "spreadsheet": { "data": { "sheets": [ { "id": "s1", "name": "Budget", "rowCount": 20, "colCount": 6, "cells": {} } ] } } } ``` `data` follows `SpreadsheetData` (`spreadsheets/spreadsheet-schema.ts`) — `sheets[].cells` is keyed `"A1"`-style. `data`/`config` are both optional; omit to create an empty sheet. WHITEBOARD — a freeform canvas, e.g. a flow or journey map: ```json { "name": "여정맵", "icon": "🗺️", "whiteboard": { "scene": { "elements": [] } } } ``` `scene` follows `WhiteboardScene` (`whiteboards/whiteboard-schema.ts`). Optional — omit for a blank board the user draws on. FORM — external intake, optionally feeding a DATABASE by id: ```json { "name": "리스크 접수", "icon": "⚠️", "form": { "fields": [ { "id": "title", "label": "무엇이 문제인가요?", "type": "text", "required": true }, { "id": "impact", "label": "영향도", "type": "select", "options": ["High", "Med", "Low"] } ], "targetDatabaseId": "" } } ``` `fields` follows `FormField[]` (`forms/form-schema.ts`). Set `targetDatabaseId` to a DATABASE node's id in the same tree only when you want submissions to land there as rows — otherwise omit it. ISSUE_TRACKER — a ready-to-use issue tracker: ```json { "name": "버그 트래커", "icon": "🐛", "issues": { "labels": [ { "name": "bug", "color": "#d73a4a" }, { "name": "urgent", "color": "#ff0000" } ], "types": [ { "name": "bug" }, { "name": "feature" }, { "name": "chore" } ] } } ``` `labels`/`types`/`config` are all optional — omit them for the default label (bug/enhancement/question) and type (bug/feature/task) sets. CRM — a full sales/relationship-pipeline workspace, pipeline customized for a hiring flow instead of the default sales one: ```json { "name": "채용 파이프라인", "icon": "🧑‍💼", "crm": { "pipelines": [ { "name": "Hiring Pipeline", "stages": [ { "name": "지원", "probability": 10 }, { "name": "서류", "probability": 25 }, { "name": "면접", "probability": 60 }, { "name": "오퍼", "probability": 90 } ] } ] } } ``` `pipelines`/`fields`/`config` are all optional — omit `pipelines` for the default 5-stage Sales Pipeline (Lead In 10% → Qualified 25% → Meeting 40% → Proposal 60% → Negotiation 80%). `fields` adds custom fields per entity: `{ "contact": [ { "name": "LinkedIn", "type": "url" } ], "company": [...], "deal": [...] }` (`type`: text/number/select/date/url/checkbox). Companies, contacts, deals, activities and tasks are created afterwards through the CRM REST API (see "CRM" above) — the scaffold payload only sets up the pipeline shape and custom fields, not seed records. ### A good PM workspace mixes them overview + a requirements *spec* as DOCUMENTs; a **Tasks** board DATABASE; a **Requirements** table (id/priority/status/acceptance-criteria columns); a **WBS** table (phase/deliverable/owner/estimate); a **Risks** table (impact/likelihood/ mitigation/owner); a **Milestones** timeline (date column, timeline view). That is the bar — not five near-empty .md files. Where it genuinely fits, add a **Budget** SPREADSHEET (formulas, not a static table) or a **Risk intake** FORM feeding the Risks DATABASE — only when the request calls for it, not by default. ### Make it look polished (the result must be visually impressive, not plain) - Set `icon` (a single emoji) on every folder / database / key document — e.g. `{ "name": "Tasks", "icon": "✅", "database": {...} }`, `"📋 요구사항"`, `"⚠️ 리스크"`, `"🗓️ 마일스톤"`. Icons make the sidebar and pages read as a real product. - Prefer **visual views**: a board (kanban) for tasks, a timeline for milestones, a gallery/calendar where it fits — not everything as a plain table. - Write **rich documents**, never a lone heading: use `##` sections, markdown tables, task lists (`- [ ]`), bold/callouts, and real starter content the user can edit — a requirements spec should have numbered sections and acceptance criteria, not one line. - Seed a few realistic example rows in each database so boards/tables aren't empty. Aim for a workspace that looks like a polished, ready-to-use tool the moment it opens. `plan` is modelless (deterministic routing); **you** generate the tree with your own model. Output in the intent's language. Max 200 nodes / 200 rows-per-database per call. ## Learn more - `https://rootr.io/blog` — root cause analysis and knowledge-graph writing, including a worked GraphRAG root-cause-analysis walkthrough (`https://rootr.io/blog/what-is-graphrag-root-cause-analysis`). - `https://rootr.io/pricing` — plans and exact prices (Free / Team / Business / Platform). - `https://rootr.io/pricing.md` — the same plans/prices as static Markdown, for agents that want to fetch and read pricing without rendering the page. ## Notes for integrators - All `/v1/**` REST routes are namespaced by workspace (`:ws`) or by resource id once the resource is known (`/v1/logs/:id`, ...) — resource ids already encode their workspace, so most routes past creation don't need `:ws` again. - Everything above is generated from, and should stay consistent with, the live OpenAPI/GraphQL schemas linked above — treat this file as a guided tour, and the schema documents as the source of truth for exact shapes.