list_canvases
read
Included
List canvases in the vault (id, name, node count).
args none
FlowGraph is agent-native. Agents read and write your graph through the MCP server, and every write flows through the same governed, lock-aware path as a human edit. AI proposes, you decide.
Raw markdown (/api.md) AGENTS.md OpenAPI llms.txt
Every write ends up as a GraphPatch {ops[]} through ONE governed path: provenance-stamped and lock-aware. A write to a human-approved (locked) atom, or one the AI lacks the modify capability for, is rejected. No tool is destructive — every write is additive, staged and reversible.
For anything content-bearing, do not hand-write ops. Read the canvas with get_canvas_digest and write back a draft in FDG (plain markdown) with submit_knowledge_draft — a deterministic compiler turns it into the governed patch. apply_graph_patch remains for surgical, programmatic ops.
Plan tiers. The agent API runs on the free, local stdio server, so every MCP tool below is Included. REST endpoints ride the optional cloud backend; where a plan tier is not yet certain, no plan badge is shown rather than a guessed one.
The MCP server speaks stdio over a folder of .fgraph files — no account, no key, nothing leaves your device:
FLOWGRAPH_VAULT=/path/to/vault npx tsx app/mcp/server.ts
Registry descriptor: /.well-known/mcp/server.json. Hand an agent the compact spec in-app via API & Docs → Copy for AI. The REST endpoints (below) are the optional Cloudflare backend — provider keys stay server-side; a fresh install talks to no server until you opt in.
35 tools. 🔒 = governed write path. Every tool is Included (free).
list_canvases
read
Included
List canvases in the vault (id, name, node count).
args none
get_canvas
read
Included
Get a canvas — its nodes and edges — by id.
args canvasId
{
"canvasId": "cv_…"
}query_graph
read
Included
Edge-aware query: text · facet · tags · relType · traverse(up/down). Returns matching node ids + titles.
args canvasId query
{
"canvasId": "cv_…",
"query": {
"traverse": {
"from": "node_id",
"dir": "down",
"via": [
"feeds"
]
}
}
}flowgraph_retrieve
read
Included
Graph-CCR retrieval (READ-ONLY): pull exactly the sub-graph you need. Give a `query` (semantic/keyword seed → structural expansion) OR a `handle` (a retrieval handle "canvasId::nodeId" from an assembled context packet) → its original atom + neighbourhood. A STALE handle returns a recovery hint, never an error. Output is governance-aware (atoms without an AI use-as-context grant come back degraded, no body) and provenance-stamped.
args query handle canvasId k hops
{
"query": "what feeds the priced BOM",
"hops": 1
}create_canvas
read
Included
Create a new empty canvas; returns its id.
args name
{
"name": "Onboarding workflow"
}apply_graph_patch
🔒 write
Included
Apply a GraphPatch (ops[]) through the GOVERNED, provenance-stamped, lock-aware write path. A write to a human-approved (locked) atom — or one the AI lacks the "modify" capability for — is REJECTED.
args canvasId patch
{
"canvasId": "cv_…",
"patch": {
"ops": [
{
"op": "addCard",
"tmpId": "a",
"data": {
"title": "New step",
"meta": {
"confidence": {
"score": 0.6
}
}
}
},
{
"op": "addEdge",
"source": "a",
"target": "existing_node_id",
"data": {
"relType": "feeds"
}
}
]
}
}refresh_codebase_map
🔒 write
Included
Refresh a multi-repo (fleet) codebase map: pass each repo's label + package name + declared dependencies (with the manifest line each is declared on), and this stages a governed ghost-draft that (re)builds one card per repo and the typed `depends` cross-repo edges, each carrying its manifest-line receipt. Reconciled against the target canvas — existing repo cards update, new repos add, the edge set recomputes — so a scheduled re-run is idempotent and never duplicates. A human accepts it in the Review Inbox (agents never auto-apply). Use it as the cron/agent codebase-refresh recipe.
args canvasId repos pruneRemoved
{
"canvasId": "cv_…",
"repos": [
{
"label": "auth-service",
"packageName": "@acme/auth",
"language": "TypeScript",
"dependencies": []
},
{
"label": "billing-service",
"packageName": "@acme/billing",
"language": "TypeScript",
"dependencies": [
{
"name": "@acme/auth",
"versionSpec": "^1.0.0",
"manifestPath": "package.json",
"line": 12,
"raw": "\"@acme/auth\": \"^1.0.0\""
}
]
}
]
}get_canvas_digest
read
Included
Read a canvas as an FDG digest (READ-ONLY): the anchored knowledge grammar the in-app AI reads and writes. Every card, section, entry and group carries a short anchor ([c3], [c3.f2], [c3.f2.i4], [g1]) you cite when you write back with `submit_knowledge_draft`. `nodeIds` focuses the read (those cards + their neighbours render in FULL, the rest as an outline); `hops` widens that full-fidelity ring; `budget` caps the characters (an honest tail line says what was withheld — never a silent truncation). Returns the digest text, the anchor→atom map, per-card fidelity, stats, and a `digestId` — pass that back to `submit_knowledge_draft` so your anchors mean the atoms you actually read. The cheap way to orient before writing; complements `flowgraph_retrieve`.
args canvasId nodeIds hops budget
{
"canvasId": "cv_…",
"nodeIds": [
"node_checkout"
],
"hops": 1
}submit_knowledge_draft
🔒 write
Included
WRITE KNOWLEDGE, NOT OPS — the preferred way to write anything content-bearing. Submit a draft in FDG (the markdown grammar `get_canvas_digest` speaks) and a DETERMINISTIC compiler turns it into a GraphPatch applied through the same governed, lock-aware, provenance-stamped path as every other write; a locked atom is refused. `mode:"apply"` (default) writes it; `mode:"stage"` lands it as a Review-Inbox ghost-draft for a human to accept. On ANY compile or validation problem NOTHING is written — you get indexed errors[], every dropped block with its line and reason, and a fix hint, so you can correct and resubmit. Returns applied/newCards/anchorMap/dropped/depth/questions/suggestions, so your [anchors] stay addressable and you can relay the AI's questions or chain its suggested next moves. `apply_graph_patch` remains for surgical, programmatic ops.
args canvasId draft mode digestId
Write the `draft` in FDG — FlowGraph's draft grammar. Plain markdown, no JSON: you think in
knowledge, and a deterministic compiler (never a model) turns it into a governed patch. This is
the SAME teaching FlowGraph's own AI receives, so what works in the app works here:
You are FlowGraph's knowledge architect. Your job is to COMPOUND the user's knowledge graph:
make what already exists deeper, better connected, and more honest — and extend it only where
the intent genuinely needs something new.
You will receive a DIGEST of the current canvas, written in the same grammar you must write.
Every card, section, item, and group in it carries an anchor: [c3], [c3.f2], [c3.f2.i4], [g1].
Each card anchor also says how many sections that card already carries, and (full) marks one
that carries all it can. The digest is the single source of truth for what already exists.
Write a KNOWLEDGE DRAFT in this grammar — plain markdown, no JSON, no commentary outside it:
# Title → new card; # [c3] Title → work on existing card c3
{id: a, kind: …, accent: …, confidence: 0.x, group: [g1] | "New theme"} → attribute line
prose under # → the card's body
## Section → section on the current card; ## [c3.f2] … → existing section
## Section (in: X, out: Y) → name the section's own input/output ports (either alone is fine)
- item → one entry; - [ ] item → an open to-do; - Key :: value → a property
- [c3.f2.i4] new text → rewrite that existing item (full replacement)
-> [c7] feeds: label → edge from the current card ([a] -> [c7] … works anywhere)
> why: / > source: title — excerpt / > assume: inference → attach to the block above
THE DEPTH BAR — a card without substance is a failure:
- Every NEW card: 2-4 sections, each with 3-6 substantive entries. An entry is a fact, step,
input, output, risk, metric, or decision — written so a colleague could act on it tomorrow.
Never "TBD", never "misc", never a section that restates its own title.
- Prefer enriching an EXISTING card (open it with # [c3]) over creating a near-duplicate.
- A card the digest marks (full) already carries all the sections it can hold: do NOT give it
another one. Put that knowledge on a sub-card instead — a new # Title card with the parent's
group ({group: [g1]}) or an edge to it (-> [c3] contains:) — and keep the card readable.
COMPOUNDING RULES:
- No islands: every new card connects to at least one existing anchor with a typed edge
(feeds|depends|maps|contains|related|blocks|proves|supersedes).
- Never repeat what the digest already shows — extend it, sharpen it, or challenge it.
- Groups and colors mean things on this canvas. Join an existing group where a card belongs;
name a new group only for a genuinely new theme. Match the canvas's color usage.
- Match the scope of the ask: a small ask means a small draft. Zero changes is a valid
answer — say why in one > why: line.
HONESTY:
- Every non-obvious claim carries > source: (backed) or > assume: (inferred). Confidence
numbers are honest, not polite. > why: on any block whose purpose isn't self-evident.
AFTER the draft content, optionally:
- Up to 3 "> ask: …" lines — only questions whose answer would change the graph and that
only this user can answer.
- Up to 3 "> next: label | full prompt" lines — each pointing at a NAMED gap visible in your
draft or the digest. No "want more detail?" filler.
WORKED EXAMPLE (intent: "flesh out the money-back side of checkout"):
# [c2] Checkout service
## [c2.f1] Risks
- Provider webhook retries can double-fire on timeout — idempotency key required on /confirm
> source: Stripe docs — webhook retries continue up to 3 days with backoff
- [ ] Chargeback flow untested in staging
> assume: staging has no card-network simulator; inferred from the empty test-card set in [c4.f2]
# Refund pipeline
{id: rp, kind: process, accent: teal, confidence: 0.75, group: [g1]}
Handles all post-capture money-back paths — the missing sibling of [c2].
## Steps
- Validate claim window (14 days default; 30 for EU per the policy card [c7])
- Reverse capture via provider API — partial refunds only before settlement
- Emit refund-completed event for nightly reconciliation
## Properties
- Owner :: Payments team
- SLA :: 5 business days
- System of record :: provider dashboard + ledger table `refunds`
## Open risks
- Currency-conversion drift between capture and refund dates — who absorbs the spread?
> assume: no policy found on the canvas; flagging it rather than inventing one
-> [c2] depends: refund needs the original capture reference
-> [c5] feeds: refund events reconcile nightly
> ask: Who owns the currency-drift decision — Payments or Finance?
> next: Map the dispute path | Extend [c2] and [rp] with a chargeback/dispute card covering evidence deadlines, provider fees, and reconciliation impact
Two notes for agents specifically:
- Anchors ([c3], [c3.f2], [c3.f2.i4], [g1]) are aliases issued BY A DIGEST, not durable ids.
Call get_canvas_digest first and pass its `digestId` back here, or your anchors may resolve
to atoms you never read.
- Nothing is written unless the WHOLE draft compiles and validates. On a problem you get indexed
errors, the exact lines that dropped and why, and a fix hint — correct them and resubmit.{
"canvasId": "cv_…",
"digestId": "dg_…",
"mode": "apply",
"draft": "# Refund pipeline\n{id: rp, kind: process, accent: teal, confidence: 0.75}\nHandles all post-capture money-back paths — the missing sibling of [c2].\n## Steps\n- Validate claim window (14 days default; 30 for EU per [c7])\n- Reverse capture via provider API — partial refunds only before settlement\n- Emit refund-completed event for nightly reconciliation\n## Properties\n- Owner :: Payments team\n- SLA :: 5 business days\n-> [c2] depends: refund needs the original capture reference\n> ask: Who owns the currency-drift decision — Payments or Finance?"
}add_card
🔒 write
Included
Convenience: add one card to a canvas.
args canvasId title body
connect
🔒 write
Included
Convenience: connect two cards with a typed relationship.
args canvasId source target relType label
list_skills
read
Included
List reusable SKILLS (promoted action items) — name, the card/canvas they live on, their inner-canvas id, and whether they are READY (inner canvas non-empty). The agent capability catalog.
args readyOnly
{
"readyOnly": true
}list_due_reviews
read
Included
The ranked review agenda — scheduled reviews across the vault joined with attention state, bucket (overdue/today/week/later), blast-radius and owner. The same agenda the human sees.
args window assignee
{
"window": "overdue"
}schedule_review
🔒 write
Included
Propose a review date on an atom (ISO date or relative like "in 2 weeks"). A GOVERNED write: stamped provenance.kind=ai, REJECTED on human-approved (locked) atoms, surfaced for the human in the calendar / Review Inbox — agents never silently sign off.
args canvasId nodeId when assignee note
{
"canvasId": "cv_…",
"nodeId": "node_…",
"when": "in 2 weeks",
"note": "sources may be stale"
}reschedule_review
🔒 write
Included
Move an atom’s existing review to a new date WITH A REQUIRED REASON (lands in the atom’s history). Governed; rejected on locked atoms. Use to sequence a review after an upstream one resolves.
args canvasId nodeId when reason
{
"canvasId": "cv_…",
"nodeId": "node_…",
"when": "in 1 month",
"reason": "blocked on the SAP migration review"
}complete_review
🔒 write
Included
Propose marking a scheduled review DONE plus optional findings — as a governed write the human sees. NEVER approves or locks (only a human signs off); just records that the agent did a first-pass review.
args canvasId nodeId findings
{
"canvasId": "cv_…",
"nodeId": "node_…",
"findings": "sources still valid; confidence holds"
}list_actions
read
Included
EVERY action across the vault (not just promoted skills): its address 4-tuple {canvasId,nodeId,facetId,itemId}, text, binding, current test status, unmet prerequisites, and strategy. The agent’s runnable-work catalog.
args canvasId
{
"canvasId": "cv_…"
}get_action
read
Included
One action’s FULL contract (binding + schemas, test, inputs, loop policy, strategy) plus its recorded runs, by address 4-tuple.
args canvasId nodeId facetId itemId
run_action_test
🔒 write
Included
Run the DRY-RUN pipeline (precondition → contract-check → deterministic; no world touched, no network) against an action. Returns the full ActionRun; writes `action.test` through the governed patch and records the run in the agent ledger. It can never approve, lock, or execute live.
args canvasId nodeId facetId itemId
{
"canvasId": "cv_…",
"nodeId": "node_…",
"facetId": "facet_…",
"itemId": "item_…"
}report_action_run
🔒 write
Included
Driver B: report a run YOU executed with your own tools — steps, verdict, artifacts, tokens/cost, and your identity. Schema-validated; recorded as ActionRun{driver:"agent"} + a RunSummary. A “live” claim is labeled AGENT-CLAIMED (never presented as app-verified execution). Requires the `execute` AI capability on the action atom — WITHOUT it the report stages as a ghost-draft for human accept. Idempotent on runId.
args canvasId nodeId facetId itemId runId verdict live steps artifacts tokens costUsd note
{
"canvasId": "cv_…",
"nodeId": "node_…",
"facetId": "facet_…",
"itemId": "item_…",
"runId": "agent-run-1",
"verdict": "pass",
"live": true,
"artifacts": [
{
"name": "model.nwc",
"size": 1048576,
"sha256": "…"
}
]
}list_runs
read
Included
The Control-Tower read: recorded runs (app + agent) filtered by canvas / action address / verdict / driver, newest first.
args canvasId nodeId itemId verdict driver limit
{
"driver": "agent",
"verdict": "pass"
}ifc_inventory
read
Included
A Smart IFC model’s data-first inventory (READ): schema, element/storey/space/type counts, per-class counts, storeys, and materials — DERIVED from the parsed model (never AI-guessed). `modelId` is a registered model id OR the model’s sha256 (content identity).
args modelId
{
"modelId": "sha256_or_model_id"
}ifc_query
read
Included
Run the FlowGraph IFC query grammar over a model’s element index (READ): e.g. `class:IfcDoor storey:"Level 2" Pset_DoorCommon.FireRating=EI30`. Returns matched elements (GlobalId · class · name · storey), their GlobalIds, a mini-QTO quantity summary, and honest parse warnings. Pure — no geometry, no wasm on the caller, no network.
args modelId query text
{
"modelId": "sha256_or_model_id",
"query": "class:IfcDoor FireRating=EI30"
}ifc_element
read
Included
One element’s full properties/psets/quantities by GlobalId (READ), derived from the parsed model. Absent properties are honestly absent, never fabricated.
args modelId globalId
{
"modelId": "sha256_or_model_id",
"globalId": "2O2Fr$t4X7Zf8NOew3FLD1"
}ifc_create_set
🔒 write
Included
Create a selection set on a model card (WRITE → staged for human accept). Give a `query` (a dynamic set — the query IS the set) OR explicit `globalIds` (a static snapshot). Lands as an IfcAnchor set item on the card’s model facet — never applied directly; a locked atom is refused.
args canvasId nodeId facetId modelId query globalIds label
{
"canvasId": "cv_…",
"nodeId": "node_…",
"facetId": "facet_…",
"modelId": "sha256_or_model_id",
"query": "class:IfcDoor FireRating=EI60",
"label": "EI60 doors"
}ifc_link
🔒 write
Included
Link a model item (a pin/set/viewpoint) to another card with a typed relationship (WRITE → staged for human accept). A governed edge from the item’s handle → the target card; default relType `appears_in`. Never applied directly; a locked atom is refused.
args canvasId nodeId facetId itemId toNodeId relType
{
"canvasId": "cv_…",
"nodeId": "node_…",
"facetId": "facet_…",
"itemId": "item_…",
"toNodeId": "node_pump",
"relType": "appears_in"
}ifc_viewpoint
🔒 write
Included
Save a named viewpoint on a model card (WRITE → staged for human accept): an engine-neutral IfcAnchor view item (BCF-shaped `camera` optional). Never applied directly; a locked atom is refused.
args canvasId nodeId facetId modelId label camera
{
"canvasId": "cv_…",
"nodeId": "node_…",
"facetId": "facet_…",
"modelId": "sha256_or_model_id",
"label": "Entrance approach"
}ifc_project
🔒 write
Included
Project a model’s structure INTO the graph (WRITE → staged ghost-diff for human accept): deterministic storey cards + top type-summary cards + contains/located_at edges, every atom provenance `derived`. Never applied directly; reject leaves the vault unchanged.
args canvasId nodeId facetId modelId
{
"canvasId": "cv_…",
"nodeId": "node_model",
"modelId": "sha256_or_model_id"
}aps_translate
🔒 write
Included
Request an APS (Autodesk cloud) translation of a proprietary model (.rvt/.nwd/.dwg/.dgn) so it can be viewed + graphed (WRITE → staged for human accept). It NEVER translates directly: cloud translation sends bytes off-device and spends the user’s Autodesk credits, so it always stages a ghost-draft the human accepts in the app. Returns the staged request. Once translated, query it with the `ifc_*` tools.
args canvasId name sourceFormat acc
{
"canvasId": "cv_…",
"name": "tower.rvt",
"sourceFormat": "rvt"
}aps_browse
read
Included
Browse Autodesk Construction Cloud / BIM360 (READ): hubs → projects → folders → items. Answers from an OPTIONAL cached browse file (`_aps-browse.json`) in the vault when present (the tool stamps source:"cached" itself, so the cache can’t forge the label); absent ⇒ an honest not-configured notice — the 3-legged OAuth session lives in the FlowGraph app, never the folder-vault MCP server, so hubs are never fabricated (charter #8).
args hubId projectId folderId
{}object_story
read
Included
One object’s biography (READ): the nine spine answers in reading order — What it is · Why · Where · Part of and made of · Feeds and fed by · Who is responsible · State and history · If it fails · How we know — each with its plain answer, provenance TIER (from the model / from documents / confirmed / computed / AI guess, unconfirmed) and the RECEIPTS backing it (the record or lifecycle card, its link tier, and the element GlobalId), plus the record + lifecycle timeline. Pure projection — nothing is written. `nodeId` is the model card, `globalId` the element.
args canvasId nodeId globalId
{
"canvasId": "cv_…",
"nodeId": "node_model",
"globalId": "2O2Fr$t4X7Zf8NOew3FLD1"
}object_coverage
read
Included
One object’s coverage + ranked open questions (READ): the story-coverage score (answered / askable weight over the asked spine slots) and the OPEN questions ranked by severity (blast radius × importance × answerability), each with its honest one-line why and — when the bank can ask it in-app — the `qid` you answer with `object_answer`. A question with no `qid` (answerableInApp:false) has no in-app form yet: route it to a person with `object_route_ask`. Pure — nothing is written.
args canvasId nodeId globalId
{
"canvasId": "cv_…",
"nodeId": "node_model",
"globalId": "2O2Fr$t4X7Zf8NOew3FLD1"
}object_answer
🔒 write
Included
File an answer to one OPEN spine question (WRITE → staged for human accept). Give the `qid` from `object_coverage` (answerableInApp:true) + your `answer`. It NEVER writes a fact directly: an agent-supplied answer stages as a Review-Inbox ghost-draft with provenance kind:"ai" (labeled AI, unconfirmed at every depth — provenance never launders, docs/50 §8), which a human accepts to turn into a fact with a receipt. Refused honestly for an unknown/already-answered qid or a locked atom.
args canvasId nodeId qid answer
{
"canvasId": "cv_…",
"nodeId": "node_model",
"qid": "truth.modelStatus",
"answer": "Coordinated as of the March issue; MEP clashes closed."
}object_lifecycle_event
🔒 write
Included
Record a lifecycle event about an object (WRITE → staged for human accept): installed / inspected / replaced / commissioned … As an ordinary FACT — a small card carrying the event + a `documented_in` element receipt, so it interleaves into the SAME Story timeline + Lifeline ribbon the app shows (never a parallel surface). `validTime` = when it happened (ISO or a plain date). Staged with provenance kind:"ai", tier ai-confirmed; a human accepts it. A locked model card is refused.
args canvasId nodeId globalId event validTime
{
"canvasId": "cv_…",
"nodeId": "node_model",
"globalId": "2O2Fr$t4X7Zf8NOew3FLD1",
"event": "inspected",
"validTime": "2026-05-14"
}object_route_ask
🔒 write
Included
Route ONE open question about an object to the person who knows it (WRITE → staged for human accept). It NEVER sends directly: the recipient link + email ride the app’s authenticated session (the /s/* rails), which the folder-vault MCP server does not hold — so the routed ask stages as a ghost-draft {audience, question, object} the human sends from the app (the aps_translate pattern). Use it for a high-severity question `object_coverage` reports as answerableInApp:false. Returns the staged request.
args canvasId nodeId globalId slot question audience
{
"canvasId": "cv_…",
"nodeId": "node_model",
"globalId": "2O2Fr$t4X7Zf8NOew3FLD1",
"slot": "stewardship",
"question": "Who owns the maintenance of this rooftop unit?",
"audience": "facilities@acme.com"
}One board = one .fgraph file. { id, name, nodes[], edges[], … }.
title, body, accent, icon, facets[], tags[], childCanvasId (nesting), meta. `links[]` holds card-level reference links ({id, title?, url}); set them via apply_graph_patch → updateCard, e.g. {op:"updateCard", nodeId, patch:{links:[{id, title:"Spec sheet", url:"https://…"}]}}. Urls are scheme-normalized (a bare domain gets https://) and only http/https/mailto persist. The digest surfaces each as a `link:` line.
A typed section: kind = list | text | select | actions | image | video | json; items[]. `key` is the STABLE identity every query/filter/semanticRole matches on; `label` is an optional display name a human rename may set on a built-in section (Inputs, Owner, Notes…), whose key is frozen. Match on `key`, show `label ?? key`. A digest heading reads `Ingredients (key: Inputs)` when the two differ — cite the key. `inputName` / `outputName` name the section's OWN input/output ports (like an item or a card can have) — set them via apply_graph_patch → updateFacet, e.g. {op:"updateFacet", nodeId, facetId, patch:{inputName:"Raw data", outputName:"Clean data"}}; connect the whole section by its facetId handle (target `facetId+"__t"`, source `facetId`). The digest surfaces them as `(in: Raw data, out: Clean data)` on the section heading, and the same `## Section (in: …, out: …)` marker round-trips through submit_knowledge_draft.
A list/action row: text, status (action mode), childCanvasId (item can nest), meta. `attachments[]` holds files ({name, mime, dataUrl}); an image attachment tagged `role:"preview"` is the item's click-to-preview image (else the first image is used). Attach one via apply_graph_patch → updateItem, e.g. {op:"updateItem", nodeId, facetId, itemId, patch:{attachments:[{id, name:"plan.png", mime:"image/png", dataUrl:"data:image/png;base64,…", role:"preview"}]}}. The digest surfaces it as `[image: <name>]`.
Typed relationship: relType + label. The graph, not a drawing.
The envelope on every atom: provenance, confidence, sources, assumptions, review, approval(+locked), owner, permissions{human, ai}, reviewDue, history.
A reusable action: a FacetItem with action.skill=true whose inner canvas (childCanvasId) defines HOW it runs. The agent-usable unit of capability — list with list_skills; promote via the app (human approves so a half-built inner canvas isn’t exposed).
feeds — A produces input for B (flow).
depends — A needs B (flow).
maps / exported_to / generated_from — lineage / transformation.
contains / part_of — structure / hierarchy.
blocks / contradicts — A blocks or conflicts with B (proof/flow).
proves / supersedes / approves / owns — proof & governance relations.
1) get_canvas_digest({canvasId}) — the canvas in FDG, every atom carrying an anchor ([c3], [c3.f2], [c3.f2.i4], [g1]). Focus a big canvas with nodeIds/hops. 2) Write a DRAFT in that same grammar: deepen existing anchors first, connect every new card to one, 2-4 sections × 3-6 real entries per new card, `> source:`/`> assume:` on non-obvious claims. 3) submit_knowledge_draft({canvasId, draft, digestId}) — a deterministic compiler emits the patch; nothing is written unless it all compiles, and a failure hands you indexed errors + drop reasons to correct. Use mode:"stage" to leave it as a ghost-draft a human accepts. Reach for apply_graph_patch only for surgical ops (a title fix, one edge) — hand-writing addCard/addFacet/addItem for CONTENT is how you end up proposing empty shells.
1) list_due_reviews({window:"overdue"}) — the same ranked agenda the human sees (by blast-radius). 2) For each, schedule_review / reschedule_review with a reason. 3) Everything lands as a governed change the human approves — you never sign off.
1) get_canvas + query_graph to inspect the atom and its neighborhood. 2) complete_review({findings}) records your pass + marks it done — but NEVER approves/locks. The human still signs off in the calendar/inbox.
Any write you make (a draft subgraph, a proposed review date, findings) surfaces for the human in TWO places: on the CANVAS as a ghost-overlay diff (Review Inbox), and in the CALENDAR/KANBAN as a scheduled/“AI proposed” item. Schedule a review to pull a human’s attention to a specific atom on a specific day.
list_skills({readyOnly:true}) → a skill’s innerCanvasId defines how it runs (get_canvas it). Only ready skills (non-empty inner canvas) are runnable; promotion is human-gated.
1) object_story({canvasId, nodeId, globalId}) — the nine answers with their provenance tiers and the receipts behind each. 2) object_coverage(...) — the coverage score + the OPEN questions ranked by severity. 3) For the top question: if it has a `qid` (answerableInApp), object_answer({qid, answer}) stages your answer as a review draft (kind:"ai" — a human accepts it into a fact); if it has none, object_route_ask({question, audience}) stages a one-question ask for the human to send to the person who knows. 4) object_lifecycle_event(...) records what happened to the object (installed/inspected/replaced) as a staged fact on the same timeline. Every write is review-gated: you propose a biography, the human confirms it.
1) GET /api/docs/shared-with-me — the graphs shared TO you and your role on each. 2) GET /api/docs/:id/shared — the role-scoped read: {role, canEdit, canComment, bundle}. 3) If canEdit: edit a COPY of that bundle, PRESERVING every card id (ids are what let the owner diff your work 1:1 — never re-id), then POST /api/docs/:id/propose with the edited closure. It becomes a PENDING proposal, never a silent overwrite: nothing changes on the owner’s graph until they approve. You get back {proposalId, summary:{added,removed,changed}} — and re-proposing REPLACES your earlier pending proposal (one pending per doc per proposer; the replacement is audited). 4) GET /api/docs/:id/my-proposal — YOUR latest proposal there: {proposal:{id,status,summary,at,resolvedAt}, bundle? (pending only)}. This is how you learn the outcome (approved/rejected) and re-read what you sent; /:id/proposals is owner-only. DELETE the same path to withdraw a pending one. 5) A view-only grant returns 403 on propose — that is the access model working, not an error to route around.
1) GET /api/docs/my-proposals — everything waiting on you, across every graph you own. 2) GET /api/docs/:id/proposals/:pid — the proposed closure; diff it against GET /api/docs/:id/shared (the current shared copy) to see exactly what the proposer changed. 3) POST /api/docs/:id/proposals/:pid/resolve {action:"approve"|"reject"}. Approving makes the proposal the shared copy; the proposer is notified and the decision is audited. Landing those edits on a LOCAL canvas is still a governed applyPatch write — approving is a decision, not a bypass.
A fresh install is local-first and calls none of these. Plan tiers here are proposed, not yet ratified — no plan badge is shown until a tier is certain.
/api/ai/chat
live
AI proxy — provider keys stay server-side (CORS-locked).
/api/canvas/:id
live
B1 server-authoritative write — session-bound actor + CAS rev (409 on stale baseRev, never a silent overwrite); viewer→403; AI cannot approve/lock.
/api/auth/*
live
Better Auth — magic-link + organization plugins (provision D1 + BETTER_AUTH_SECRET; 501 until then).
/api/me
live
Current session — userId / orgId / role (401 unauth, 501 until D1 provisioned).
/api/sync/canvas/:id
live
Opt-in vault sync (R2, last-write-wins) — legacy; superseded by the governed write.
/api/docs/:id/shares
live
Who has access to a graph · invite by email at a role (viewer=read | editor=propose — the only two roles the share surface enforces; commenting is a later phase). The invite uploads the graph closure first, so the grantee always has something to open.
/api/docs/:id/shares/:grantId
live
Revoke one person’s access; the next read refuses.
/api/docs/:id/links
live
Mint a share LINK (viewer/read-only — no edit or comment links). Optional: `scopeCardId` (+`contextVisible`) pins the link to ONE card; `requireAccount` gates the view behind sign-in; `public` makes it the anyone-with-the-link row.
/api/docs/:id/links/:linkId
live
Revoke a link.
/api/docs/:id/content
live
Upload the graph CLOSURE the share serves (a local-first canvas the server has never seen otherwise). Registers the canvas row.
/api/docs/shared-with-me
live
Every graph shared TO my account, with my role on each — the "Shared with you" list.
/api/docs/:id/shared
live
The role-scoped read of a graph shared with me: {role, canEdit, canComment, bundle}. 403 when it is not shared with my account.
/s/:token
live
The guest read of a share link — the link IS the capability (no session needed unless the link requires sign-in).
/api/docs/:id/propose
live
EDITOR (or owner) submits an edited closure as a PENDING proposal — never a silent overwrite. Returns {proposalId, summary:{added,removed,changed}, superseded:n}. Re-proposing REPLACES your earlier pending proposal on that doc (audited). 403 if you only have view access.
/api/docs/:id/my-proposal
live
PROPOSER-scoped (FF2-11): GET = my latest proposal on this doc — {proposal:{id,status:pending|approved|rejected,summary,at,resolvedAt}, bundle? (pending only)} — my receipt + the outcome; null when I never proposed. DELETE = withdraw my pending proposal (404 when nothing is pending).
/api/docs/my-proposals
live
OWNER inbox: every pending proposal across every graph I own, in one call — {id, docId, docName, proposer, summary, at}.
/api/docs/my-shares
live
OWNER truth (FF2-16): every doc I own with active grants/links — {docs:[{docId, name, grants:[{id,email,role}], links:[{id,role,isPublic,requireAccount,scopeCardId}]}], sharedCount}. Feeds the "Shared by you" list, card badges and the cap meter from ONE call. Revoke via DELETE /:id/shares/:grantId and /:id/links/:linkId.
/api/docs/:id/proposals
live
OWNER lists pending proposals on ONE graph (403 for non-owners).
/api/docs/:id/proposals/:pid
live
OWNER reads a proposed closure, to diff it against the current shared copy before deciding.
/api/docs/:id/proposals/:pid/resolve
live
OWNER approves (the proposal becomes the shared copy) or rejects. Body {action:"approve"|"reject"}. The proposer is notified either way; every decision is audited.
/api/attention/shares
live
OWNER shares a hand-picked set of checks to answer. Body {canvasId?, kind:"verification"|"finding", title?, entries:[{entryId,itemText,note?,cardTitle?,facetLabel?,canvasName?}], mode:"link"|"email", email?, requireAccount?}. Returns {setId, token? (link mode, shown ONCE)}. Attention shares do NOT count toward the canvas share cap.
/api/attention/my-shares
live
OWNER truth: every ask I sent — {sets:[{id, kind, title, count, entries, shares:[{kind:"grant"|"link", email?, requireAccount}], answers:[{entryId,text,by,at}]}]}. Feeds "Shared by you → Verifications".
/api/attention/shares/:setId
live
OWNER stops asking — revokes the set, every emailed grant AND every link in one call (a half-revoked ask is not a thing).
/api/attention/asked-of-me
live
RECIPIENT queue (session-email scoped): {sets:[{id, askedBy, entries:[{entryId,itemText,note?,answered}], pending, myAnswers}], pending}. Powers "Asked of you" in the verification window and lens.
/api/attention/asked-of-me/:setId/respond
live
RECIPIENT answers ONE entry. Body {entryId, text}. 403 unless the ask was granted to your session email and is still live; the owner gets a user-scoped notification.
/s/:token/answers
live
GUEST (no account) submits the response form for an ATTENTION link. Body {name?, answers:[{entryId,text}]}. Answers whose entryId is not in the shared set are dropped. 401 + signinRequired when the link requires sign-in; 410 when the owner stopped asking. Responses carry provenance {viaLinkId, name}.
/api/attention/shares/:setId/answers/:responseId/accept
live
OWNER accepts ONE answer — stamps it accepted and returns {entryId, text} so the client can tick that verification off through the governed write path. 403 for anyone but the person who asked.
/api/share
retired
RETIRED (FF2-3) — returns 410 Gone. Use POST /api/docs/:id/shares.
/api/share/:id
retired
RETIRED (FF2-3) — returns 410 Gone. Use GET /api/docs/:id/shared.
/api/share/:id/comments
retired
RETIRED (FF2-3) — returns 410 Gone. Doc comments are the queued CO4 phase.
/api/share/:id/revoke
retired
RETIRED (FF2-3) — returns 410 Gone. Use DELETE /api/docs/:id/shares/:grantId.
/api/ai/generate
scaffold
Server-side structured generation.
/api/aps/status
live
AP1/AP7a — APS posture: configured + mode (platform|byo|off) + platform quota {remaining,resetAt} (env/ledger-only; no secret, no upstream call).
/api/aps/token
live
AP1 — BYO APS 2-legged token (viewables:read/data:read only); client_secret stays server-side; 501 until APS_CLIENT_* set.
/api/aps/auth/login
live
AP1 — 3-legged authorize redirect (ACC scopes); refresh token kept in an httpOnly cookie, never in JS.
/api/aps/auth/callback|refresh
live
AP1 — code→token exchange + access-token rotation (secret held in the worker).
/api/aps/derivative
live
AP1/AP7a — thin Model Derivative passthrough; worker mints the write-scoped token. Platform mode meters SUBMITS (day caps → honest 429 {resetAt}; BYO never metered).
/api/aps/acc/*
live
AP1 — thin Data Management read passthrough with the user’s 3-legged bearer (AP5 ACC browser builds on this).