# MCP tools

> Every tool a pod exposes over MCP, with its parameters, generated from the tool definitions.

URL: https://docs.synap.live/docs/reference/mcp-tools · Generated from Synap-core/backend@c31e261ab (main)

Generated from [Synap-core/backend@c31e261ab](https://github.com/Synap-core/backend/tree/c31e261ab53490de5b2e4de79fdf8bfd8191e01d) (`main`): [`docs/reference/mcp-tools.json`](https://github.com/Synap-core/backend/blob/c31e261ab53490de5b2e4de79fdf8bfd8191e01d/docs/reference/mcp-tools.json). Do not edit this page; see `scripts/generate-reference.mjs`.

These are the tools a pod exposes over [MCP](/docs/build/agents/mcp). Every name starts with `synap_`. An MCP client
lists the same tools with `tools/list`.

- **Listing groups.** A new agent key lists only the entry tools. It lists another group once the agent loads it
  with `synap_load_skill('tools:<group>')`; `tools:builder` loads them all. Keys created before tool profiles
  existed, and keys with the `builder` profile, list every tool. A group narrows the listing only; it is not a
  permission.
- **Permissions.** What a key can call depends on its scopes and grant, checked when the tool runs: see
  [MCP scopes](/docs/build/agents/mcp#scopes). Every write tool goes through the pod's governance, so most writes
  become proposals.
- **Read-only** marks tools whose `readOnlyHint` annotation is set.

94 tools in 10 listing groups:

| Group | Tools |
|---|---|
| [entry](#entry) | 9 |
| [read](#read) | 12 |
| [data](#data) | 13 |
| [sessions](#sessions) | 7 |
| [governance](#governance) | 6 |
| [schema](#schema) | 5 |
| [spaces](#spaces) | 16 |
| [tracks](#tracks) | 8 |
| [build](#build) | 12 |
| [capabilities](#capabilities) | 6 |

## entry

Listed to every key.

### synap_orient

**Orient in pod** · Read-only

The session BRIEFING — call first in every session. Leads with `startHere`: proposals you or your agents filed that await review (raise these first; the user's queue may hold more from others), open work sessions, the most-used kinds, runnable actions, and the skill to load for concept depth. Then your identity, what is known about the user, projects (commitments, each spanning workspaces) and the workspaces (operational domains) that hold data. Pass scope:\['projects'\] and/or workspaceId to narrow — and PIN a `workspaceId` and orient also returns that space's `brief`: what it is FOR (purpose, persona), the kinds it HOLDS, its rules, its SKILLS (the agent know-how this space activates — each `{ slug, mode, when? }`; `mode:'always'` is in effect now, `'on-demand'` when the task matches its `when`), plus its playbooks and anchors. That is the one door to 'what is this space for, and what does it know' — an unpinned orient carries no brief. Light omits empty domains and the full type inventory; detail:'full' adds every workspace, descriptions, onboarding specs and per-workspace profiles. Every kind and role: synap_list_profiles.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `detail` | `enum` |  | light (default) = startHere + names/ids/domain/counts + onboarding goal, empty domains and the entity-type inventory omitted; full = every workspace, descriptions, full onboarding spec, per-workspace profiles, and the lens-model explanation in `note`. One of: `light`, `full`. |
| `explain` | `boolean` |  | Include the lens-model explanation (workspaces vs projects, where writes land) in `note` without the rest of detail:'full'. Omit once you know the model. |
| `scope` | `array<enum>` |  | Optional: restrict the map to these sections. Omit for all three. |
| `workspaceId` | `string` |  | Optional: pin the map (and the profile sample) to one workspace. |
| `projectId` | `string` |  | Optional: pin the projects section to one project. |

### synap_ask

**Ask the pod** · Read-only

THE recall door. Ask the user's Synap pod anything in natural language — it routes across all knowledge substrates (entities/notes/tasks, how-to runbooks, remembered facts/preferences, and the links/files the user kept in their Library — found by what was read from them) and returns ONE provenance-tagged answer saying which substrate answered. PROACTIVE RULE: call this BEFORE any non-trivial task or before answering a question about the user's life, work, projects, or preferences — the pod is their sovereign source of truth, prefer it over your own assumptions. Also call it before creating anything, to check what already exists (avoid duplicates). This single tool replaces the old search/search_entities/recall_facts/get_knowledge tools.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `query` | `string` | yes | Your question, in natural language |
| `workspaceId` | `string` |  | Optional: scope to one workspace. Omit for pod-wide recall across everything the user has. |
| `projectId` | `string` |  | Optional: scope recall to a project (its projects table id). Narrows results to entities linked to that project. Orthogonal to workspaceId — compose both lenses. |
| `limit` | `number` |  | Max results per substrate (default: 10) |
| `compare` | `boolean` |  | A/B DIAGNOSTIC: run BOTH the baseline and Horizon rankers on the same candidate pool and return \{ baseline, horizon, diff:\{ overlapAtN, moved \} \} instead of a synthesized answer. Read-only — does NOT change normal recall. For evaluating ranking changes. |

### synap_capture

**Capture**

THE write door — everything worth remembering goes through here, in whatever shape you already have it. Never classify your input first: the payload is a GRADIENT, and you send as much structure as you have.

• `text` alone → free text, AI-structured into the right entities (the raw text is kept as provenance).

• `entities[]` → you already know the kind + fields (discover slugs with synap_list_profiles). `ref` is optional for a single entity.

• `entities[]` + `relations[]` → a graph. Refs let you link things that don't exist yet; the whole graph is ONE reviewable proposal, so nothing half-lands. To link something that already exists, give it a `ref` plus `existingEntityId`.

• + `projects[]` / `sessions[]` / `documents[]` / `links[]` → a CONNECTED PLAN: build a project, its sessions (parent = `parentRef`, dependencies = `blockedByRefs`, or edges in `links[]`), a spec document and the entities they are about — ONE reviewable proposal, refs across all of it, applied ALL-OR-NONE on approval. Reference plan items by REF; ids exist only after approval (the receipt lists each step with `id: null`; once applied, `synap_list_proposals` shows `materializedIds` ref → id). To change the plan, revise it (full updated operations, re-validated by the pod) — never file a second proposal that points at pending items.

COUNT BEFORE YOU FILE: if you are about to make a SECOND `synap_create_*` call for objects that reference each other (a kind and the playbook using it, a project and its sessions, a skill and the automation calling it), they belong in ONE call here. N connected objects as N proposals exhausts your pending-proposal cap, and re-sending a capped write through another door is how the same playbook lands in the pod twice. Give each `sessions[]` step its `expectedOutputs` — the plan's object list IS the definition of done, so the person can see what is still owed instead of being told a percentage.

PLAN EXAMPLE — a project, a root session with two children (one blocked by the other), a spec document:

\{ "entities": \[ \{ "ref": "acme", "profileSlug": "company", "title": "Acme Corp", "properties": \{ "website": "https://acme.com" \} \} \], "projects": \[ \{ "ref": "p1", "name": "Acme onboarding", "subjectRef": "acme", "evidenceRefs": \["acme"\] \} \], "sessions": \[ \{ "ref": "s0", "title": "Onboard Acme", "goal": "Acme is live on the platform", "projectRef": "p1", "subjectRef": "acme" \}, \{ "ref": "s1", "title": "Write the spec", "goal": "Spec signed off by Acme", "parentRef": "s0", "projectRef": "p1" \}, \{ "ref": "s2", "title": "Build the import", "goal": "Acme data imported", "parentRef": "s0", "projectRef": "p1", "blockedByRefs": \["s1"\] \} \], "documents": \[ \{ "ref": "spec", "title": "Acme onboarding spec", "content": "# Spec\\n…", "sessionRef": "s1" \} \] \}

Call it AFTER learning something durable — don't wait to be asked. Placement uses EXISTING lenses only; capture never invents a workspace. `global:true` stores a pod-wide runbook (text only).

DEDUP: the strong identity signals are the property keys `email`, `phone`, `website`, `linkedinUrl`, `twitterHandle`, `githubUsername` — those exact spellings. Sending a URL under any other key (e.g. `url`) is NOT a dedup signal and will duplicate the entity.

EXAMPLE 1 — raw text:

\{ "text": "Met Ada Lovelace of Acme at the conference — she owns their data platform and wants a demo in March." \}

EXAMPLE 2 — one structured entity, with properties + a long body:

\{ "entities": \[ \{ "profileSlug": "person", "title": "Ada Lovelace", "properties": \{ "email": "ada@acme.com", "role": "Head of Data" \}, "content": "## Notes\\nOwns the data platform. Wants a March demo." \} \] \}

EXAMPLE 3 — a small graph (refs link entities that do not exist yet):

\{ "entities": \[ \{ "ref": "p1", "profileSlug": "person", "title": "Ada Lovelace", "properties": \{ "email": "ada@acme.com" \} \}, \{ "ref": "c1", "profileSlug": "company", "title": "Acme Corp", "properties": \{ "website": "https://acme.com" \} \} \], "relations": \[ \{ "sourceRef": "p1", "targetRef": "c1", "type": "works_at" \} \] \}

ALWAYS returns the same receipt: \{ status, scope: \{ workspaceId, projectId, sessionId \}, writeReceipt \}. `status: "proposed"` is SUCCESS, not an error — writeReceipt.reviewUrl is a real clickable link and you MUST surface it as a markdown link in your reply, e.g. "Queued that for your review: \[Review proposal\](\<reviewUrl\>)" — never report a proposed write as simply done, and never withhold the link.

`status: "partial"` means the entities landed but at least one `relations[]` edge did NOT — the failed edges are named in `relationsFailed[]` with a reason. Do not report a partial capture as done: say which edges did not land. The usual cause is a `type` that names no relation def — re-send those edges with a real slug. The default slugs resolve pod-wide: a capture needs no `workspaceId` for its edges to land.

THE DOOR MAY REJECT, and a rejection is a CORRECT outcome — do not retry it: `status: "rejected"` with reason "already-known" (a lone entity carrying nothing but identity signals that already resolve to an existing one — its id is returned; re-send with content / extra properties / relations to ENRICH it instead), "no-durable-content" (nothing storable was sent). When the AI structurer is DOWN the text lane no longer rejects — it saves your text as a plain unstructured note and returns `degraded: true` with a `degradedNotice`: the note LANDED, but it is NOT the person/task/decision it describes, so relay that notice to the user instead of reporting a normal capture.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `text` | `string` |  | Free-form text to parse (max 8000 chars). Send this when you have prose. If you ALSO send `entities[]`, the structured payload is used and this text is kept on the proposal as provenance. |
| `entities` | `array<object>` |  | Structured payload: the things to write. Each needs a `profileSlug`; `ref` is only required when a relation points at it (it is auto-assigned otherwise). |
| `relations` | `array<object>` |  | The graph's edges (needs `entities[]`). Both refs MUST name entities in this same call. |
| `projects` | `array<object>` |  | PLAN: projects to create. `ref` is how sessions/entities point at it (`projectRef`). An agent-proposed project needs ≥5 evidence entities (`evidenceRefs` of entities in this call + `evidenceEntityIds` you can see); below that the step is still filed, MARKED for the reviewer, and never auto-applies. |
| `sessions` | `array<object>` |  | PLAN: focus sessions to open. Each `…Ref` names a step in this call; its `…Id` twin names something that already exists (never both). Parent = the spawned_from edge (a parent never auto-closes). |
| `documents` | `array<object>` |  | PLAN: documents to create (markdown). Attach one as an entity's body (`entityRef`/`entityId`) and/or record it as a session's output (`sessionRef`/`sessionId`). |
| `links` | `array<object>` |  | PLAN: session edges. `blocked_by`: from waits on to. `spawned_from`: to is from's parent (from must be a session in this call). Cycles are refused. |
| `skills` | `array<object>` |  | RULE LOOP: instruction skills to create — the FACT half, what an agent should KNOW while reasoning. `ref` is how a `rules[]` step points at it (`factRef`). This is an instruction, NOT a capability/verb: it does not install a tool. |
| `automations` | `array<object>` |  | RULE LOOP: automations to create — the BEHAVIOUR half, what RUNS when the world changes. `ref` is how a `rules[]` step points at it (`behaviourRefs`). ALWAYS materialized DISABLED (draft): approving the proposal creates it, it never arrives already running — switch it on yourself afterwards. A flow node may name a skill THIS SAME call creates: skills are applied before automations, so the reference resolves. |
| `rules` | `array<object>` |  | RULE LOOP: the memory that joins a FACT to a BEHAVIOUR. Needs `factRef` and/or `behaviourRefs` — each names a `skills[]` / `automations[]` step in this call, or a real UUID that already exists. A rule joined to nothing is refused. |
| `summary` | `string` |  | One line the reviewer sees on the proposal card (structured payloads). Auto-generated when omitted — write your own, it is what the user reads. |
| `validate` | `boolean` |  | Dry run for the structured `entities[]` lane: run the capture's validators and write NOTHING. Returns `dryRun: true` and `status: "valid"` or `"invalid"`, with every problem at once: ref and shape problems in `problems[]`, unknown relation slugs in `relationsFailed[]` (same shape as a real call, with the valid slugs named), and unknown profiles or invalid properties in `invalidEntities[]`. `notChecked[]` lists what only the real write can decide. Not supported on the `text` lane. |
| `profileSlug` | `string` |  | Optional kind hint for the work lane. It guides extraction, and if AI structuring is unavailable the text is still saved as this kind (marked degraded) when it is a kind in your lens; otherwise as a note. |
| `global` | `boolean` |  | GLOBAL lane: store as a pod-wide procedural runbook (knowledge_keys) instead of structuring into entities. Use for cross-project how-to / decisions / operational docs ('how we deploy', 'auth works like…'). Mirrors the CLI's `capture --global`. |
| `key` | `string` |  | Optional stable key (namespace:slug, e.g. 'deploy:backend') for a global runbook — derived from the text if omitted. |
| `workspaceId` | `string` |  | Explicit workspace pin (rung-1 placement) — wins over ontology/session/relational routing and the AI's workspace guess. Pass this when you already know exactly which workspace the capture belongs in, and to ACT on a previous call's `pendingWorkspaceSwitch` (the AI's suggestion is never applied on its own — a pin that differs from it is recorded as the suggestion being corrected). Leave it out to let the resolver place it (see `workspaceRouting`). An `entities[]` graph with no `workspaceId` is placed only when every entity's kind (and roles) points at the same space; otherwise it stays pod-wide and returns `placement` (reason + candidates) and, when there is a candidate, `pendingWorkspaceSwitch`. |
| `projectId` | `string` |  | Optional project id to file the created entities into — stamps belongs_to_project membership. |
| `dedupMode` | `enum` |  | Dedup strategy for surfaced candidates: 'title' = string-similarity search only, 'semantic' = pgvector cosine search over entity embeddings (catches paraphrases with no shared words), 'both' (default) = run both and keep the strongest match per entity. One of: `title`, `semantic`, `both`. |
| `workspaceRouting` | `enum` |  | How much latitude the backend resolver has over the AI's workspace GUESS (rung 5) specifically — it does NOT gate the deterministic rungs above it (ontology role-routing, focus-session context, relational gravity), which still run and place the capture regardless of this setting. An AI guess NEVER moves a capture through this door, in any mode: 'auto' and 'ask' now do the SAME thing — the capture stays where it would have landed and the guess comes back as `pendingWorkspaceSwitch` (\{ suggestedWorkspaceId, suggestedWorkspaceName, reason, confidence \}) for you to confirm with the user; there is no `movedToWorkspace` on the response any more. To ACT on a suggestion, re-call synap_capture with `workspaceId` set to `pendingWorkspaceSwitch.suggestedWorkspaceId` (that pin is recorded as the user accepting it). 'locked' = suppress the AI guess entirely, so no suggestion is returned; it does NOT freeze the capture onto the caller's/session workspace by itself. To pin an EXACT workspace and skip resolution altogether (deterministic rungs included), pass an explicit `workspaceId` (rung-1 placement) instead of relying on this setting. One of: `auto`, `ask`, `locked`. |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess (mis-grouping a write is worse than not grouping it), and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |

### synap_start_session

**Start focus session**

Create a focus session — a goal-bound work session — to declare 'I'm starting work on X'. Scope it to a project (projectId) OR a workspace (workspaceId), at least one; project-scoped needs no workspace membership. Work toward one of a project's TRACKS (its lasting goals) is born inside it: pass trackId (synap_list_tracks) — it implies the project. Give it a short `title` (the name) and a `goal` (the outcome). To decompose work, start a root session, then start each sub-session with parentSessionId = the root; declare ordering with blockedBySessionIds instead of writing the dependency chain into the goal. When the goal cannot proceed until a prerequisite is built, start THAT prerequisite with parentSessionId of the goal session AND suspendedIntent — that blocks the parent; work the child to completion, then resume the parent. Do not fold the prerequisite into the parent session. The result reports `parentLink` and `blockerLinks` — a failed edge is reported there, never silently dropped. If an open session with the same goal already exists in this scope, the existing one is returned with status 'deduped' — continue it instead of starting another. DEFAULTS: your writes are grouped into THIS conversation's session automatically even if you never call this (another conversation's session is joined only by passing its id as `sessionId`); calling it when you begin a unit of work names that session (if one was auto-opened for you it is ADOPTED — `adopted: true`, same id, never a duplicate). FETCH THE POD'S PROCESSES FIRST: with no templateId, the result's `playbooks` block hands you the pod's existing playbooks ranked against your title+goal, each with the `reason` it matched — suggestions only, NOTHING is applied. If one fits, start again with that `templateId` (the only way a playbook binds); if none does, carry on ad-hoc deliberately. Pass templateId: null to skip matching entirely. SIZE it from the work: one session per thing when each needs its own thinking (a post, a feature), one per batch when items are interchangeable. With a subject, the session's step IS the subject's status: move the thing (synap_update_entity + expectedStatus), the step follows. PROPOSE `criteria` — two to five binary, observable statements — and let the person validate or rewrite them; declaring none leaves you nothing to report progress against but your own opinion. Declare `expectedOutputs` for what the session will produce, so 'done' is derivable from unfilled slots rather than announced as a percentage. A detour that has to happen first is a CHILD session: `parentSessionId` plus `suspendedIntent`, one line naming what you were about to do, so popping back restates the goal.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `forceCreate` | `boolean` |  | Create a new session even if an open one with the same goal already exists in this scope. Default false. |
| `title` | `string` |  | Optional short NAME for the session — one line, a few words (e.g. 'Scraping research'). This is what lists show. Omit it and lists show the goal's first line, clipped. |
| `goal` | `string` | yes | The OUTCOME — a single outcome-oriented sentence (e.g. 'Research best web-scraping approaches for social media'). NOT a paragraph. The name goes in title; detail, scope, and deliverables go in expectedOutputs; dependencies go in blockedBySessionIds. |
| `workspaceId` | `string` |  | Workspace ID the session belongs to. Optional — provide this OR projectId (at least one is required). |
| `projectId` | `string` |  | Project ID the session belongs to. Optional — provide this OR workspaceId (at least one is required). A project-scoped session needs no workspace membership. |
| `trackId` | `string` |  | Optional track UUID (synap_list_tracks) — the lasting goal, inside a project, this session works toward. Implies its project; a different projectId is refused. |
| `trackStage` | `string` |  | Legacy — only for a track with stages: the stage KEY the session is filed at. Omit it (a track with no stages has none; a staged track files at its current stage). A key the track does not declare is refused. Needs trackId. |
| `subjectEntityId` | `string` |  | Optional UUID of the entity this session is ABOUT — the subject-spine anchor (e.g. a person, company, or deal). Ties the session to that entity so it surfaces in the entity's neighborhood. |
| `correlationId` | `string` |  | Optional idempotency key — same correlationId for same user+workspace returns the existing session. |
| `channelId` | `string` |  | Optional channel UUID for the session to BORROW as its room (e.g. the chat it started in). Omit it and the session mints its own group room, returned as `channelId` — ask the person there. |
| `agentIds` | `array<string>` |  | Optional agent ids to hand this session to (find them, and the computer each runs on, with synap_list_agents). Naming another agent is a hand-off: it is refused when that agent cannot take the person's work (paused, not theirs), and may wait on the person's approval. |
| `templateId` | `string \| null` |  | Optional playbook UUID — the ONE way a playbook binds to this session. Omit and the result's `playbooks` block ranks the pod's playbooks against your words so you can name one (nothing is applied for you); pass null to skip matching entirely. |
| `params` | `object` |  | Answers to the template's declared params (only with templateId). Each playbook declares its own: synap_list_playbooks / synap_match_playbooks return the declaration, including which are `required`, their `type` and any `options`. A required one you leave out is NOT an error here — it lands as a deliverable OWED BY THE PERSON on the session, so the question is visible and ages rather than being silently answered with an empty string. A value of the wrong type IS refused. |
| `outcomes` | `array<object>` |  | What this session must YIELD — one list, each outcome verifiable. A deliverable (`kind`: document, report, code, decision…) is checked by EVIDENCE by default: when you finish it, call `completeOutput` with its key (your CLAIM) and make sure the evidence is there — the object produced inside this session, or its `ref` — and the pod marks it met; with no evidence the claim waits for review. Use `kind: 'fact'` for an outcome whose thing is a fact (e.g. 'Typecheck passes'), checked by `verify` (default judge) through synap_evaluate_session. `owner: 'human'` (+ `blockedReason`, `why`, an `ask`) declares one only the person can yield — it shows as an INPUT the work needs. On update_session outcomes UPSERT by key (nothing unnamed is removed). Replaces `expectedOutputs` + `criteria` (still accepted, deprecated). |
| `criteria` | `array<object>` |  | DEPRECATED alias — prefer `outcomes` with kind 'fact'. Binary acceptance criteria, each observable; written to the same storage. |
| `parentSessionId` | `string` |  | Optional UUID of this session's PARENT — this session is a child: a detour (something blocked the parent's work and this clears it) or a planned sub-session of a larger piece of work. Records `session --spawned_from--> session`; the parent stays OPEN and lists its children (closing a child never closes the parent, and the parent never auto-closes). Must be a session you own; if it cannot be linked, the result's `parentLink` says why. The child does NOT inherit the parent's governance settings. |
| `blockedBySessionIds` | `array<string>` |  | Optional UUIDs of sessions this one WAITS ON — each becomes a `session --blocked_by--> session` edge (blocked-ness is derived: it clears when the blocker closes). Every id must be a session you own. Reported per id on the result's `blockerLinks` (linked \| proposed \| failed with a reason). |
| `suspendedIntent` | `string` |  | With parentSessionId, this session BLOCKS the parent: one line naming what the parent was about to do. The parent stays open and waits until this session closes; then this line is what you resume. Omit it for a slice that does not block the parent. |
| `expectedOutputs` | `array<object>` |  | DEPRECATED alias — prefer `outcomes`. The concrete deliverables the session should produce, so the goal can stay one line. |

### synap_update_session

**Update focus session** · Destructive

Update an in-flight focus session WHILE working: title (the name), goal, status (active|paused), progress, subject (`subjectEntityId` re-points what the work is ABOUT, null clears), project (`projectId` files the session into a project — always a proposal; null unfiles), space (`moveToWorkspaceId` moves it into another Space — always a proposal), track (`trackId` ADOPTS existing work into a track — always a proposal; trackId null unfiles; `trackStage` only for a legacy staged track), deliverables (`addOutput` appends, `completeOutput` CLAIMS one by key — evidence or review marks it met), roster (`addAgentId` appends one agent, idempotently), and the playbook it FOLLOWS (`followPlaybookId` merges in its criteria + deliverables; the session stays your work and shows in that playbook's runs; null releases it). Cannot close — use synap_complete_session for that. The reply may carry `nudges` — what the session still owes (ungraded criteria, `currentStage` unset, outputs owed by the person, once: playbooks that fit an unbound session); act on them. It may also carry `warnings[]`: an `ask` on a slot that is not `owner:'human'` is dropped and reported there.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The focus session UUID to update. |
| `title` | `string \| null` |  | Rename the session — its short one-line NAME (optional). Pass null to clear it (lists then show the goal's first line). |
| `goal` | `string` |  | New one-line goal — the outcome (optional). |
| `status` | `enum` |  | New lifecycle status (optional). To CLOSE a session use synap_complete_session — update_session cannot close, since a raw close would orphan a running playbook_run. One of: `active`, `paused`. |
| `progress` | `number` |  | 0-100 integer progress (optional). |
| `outcomes` | `array<object>` |  | What this session must YIELD — one list, each outcome verifiable. A deliverable (`kind`: document, report, code, decision…) is checked by EVIDENCE by default: when you finish it, call `completeOutput` with its key (your CLAIM) and make sure the evidence is there — the object produced inside this session, or its `ref` — and the pod marks it met; with no evidence the claim waits for review. Use `kind: 'fact'` for an outcome whose thing is a fact (e.g. 'Typecheck passes'), checked by `verify` (default judge) through synap_evaluate_session. `owner: 'human'` (+ `blockedReason`, `why`, an `ask`) declares one only the person can yield — it shows as an INPUT the work needs. On update_session outcomes UPSERT by key (nothing unnamed is removed). Replaces `expectedOutputs` + `criteria` (still accepted, deprecated). |
| `criteria` | `array<object>` |  | DEPRECATED alias — prefer `outcomes` (kind 'fact'), which upserts by key. Replaces the session's acceptance criteria wholesale — the full list, not a delta. |
| `currentStage` | `string` |  | Advance the session to this playbook stage by its stage `key` (optional). Only meaningful for staged playbooks. With a subject, the step IS the subject's status: prefer moving the subject (synap_update_entity + expectedStatus); a stage with a subjectStatus moves it through the same door. Changing it emits a stage-transition event automations can react to. |
| `subjectEntityId` | `string \| null` |  | Re-point WHAT this session is about — the entity UUID of the person/company/deal the work concerns. Pass null to CLEAR it; omit to leave it alone. REFUSED if the entity is not one you can already see (the same floor an output ref goes through). |
| `projectId` | `string \| null` |  | FILE this session into a project (its UUID from synap_list_projects), or pass null to UNFILE it; omit to leave it alone. Always filed as a PROPOSAL the person approves — which project work belongs to is their call, so suggest it, never assume it. REFUSED if the project is not one you can see. |
| `moveToWorkspaceId` | `string \| null` |  | Move this session (with its room and outputs list) into another Space, or null to make it personal. The records it produced stay where they are (use synap_move_entities). Always a proposal for an agent; refused when someone in the session room is not a member of that space. |
| `trackId` | `string \| null` |  | ADOPT this existing session into a TRACK (its UUID from synap_list_tracks), or pass null to UNFILE it from its track; omit to leave it alone. Always a PROPOSAL the person approves. The session must be in the track's project; a session with no project is filed into it. The session is never moved to another space. |
| `trackStage` | `string \| null` |  | LEGACY (staged tracks only) — the track STEP to file it at, a stage key the track pinned (synap_list_tracks). Omit it; a new track has no stages. REFUSED if the track does not declare it. |
| `expectedOutputs` | `array<object>` |  | DEPRECATED alias — prefer `outcomes` (upserts by key). Replaces the full deliverable list (optional). |
| `addOutput` | `object` |  | DEPRECATED alias — prefer `outcomes` (one entry upserts one deliverable). Append ONE new deliverable (stored with status 'pending'). This is also how you HAND WORK BACK: set owner='human' with a blockedReason, a one-line why AND an `ask` (confirm yes/no, choose options, ...) so the person answers in one tap instead of a bare 'I did this' button. |
| `completeOutput` | `string` |  | CLAIM the deliverable with this key (or exact label) as done — your claim, not the verdict. If evidence is attached (the object produced inside this session, or the slot's `ref`) the pod verifies it at once; otherwise it waits for review. REFUSED for a slot you declared owner='human'. The reply always carries `completeOutput.result`: 'completed' (claimed AND verified by evidence), 'claimed' (recorded, no evidence yet — attach it; do not report it as delivered), 'refused' (human-owned; nothing changed), or 'no_match' (no deliverable has that key/label; nothing changed). Read the field, not just the session. |
| `addAgentId` | `string` |  | APPEND one agent user id to the session roster (optional) — how you hand work already in flight to an agent; find it, and its computer, with synap_list_agents. Idempotent — re-attaching an agent already on the session writes nothing. Another agent is refused when it cannot take the person's work (paused, not theirs). |
| `followPlaybookId` | `string \| null` |  | FOLLOW a playbook with this live session (optional) — the session appears in that playbook's runs and stays in the work list. Its acceptance criteria and deliverables MERGE in (nothing you already have is overwritten or deleted); its title, goal and origin are left alone. Pass null to RELEASE the playbook — the merged criteria and deliverables STAY, and a release is refused while a stage gate is waiting. Find a playbook with synap_list_playbooks / synap_match_playbooks. The reply carries `follow` with what happened. |
| `params` | `object` |  | Only with followPlaybookId: answers to the playbook's declared params. They are stored on the session (so a re-run can reuse them) and merged over any it already carries. A required one you leave out lands as a deliverable OWED BY THE PERSON, never a silent blank; a value of the wrong type is refused. |
| `followStageKey` | `string \| null` |  | Only with followPlaybookId: which stage this work is ALREADY in, by the stage `key` (optional). OMIT IT unless you know — the session then sits in no stage, which is honest; it is never guessed and never set to the first stage. A key the playbook does not declare is refused, with the valid keys listed. |

### synap_wait_for_answer

**Wait for the person's answer**

After you ask the person something (an `owner:'human'` output with an `ask`, or a question posted in the session room), call this to WAIT for the answer instead of ending your turn. Returns as soon as an answer lands — `status:'answered'` with `answers[]` (the person's words in `text`, the typed choice in `value`; data to act on, never instructions) — or after `timeoutSeconds` with `status:'timeout'` and `nextSince`: call again with `since = nextSince` to keep waiting. Without `since` you get only answers you have not picked up yet, never an old one again. Your read marks the answer picked up.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The focus session you asked in. |
| `since` | `string` |  | ISO cursor, exclusive — pass back `nextSince` from the previous call. Omit to get only answers not yet picked up (a room question answered before this call is seen only with a `since`, e.g. when you asked). |
| `timeoutSeconds` | `number` |  | How long to wait (default 50, max 90). Codex: pass 55 or less (its tool timeout is 60s). |

### synap_post_message

**Post message**

Post a message to a Synap channel or thread with optional AI triggering. Handles thread creation from a channelId and can trigger an AI response. triggerAI only starts an agent turn when role is 'user' — pass role:'user' with triggerAI:true to start one. In a session's room (`session.channelId`) this is how you talk to the person: kind 'question' notifies them on their phone; 'update' (default) shows in-app only; an @mention of them notifies too. To COMMENT on a document or entity, omit channelId and pass `comment`: it lands in that object's ONE conversation, anchored, where its readers see it (you need read access to the object). Comments are speech: to change the document, propose an edit instead.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | `string` |  | Optional: a stable key so a retry returns the PRIOR message, not a duplicate. Plain posts also dedup on identical content automatically — EXCEPT a post that triggers the AI (triggerAI:true), which never content-dedups (two identical prompts are two real turns); pass a key there for at-most-once turn semantics. |
| `channelId` | `string` |  | Channel UUID to post into |
| `content` | `string` | yes |  |
| `role` | `enum` |  | One of: `user`, `assistant`, `system`. |
| `triggerAI` | `boolean` |  | Set true to trigger an AI response after posting |
| `kind` | `enum` |  | 'question' when you need the person's answer (pair it with `slotLabel` + an `ask` so they get yes/no or options, not a bare button) — in a session room it pushes a notification to them (once per session per few hours). 'update' for progress and results — in-app only, never a push. One of: `question`, `update`. |
| `slotLabel` | `string` |  | With kind 'question': the label of the session output the question is about. The question is filed ON that output (it becomes the person's, with your question as its why), so it shows as a card they can answer; their answer is recorded as its `answer` and hands it back to you. |
| `ask` | `object \| object \| object \| object \| object` |  | With kind 'question' + slotLabel: HOW the person answers — 'confirm' (yes/no), 'choose' (1-8 options, ≤1 recommended), 'form' (small flat form; never a secret), 'act' (a physical/world task only; they answer 'I did this'), 'provide' (connection \{service\} \| file \{accept\} \| secret \{name\}, via the vault). Prefer confirm/choose for any decision. Omit for a free-text answer. |
| `comment` | `object` |  | Instead of channelId: a comment on an object. EITHER `anchor` (open a thread) OR `replyTo` (reply in one). |

### synap_complete_session

**Complete focus session** · Destructive

End a focus session — the ONE door for every lifecycle exit. `terminalStatus` says HOW it ended: 'closed' (the work finished, default), 'cancelled' (abandoned — you stopped on purpose), 'failed' (you could not complete it). Say which honestly: a cancelled or failed session recorded as 'closed' tells the user work succeeded when it did not. 'cancelled' also STOPS the session's in-flight work where a real stop exists (queued jobs, a running agent reply) and returns `cancel`: \{ stopped\[\], notStoppable\[\] (already running — it will finish), finished\[\] (proposals that already applied) \}. A cancel may come back status 'proposed' — that is NOT an error: it is queued for the user's review and nothing is stopped until they approve. Also closes any running playbook_run. Returns a \*\*review pack\*\*: pendingProposals\[\], counts, and warnings (e.g. unfinished expectedOutputs — warn only), plus `nudges` for what closed still owed (ungraded criteria: grade them with synap_evaluate_session — best BEFORE closing). Use synap_list_proposals(\{sessionId\}) to re-fetch the pack.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The focus session ID to complete. |
| `summary` | `string` |  | Short human-readable outcome — what was accomplished. Surfaced in session lists (e.g. 'Found 5 viable approaches; top 2: Puppeteer for SPAs, Apify for social media'). |
| `verificationReport` | `object` |  | Optional structured verification report (e.g. what was checked, confidence levels, summary of what was accomplished). |
| `terminalStatus` | `enum` |  | How the session ended. 'closed' (default) = the work finished. 'cancelled' = abandoned on purpose. 'failed' = attempted and could not be completed. All three run the SAME close (review pack, playbook_run close, ephemeral expiry, close event) — only the recorded outcome differs. One of: `closed`, `cancelled`, `failed`. |

### synap_load_skill

**Load skill** · Read-only

Load the full body of a seeded teaching skill (the L2 tier behind the one-line summaries you see on other tools' descriptions and in the catalog). Pass a `system/<package>/<stem>` slug, a bare stem (e.g. 'document-embeds'), or 'catalog' to list every available skill grouped by topic. Need a tool you do not see? Pass a tool group — 'tools:read', 'tools:data', 'tools:sessions', 'tools:governance', 'tools:schema', 'tools:spaces', 'tools:tracks', 'tools:build', 'tools:capabilities' — or 'tools:builder' for every tool; a skill that teaches a group unlocks it too.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `ref` | `string` | yes | A skill slug/stem (e.g. 'document-embeds', 'system/synap/document-embeds'), 'catalog', or a tool group ('tools:tracks'; 'tools:builder' = every tool). |
| `workspaceId` | `string` |  | Optional: also include this workspace's skills (you must be a member). Omit to use your declared workspace focus, if any; otherwise only pod-wide and your own skills are read. |

## read

Listed after `synap_load_skill('tools:read')`.

### synap_get_entities

**List entities** · Read-only

List entities for a user filtered by profileSlug. Use to browse all entities of a type (all tasks, all projects). For content/semantic recall use synap_ask. Pages: `limit` (default 50) + `offset`; a reply with `nextOffset` has more — pass it back as `offset`. Returns lean rows by default; use synap_get_entity or detail:'full' for full values.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `detail` | `enum` |  | lean (default) = null columns and systemData omitted, string values over 120 chars truncated with '…\[truncated: N chars total\]' (every property key kept). full = unprojected rows. One of: `lean`, `full`. |
| `profileSlug` | `string` |  | Profile slug filter (e.g. note, task, bookmark). Prefer this over `type`. |
| `type` | `string` |  | Deprecated alias for profileSlug. |
| `workspaceId` | `string` |  | Workspace ID (optional) |
| `projectId` | `string` |  | Optional: narrow to a project (its entity id) — only entities belonging to it. Orthogonal to workspaceId; usually pre-set by the connection URL. |
| `facetSlug` | `string` |  | Kind + Facets filter — only return entities carrying a live facet of this role-profile slug (e.g. 'client', 'investor'). Use synap_list_profiles to find role slugs (profileKind='role'). |
| `limit` | `number` |  | Page size, 1–999. |
| `offset` | `number` |  | Rows to skip — pass the previous reply's `nextOffset` to read the next page. No `nextOffset` in a reply means it was the last page. |

### synap_get_entity

**Get entity** · Read-only

Get a single entity by ID with full details: all properties and metadata. Use after synap_ask to get complete data on a result. The id comes from synap_ask results or synap_create_entity responses.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `entityId` | `string` | yes | Entity UUID |
| `workspaceId` | `string` |  | Workspace ID (optional) |
| `detail` | `enum` |  | 'lean' (default) omits `effectivePropertiesByWorkspace` — the kind's property schema re-resolved once per accessible workspace, which is 83-93% of an entity read and long enough on its own to truncate the response. You still get the entity, its profile, its own workspace's `effectiveProperties` (the schema a write to this entity is validated against), facets, externalLinks and graph, plus a `propertyOverlays` signpost naming which workspaces differ. 'full' adds the per-workspace map back — ask for it only when you need to compare lenses. One of: `lean`, `full`. |

### synap_get_document

**Get document** · Read-only

Get a document by ID: its markdown `content`, the `revision` to pass back as `baseRevision` when you edit it (synap_update_document), its top-level `sections` (id, owner ai|human, title — you may only rewrite `ai` ones), and `diagnostics` (embeds that will not render, each with a `fix`; null + diagnosticsError when they could not be checked). `format: "readable"` replaces every embed with its markdown fallback. Documents are long-form content (meeting notes, research, writeups, reports); get documentId from entity.documentId or search results.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `documentId` | `string` | yes | Document UUID |
| `format` | `enum` |  | raw (default) = the stored markdown, embeds as directives (what you edit). readable = embeds replaced by their fallback text (what you quote or summarise). One of: `raw`, `readable`. |

### synap_get_thread_context

**Get thread context** · Read-only

Get full context for a thread: all messages plus linked entities and documents. Call before posting a message to orient yourself with conversation history and in-scope data. threadId from synap_post_message or the user's personal channel.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `threadId` | `string` | yes | Channel/thread UUID |

### synap_get_relations

**Get relations** · Read-only

Get all relations for an entity — inbound and outbound. Returns typed edges with sourceEntityId, targetEntityId, and relation type. Dependencies ('blocks' / 'depends_on') are included: they are stored as one dependency link, so their row carries storedAs:'link' and a link id. Check before synap_link_entities to avoid duplicates. Use to understand an entity's connections.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `entityId` | `string` | yes | Entity UUID to get relations for |
| `workspaceId` | `string` |  | Workspace ID (optional) |

### synap_get_graph

**Get graph** · Read-only

Fetch ANY object PLUS everything it's linked to, typed. Returns \{ object, neighbors\[\], counts \}. Each neighbor is \{ kind, subtype, name, id, edgeType, direction, via \} — so you see a person linked to a deal, a skill linked to its tools, a session to its produced entities, etc. Graph by default: call this to understand an object's place in the pod before acting. Works for entity, project, view, channel, session, playbook, tool, skill, automation, document, track (a project's method: its project, sessions and playbook), proposal (what it governed and the session it was filed in) and run (an automation run: its automation and subject).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | `string` |  | The object's FULL uuid — short ids are not resolved by this tool. Provide id OR name. |
| `name` | `string` |  | Fetch by NAME instead of id (a handle). Ambiguous names return candidates to pick from. Provide id OR name. |
| `type` | `string` |  | Object kind: entity (default), project, view, channel, session, playbook, tool, skill, automation, document, track, proposal, run. |
| `subtype` | `string` |  | Narrow a name lookup (entity profileSlug, view type, tool/skill kind). Optional. |

### synap_resolve_identity

**Resolve identity** · Read-only

Read-only identity PRE-CHECK — call BEFORE creating an entity to decide create-vs-enrich-vs-attach_facet. Pass the strong signals you have (email/phone/url/twitter/github/externalId) and/or a title + kindSlug. Returns match:'strong' (a globally-unique signal already resolves to an entity → do NOT create; enrich it or synap_attach_facet a new role onto entityId instead), match:'weak' (same-name candidates — advisory, inspect `candidates` before deciding), or match:'none' (safe to create). This is the dedup door: an entity exists ONCE (a person, a company); roles are facets, never second entities. Never writes.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `kindSlug` | `string` |  | The kind (profile slug) you intend to create, e.g. person, company. A same-name match must be this kind to count as a 'weak' match; cross-kind rows still appear in `candidates`. |
| `title` | `string` |  | The name/title to check. Omit to do a strong-signal-only lookup. |
| `signals` | `object` |  | Strong identity atoms you already know. Any one match auto-resolves. |
| `properties` | `object` |  | Draft property bag — strong signals are also extracted from it (merged with `signals`). |
| `workspaceId` | `string` |  | Workspace ID (optional). |

### synap_list_profiles

**List profiles** · Read-only

List all available entity types (profiles) AND the relation types you may link them with. ALWAYS call at session start before creating entities or relations. Never assume 'deal', custom types or a relation slug exist — workspaces differ. Returns \{ profiles, relationTypes \}: a lightweight digest per profile (id, slug, displayName, profileKind, entityScope, applicableKinds on roles, description ≤120 chars, workspaceId), and relationTypes as slugs grouped by lens (\[\{workspaceId:null, slugs\}\] = valid everywhere, plus per-workspace groups listing only the slugs that workspace adds). detail:'full' returns complete profile rows and full relation-type rows (displayName, description, isDirectional, inverseLabel). A relation `type` on synap_capture / synap_link_entities must be one of those slugs; any other slug is rejected with the valid list. If the relation read failed you get `relationTypesError` instead of `relationTypes`. Without a workspaceId, profiles are merged across your workspaces; a workspace whose profiles could not be read is named in `workspacesFailed` (its kinds are missing from `profiles`), never silently dropped. No property schemas here or in synap_orient. A kind's fields, enums and required keys: call synap_get_entity on any existing entity of that kind (its `effectiveProperties`); over HTTP, GET /api/hub/discover?profileSlugs=\<slug\>. For a kind with no entities yet, a write that breaks the schema is rejected with the valid fields quoted — fix and resend once.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` |  | Workspace ID (optional — omit to list across all workspaces) |
| `detail` | `enum` |  | Pass 'full' to receive complete profile rows (including renderer and uiHints columns) and full relation-type rows (displayName, description, isDirectional, inverseLabel). Omit for the lean digest (default): profile digests plus relation-type slugs grouped by lens. One of: `full`. |

### synap_find

**Find what can do this** · Read-only

What can do this, and where does it belong? Searches the pod's runnable verbs, the abstract-intent axis, the user's playbooks and their spaces (with the kinds to write there) in ONE call, and returns each match WITH its argument schema, so you can run it without a second lookup. Pass `intent` as free text (not the closed intent vocabulary). A block that is absent was not searched; `matches: []` means it was searched and nothing fit. Matching is lexical, so a miss is never proof of absence — `coverage` says how much was searched and `noConfidentMatch` carries the escalation ladder. Run a hit with synap_run_capability (verbId) or synap_run_playbook (playbook id).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `intent` | `string` | yes | What you want to DO, in your own words (e.g. 'clean up duplicate contacts'). Keep the NOUN — it is the most discriminating word. |
| `workspaceId` | `string` |  | Optional workspace lens. Omit for pod altitude, which is what 'what can this pod do?' usually means. |
| `catalogs` | `array<enum>` |  | Which catalogs to search. Omit for all. A catalog you leave out is ABSENT from the result, not empty. |
| `limit` | `number` |  | Max matches per catalog (default 5). |

### synap_get_channel

**Get or create channel**

Get or create a channel. Use mode 'personal' to get the user's personal AI thread for a workspace. Use mode 'by-context' to get/create a thread scoped to an entity or document.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `mode` | `enum` | yes | One of: `personal`, `by-context`. |
| `workspaceId` | `string` | yes |  |
| `contextObjectType` | `enum` |  | Required for mode 'by-context' One of: `entity`, `document`. |
| `contextObjectId` | `string` |  | Required for mode 'by-context' |

### synap_diagnose

**Diagnose** · Read-only

Understand what's happening / what's wrong — the pod's health door (the third door, alongside ask + capture). The MODE is derived from what you pass (never a tool name you choose): NO ARGS → whole-pod health (stuck runs, failed flows, review backlog + age, duplicate-proposal clusters, capability posture, agents hitting the daily cap) with a plain-language summary; `type` → a class as a surface (type:"proposal" = the review queue: pending count, oldest, duplicate clusters; type:"session" = stuck sessions; type:"agent" = agent roster + quality; type:"capability" = which installed verbs are turned on and which are not (a verb that is not turned on refuses to run); type:"run" = per-flow failure counts; type:"workspace" = the workspace landscape: per-workspace entity counts + which kinds live in each, PAIRWISE profile-slug overlap between workspaces, and flags for empty / duplicate-named / no-authored-identity workspaces — the "do I have too many workspaces?" read); `id` → auto-detects what the id is (proposal / session / capability / automation-run / playbook-run / agent / entity / view / document / workspace) and explains its state + WHY; `agentId` → an agent's behavioural scorecard (approve/reject/revise rates, top rejection reasons, duplicate rate, daily-cap posture). Backward-compatible: `runId`+`flowType` → that run's activity timeline (a capture's decision + trace events, each with a machine-readable reason + fixHint); `flowType`/`flowId` → the run feed. USER-scoped automatically.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `type` | `enum` |  | Diagnose a whole CLASS as a surface (the review queue, stuck sessions, the agent roster, capability health, per-flow run failures, the workspace landscape). One of: `proposal`, `session`, `capability`, `agent`, `entity`, `run`, `workspace`. |
| `id` | `string` |  | Any object id — the door auto-detects whether it is a proposal / session / capability / run / agent / entity / view / document / workspace and explains its state + why. |
| `agentId` | `string` |  | An agent-user id → its behavioural quality scorecard (approve/reject/revise rates, rejection reasons, duplicate rate, daily-cap posture). |
| `workspaceId` | `string` |  | Narrow whole-pod health or a class surface to ONE workspace lens (default: all workspaces you can see). |
| `stuckThresholdHours` | `number` |  | Override the 'stuck run' age boundary for whole-pod health (default 24h). |
| `flowType` | `enum` |  | Back-compat run-feed grammar: restrict to one ledger. REQUIRED when runId is given (the id space differs per flow). One of: `automation`, `playbook`, `capture`, `session`, `capability`. |
| `flowId` | `string` |  | Back-compat: restrict the feed to one flow's runs (automationId / playbookId). |
| `runId` | `string` |  | Back-compat: a specific run id (or a capture's correlationId) → that run's activity timeline instead of the feed. |
| `limit` | `number` |  |  |

### synap_template_health

**Template health** · Read-only

See where each workspace stands with its template — the pod's template-update radar. Per workspace: `drifted` (true = there is a step to take: a newer version, or a last update that did not finish); `state` (the last update: `active` done · `building` in progress · `awaiting-approval` its parts wait on the proposals in `awaitingApproval` · `partial`, `layer-2-not-run` or `failed` not finished); `applyWarning` (the pod's one sentence for what is left to do — relay it as is); `pendingDataChanges` (`{count, sentences, needsPodAdmin}` or null: the update changes existing data and waits for a PERSON to agree — never you; tell them what changes, in its sentences, and that they run `synap templates update <slug>` themselves, a pod owner or admin when `needsPodAdmin`); `updateProposal` (`{id}` or null: an update of this space is already proposed — point the person to that proposal, never file another); plus `installedVersion`, `latestVersion`, `warnings` and `lifecycle`: `current`, `retired` (`replacedBy` names the replacement, `retiredReason` says why; move it with `synap_retire_workspace`), `unlisted` (no longer in the catalog) or `unknown` (the catalog could not be fully read — never treat it as unlisted). The update path is `synap templates update <slug>`: run with an agent key it becomes a proposal the person approves, and data changes always need the person. Never re-apply a space that is `awaiting-approval` or has an `updateProposal`: the person's approval comes first. Read-only; scoped to the caller's own workspaces.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `driftedOnly` | `boolean` |  | When true, return only workspaces with an available update (drifted). Default false = every workspace with its health. |

## data

Listed after `synap_load_skill('tools:data')`.

### synap_create_entity

**Create entity**

Create a SINGLE typed entity — no relations parameter exists here. Use when you already know the exact profileSlug + fields: the structured, deterministic sibling of synap_capture (which parses free text). Discover slugs with synap_list_profiles; synap_ask first to avoid duplicates. Same-profile same-name creates are REJECTED with candidates (reuse the existing id, enrich, or attach a facet) unless forceCreate=true. Placeholder person/company titles (e.g. 'Not publicly disclosed', 'unknown') are rejected. To create an entity TOGETHER WITH its edge to another entity in one reviewable unit, use synap_capture instead, passing both `entities` and `relations` — synap_link_entities cannot target an entity this call just proposed (its id isn't live until approved). A `decision` with `decisionStatus:'proposed'` ASKS the person automatically: pass `decisionOptions` (≤8 of \{label, value?, description?\}) + `recommendedOption`, and `sourceSessionId` to keep the question in your session; their answer updates it. Never create a decision for an answered confirm/choose — the pod already filed it (`slot.decisionId`).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `profileSlug` | `string` | yes | Entity profile slug (e.g., note, task, event, person, contact, company, deal, bookmark, article). Use synap_list_profiles to discover available types. The cross-cutting container that spans workspaces is NOT an entity kind — it has its own door, synap_create_project. |
| `title` | `string` | yes |  |
| `description` | `string` |  |  |
| `content` | `string` |  | LONG-FORM markdown body (the write-up itself, not a summary). Materialized into a versioned document linked to the entity — so a note/article/research entity and its body land in ONE call. Use `description` for a short preview and `content` for anything multi-paragraph; do NOT stuff long text into `properties`. |
| `properties` | `object` |  | Typed entity properties (profileSlug-specific fields) |
| `projectId` | `string` |  | Optional project id to file the created entity into — stamps belongs_to_project membership. |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess, and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |
| `forceCreate` | `boolean` |  | Bypass the weak same-name gate when a same-profile entity with this title already exists. Prefer reusing the existing id. Does NOT bypass strong-signal auto-merge (email/phone/url). Default false. |
| `facets` | `array<object>` |  | Kind + Facets: attach one or more role-profiles to the new entity in the SAME call (e.g. a person who is a client + investor). A role is a facet, NOT a separate entity — resolve identity first, then attach roles here. Each item: \{ slug, properties? \}. Only applied when the entity is created (not when the create is proposal-gated). |
| `expectedLabel` | `string` |  | The declared output slot this fulfils, exactly as declared on the session. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_update_entity

**Update entity** · Destructive

Update an entity's title, description, properties (JSONB), or long-form BODY (`content`). Requires entityId from search or synap_get_entity. May return 'proposed' if the write requires review. Use for property updates and status changes. To move a status (a kind's declared lifecycle), set the property to the new option AND pass `expectedStatus` = the status you read: `conflict` means someone moved it (re-read), a move into a done/lost option proposes, and a human-only option is refused. `content` REPLACES the whole body of the entity's linked document (an alias of synap_update_document with one replace_all op): it always goes to review, and it is refused if it changes a person's section or drops an embed. To change one section or one passage, use synap_update_document instead. If the entity has no body document yet the call is refused — create and attach one with synap_create_document(\{ entityId, title, content \}). `profileSlug` converts it to another kind, keeping its id and links (always reviewed).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `entityId` | `string` | yes |  |
| `profileSlug` | `string` |  | CONVERT the entity to another kind (e.g. a misfiled knowledge entry → decision, issue or note). It keeps its id, links, document and history; always a proposal. Send the new kind's required properties in `properties` in the same call. An unknown kind is refused — list kinds with synap_list_profiles. |
| `title` | `string` |  |  |
| `description` | `string` |  |  |
| `properties` | `object` |  | Partial properties to merge into entity.properties JSONB |
| `metadata` | `object` |  |  |
| `content` | `string` |  | Full REPLACEMENT body (markdown) for the entity's linked document — not a patch (for a patch: synap_update_document). Read the current body with synap_get_document first. Its outcome comes back under `body` and is always 'proposed'. |
| `expectedStatus` | `string \| null` |  | When you change the status: the status you SAW (compare-and-set). If it moved meanwhile you get \{ status: 'conflict', conflict: \{ currentStatus \} \} and nothing is written — re-read and decide again; never retry blindly. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess, and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |

### synap_update_entities

**Update many entities** · Destructive

Change up to 500 existing entities in ONE call — retitles, property fixes, status moves. Use it for any change set; never script many single updates or Hub REST calls. Each item is \{id, patch, expected?\}; patch fields: title, description, properties, deleteProperties, expectedStatus (no kind change, no body — use synap_update_entity / synap_update_document for those). Governance is per item: each is written now (when it would auto-approve), or collected into ONE review for the person, or denied, or skipped (not found, changed since you read it, nothing to change). The answer lists applied, proposed \{proposalId, count, reviewUrl\}, denied, skipped, plus items\[\] with each verdict — a partial result is normal. 'proposed' is success: do not resend those items; a skipped item with `deferred: true` was kept on a space-join request and is replayed for you. Pass validate:true first to see the verdicts without writing. Pass an idempotencyKey so a retried call returns the first answer instead of acting twice.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `items` | `array<object>` | yes |  |
| `validate` | `boolean` |  | Dry run: return each item's verdict, write nothing. |
| `idempotencyKey` | `string` |  | 8–256 chars. The same key with the same items returns the first call's answer. |
| `reasoning` | `string` |  | WHY these changes — shown to the person who reviews them. One line. |

### synap_create_document

**Create document**

Create a standalone document. TWO modes: (1) authored text — pass `content` (markdown) for meeting notes, research, plans that don't fit entity properties. (2) external reference — pass `url` (no `content`) to reference a file/page you have a LINK to but no bytes to upload (e.g. a Google Doc, a PDF URL); this is the agent-appropriate way to add a 'file' since an agent has no filesystem. Pass `entityId` to ATTACH the document to an existing entity. If you're also creating the entity, prefer synap_create_entity with `content` (entity + body in ONE call). To store real file bytes, use `synap_store_file` (≤10MB inline; bigger: `synap_request_upload`, then `synap_store_file({uploadToken})`). Attachment only happens when the document auto-approved; a proposal-gated one has no row to link yet, and the response says so.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | `string` |  | Optional: a stable key so a retry returns the PRIOR write, not a duplicate. Omit and identical content is deduped automatically; pass one to make a retry idempotent even if its content changed trivially. |
| `title` | `string` | yes |  |
| `content` | `string` |  | Authored document body (markdown or plain text). Mutually exclusive with `url`. |
| `url` | `string` |  | External reference: an https URL to a file/page you have a link to (no bytes stored). Use INSTEAD of `content` when referencing an existing file/link rather than authoring a body. |
| `entityId` | `string` |  | Optional UUID of an EXISTING entity to attach this document to. The attach is a separate governed entity update — its own outcome comes back under `attached`. |
| `workspaceId` | `string` |  |  |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess, and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |
| `expectedLabel` | `string` |  | The declared output slot this fulfils, exactly as declared on the session. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_update_document

**Update document** · Destructive

Edit a document with ops — a section, a passage, an addition — instead of rewriting it. Read it first with synap_get_document and pass its `revision` as `baseRevision`. Ops, applied in order: `upsert_section {id, title, body}` writes ONE `::::synap-section` block by id (replaces your own `ai` section, or appends a new one); `replace_text {old, new}` — `old` must match EXACTLY once, copy it from the document and include enough context (0 or 2+ matches is refused with the count); `append {body}`; `replace_all {content}` (whole body — needs baseRevision, always goes to review). Refused: changing a section whose owner is `human` (by any op), and removing an embed unless `allow_removing_embeds: true`. Usually returns 'proposed' (the person reviews a per-section before/after); a governance rule may apply it directly. The response carries `preview` and `diagnostics` (embeds that will not render, each with a `fix` — advisory, never a refusal). A 'changed after you read it' error means re-read and redraft.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `documentId` | `string` | yes | Document UUID |
| `baseRevision` | `number` |  | The `revision` synap_get_document returned. The edit is refused if the document changed since. |
| `ops` | `array<object>` | yes | The edits, applied in order. |
| `allow_removing_embeds` | `boolean` |  | Set true ONLY when removing an embed (synap-entity/view/cell) is the point of the edit; otherwise such an edit is refused. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_request_upload

**Request upload**

Get a URL to upload a file too large for `synap_store_file` inline (over 10MB, up to 500MB for video). Returns `{uploadUrl, headers, uploadToken, expiresAt, maxBytes}`: PUT the exact bytes to `uploadUrl` with exactly `headers` before `expiresAt`, then call `synap_store_file({uploadToken})` within the hour to save it. `size` must be the exact byte count.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `filename` | `string` | yes | The file name (e.g. 'launch.mp4', 'deck.html'). |
| `mimeType` | `string` | yes | The content type (e.g. 'video/mp4', 'application/pdf'). Must be an allowed type. |
| `size` | `integer` | yes | Exact size of the file in bytes. |
| `workspaceId` | `string` |  |  |
| `sessionId` | `string` |  | OPTIONAL focus-session override, same rules as on `synap_store_file`. The upload's `synap_store_file` call defaults to this session. |

### synap_store_file

**Store file**

Store file CONTENT you already have (text via `content`, or binary via `contentBase64`) as a `file` — ANY type, stored \*\*as-is and NEVER read/analyzed\*\*. Use for a report/CSV/image/PDF/etc. you generated or hold. Pass `attachToEntityId` to attach it to an existing entity instead of creating a new file. For a link you have (no bytes), use `synap_create_document` with `url`. Max 10MB inline; for a bigger file call `synap_request_upload`, PUT the bytes, then call this with `uploadToken`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `filename` | `string` |  | The file name (e.g. 'report.csv', 'diagram.png'). |
| `mimeType` | `string` |  | The content type (e.g. 'text/csv', 'image/png', 'application/pdf'). Must be an allowed type. |
| `content` | `string` |  | UTF-8 TEXT content to store as-is. Mutually exclusive with `contentBase64`. |
| `contentBase64` | `string` |  | Base64-encoded BINARY content. Mutually exclusive with `content`. |
| `uploadToken` | `string` |  | The `uploadToken` from `synap_request_upload`, after you PUT the bytes. Replaces `content`/`contentBase64`/`filename`/`mimeType`. |
| `title` | `string` |  | Optional human-facing title (defaults to filename). |
| `workspaceId` | `string` |  |  |
| `attachToEntityId` | `string` |  | Optional UUID of an EXISTING entity — attach the stored blob to it as provenance instead of creating a new `file` entity. |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess, and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |
| `expectedLabel` | `string` |  | The declared output slot this fulfils, exactly as declared on the session. |

### synap_remember_fact

**Remember fact**

Store a durable fact about the user (preference, habit, working style, technical context) as a governed `user_observation`. Returns status:"proposed" with a `reviewUrl` when the fact is your own inference — that is normal, not an error — or status:"created" when you pass userStated:true because the user told you directly. Returns the record's id so you can link or revert it.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `idempotencyKey` | `string` |  | Optional: a stable key so a retry returns the PRIOR write, not a duplicate. Omit and identical content is deduped automatically; pass one to make a retry idempotent even if its content changed trivially. |
| `userId` | `string` |  | User ID (auto-injected from API key if not provided) |
| `fact` | `string` | yes | The fact to remember |
| `confidence` | `number` |  | How sure you are, 0–1 (default 0.8). Stored on the observation; it does NOT change the governance outcome. |
| `category` | `enum` |  | Which bucket the observation belongs to (default 'preferences'). One of: `working_style`, `communication`, `focus`, `preferences`, `habits`, `technical`. |
| `userStated` | `boolean` |  | true ONLY if the user directly stated this; do not infer. This is the single signal that makes the write auto-approve instead of proposing. |

### synap_link_entities

**Link entities**

Create a typed relation between two entities. `type` is NOT a free string: it must name an existing relation-def slug — real ones include 'relates_to', 'references', 'mentions', 'parent_of', 'depends_on', 'blocks', 'created_by', 'works_at'. To file an entity into a project use synap_file_into_project — `belongs_to_project` is refused here. The default slugs resolve pod-wide, so no `workspaceId` is needed for them; a workspace may add or override slugs. A slug that names no relation def is rejected. If available to you, check this entity's existing relations first to avoid duplicates. Both endpoints must already be LIVE entities — an id from a still-pending synap_create_entity proposal will fail; to create an entity and its edge together, use synap_capture with `entities` + `relations` instead. May return 'proposed'. Builds the knowledge graph.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sourceEntityId` | `string` | yes | Source entity UUID |
| `targetEntityId` | `string` | yes | Target entity UUID |
| `type` | `string` |  | Relation type — an existing relation-def slug (e.g. 'relates_to', 'references', 'mentions', 'parent_of', 'works_at') |
| `workspaceId` | `string` |  | Workspace ID (optional) |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_attach_facet

**Attach facet**

Attach a ROLE to an existing entity (Kind + Facets). A role — client, partner, prospect, investor, sponsor — is a FACET, never its own entity: an entity IS one kind (person, company) and HAS many roles. Resolve identity FIRST (synap_ask / synap_get_entities on strong signals like email / phone / website) so you attach the role to the REAL entity instead of creating a duplicate. Governed like any write: may return 'proposed' (a proposalId to review) — NEVER treat that as an error. Use synap_list_profiles to find role slugs (profileKind='role') and which kinds they apply to (applicableKinds).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `entityId` | `string` | yes | UUID of the entity to attach the role to. |
| `facetSlug` | `string` | yes | Role-profile slug to attach (profileKind='role', e.g. 'client'). |
| `properties` | `object` |  | Optional facet-specific properties. |
| `workspaceId` | `string` |  | Optional facet visibility lens. Omit to inherit the parent entity's workspace. |
| `contextEntityId` | `string` |  | Optional disambiguator when the same role attaches in multiple contexts (e.g. a client OF a specific company). |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_detach_facet

**Detach facet** · Destructive

Detach (soft-delete) a role from an entity (Kind + Facets). Provide the entityId + facetSlug of the role to remove (or a facetId directly). Governed like any write: may return 'proposed' — NEVER treat that as an error. Removing a role never deletes the entity; only the role-facet is retired.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `entityId` | `string` |  | UUID of the entity carrying the role (with facetSlug). |
| `facetSlug` | `string` |  | Role-profile slug to detach from the entity (paired with entityId). |
| `facetId` | `string` |  | Alternative to entityId+facetSlug: the facet's own UUID (the handle synap_attach_facet returns). |
| `workspaceId` | `string` |  | Optional workspace lens used to resolve the facet when detaching by entityId + facetSlug. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_move_entities

**Move entities to space**

Move entities into another Space (workspace; e.g. a mis-routed lead). You must be able to write the destination. Per entity: returns \{ moved, proposed, errors \} — a proposed move is success.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `entityIds` | `array<string>` | yes | Entity UUIDs to move (1-500). |
| `workspaceId` | `string` | yes | Destination workspace UUID. |
| `reasoning` | `string` |  | Why they belong there — shown to the reviewer. |

### synap_organize_library

**Organize Library**

Propose how to ORGANISE the person's Library (the links, files and notes they kept) — never add to it: your own words, links and files go to synap_capture. Without `ops`: returns the Library's groups (id, name, note, item counts) so you can name one. With `ops`: files ONE proposal for the person's review — always a proposal for an agent, `proposed` is success (surface reviewUrl); nothing changes until they approve, then it applies exactly as if they had done it. Ops: group.create \{name, note?, itemIds?\} · group.items \{groupId, add?, remove?\} (existing items) · group.update \{groupId, name?, note?, workspaceId?: the space Build files into, null clears\} · group.build \{groupId\} (approval runs Build, whose result is reviewed again). Item ids come from synap_ask (its Library hits carry their id). An id that is not in this Library is refused.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `ops` | `array<object>` |  | 1-50 organisation ops, applied in order on approval. Omit to list the groups. |
| `reasoning` | `string` |  | Why this organisation helps, in the person's words — shown to the reviewer. One line. |

## sessions

Listed after `synap_load_skill('tools:sessions')`.

### synap_get_session

**Get focus session** · Read-only

Re-find a focus session — read-only. `outcomes` is what it must yield and needs: `outcomes.outcomes[]` (key, label, verify, met + metBy, state, evidence) and `outcomes.inputs[]` (what the person must give, each pointing at the outcome it blocks); `status:'unavailable'` means the read failed, not empty. To CONTINUE a session, read `continuation` first: it is the continuation packet (userMustDecide = owed slots + pending proposals, aiCanDo = open agent deliverables, blockers, outputs, run manifest, rerun, lastCompletion, nextMove). A section with status 'unavailable' failed to load — it is NOT empty. Pass sessionId for a specific session. Omit sessionId only when you have exactly one open session (ambient). If multiple sessions are open, returns multiSession:true + openSessions\[\] — pass sessionId explicitly (ambient attach is disabled to prevent mis-attribution). Always yours: sessions are scoped to the calling user.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` |  | Optional focus session UUID. Omit only when exactly one session is open; if multiple are open you must pass sessionId. |

### synap_list_sessions

**List focus sessions** · Read-only

List YOUR focus sessions, newest first — read-only. Use it to see what work is open before starting something new (don't start a second session for work that already has one), or to find a session you closed earlier. Filter by status and/or narrow to a workspace or project. Pages of up to 50: a reply with `nextOffset` has more — pass it back as `offset`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `status` | `enum` |  | 'open' (default) = every non-terminal session (active/paused/forming/scheduled). 'all' = no status filter. Or name one exact status — incl. 'stale' (reaper-marked, reopenable). One of: `active`, `paused`, `closed`, `forming`, `scheduled`, `failed`, `cancelled`, `stale`, `open`, `all`. |
| `workspaceId` | `string` |  | Optional: only sessions in this workspace. |
| `projectId` | `string` |  | Optional: only sessions scoped to this project. |
| `subjectEntityId` | `string` |  | Optional: only sessions ABOUT this entity (the subject-spine anchor). |
| `playbookId` | `string` |  | Optional: only runs of this playbook DEFINITION. Pair with kind 'run' or 'all' — flow-linked rows are runs, so under the default nothing would match. |
| `automationId` | `string` |  | Optional: only runs of this automation DEFINITION (not one automationRunId). Same pairing as playbookId. |
| `kind` | `enum` |  | Which population. 'work' = units of work a person owns. 'run' = a playbook or automation execution. 'receipt' = the container an agent's proposals were filed under. 'all' (default) = no filter. Every row carries its own `kind` either way. One of: `work`, `run`, `receipt`, `all`. |
| `limit` | `number` |  | Page size, 1–50. |
| `offset` | `number` |  | Rows to skip — pass the previous reply's `nextOffset` to read the next page. No `nextOffset` in a reply means it was the last page. |

### synap_list_agents

**List agents and computers** · Read-only

Who can take the person's work, and on which COMPUTER — read-only. `computers[]` are theirs (match their words, e.g. 'my devbox', against these names first, then agent names/types; 2+ fit → ask). Each agent: `computer`, `canTakeYourWork` (else `whyNot`), `live`, `paused`, `lastSeenAt`, and `handOff` — what YOUR hand-off would do now: `direct`, `asks_you` (the person approves it first: give them the reply's review link), `blocked` (`handOffReason`); ready ones come first. Hand NEW work: `synap_start_session` with `agentIds:[agentUserId]`, a goal and 2-5 criteria; work in flight ('this session'): `synap_update_session` `addAgentId`. Then follow its room (`synap_get_thread_context` on the session's `channelId`; outcomes via `synap_get_session`) and report back. Paused or not live: say so, never hand off silently. How: `load_skill` `system/synap/hand-off`.

No parameters.

### synap_evaluate_session

**Evaluate focus session**

Grade a focus session against its acceptance criteria. Post deterministic evidence for evidence-checked criteria as `evidence: { <evidenceKey>: { passed, detail? } }`; capability and judge checks run automatically (the judge is never the model that did the work). Returns each criterion's result, the current evaluations and the session verdict. At most 2 automatic attempts per criterion — after that a failing required criterion is handed to the human. Never blocks closing the session.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The focus session UUID. |
| `evidence` | `object` |  | Evidence keyed by each criterion's evidenceKey (e.g. \{ typecheck: \{ passed: true, detail: "0 errors" \} \}). |

### synap_revert_session

**Ask the user to revert a session** · Read-only

Reverting takes back work that was already approved, so it is a HUMAN decision: this tool NEVER reverts anything. Call it when a session (a run), some of its proposals, or one item of a proposal should be undone. It answers status 'refused' — that is the contract, NOT an error — with the session's revertable proposals and a link to each: hand those to the user. The user reverts from the session room or the proposal — everything, only some proposals, or one item; items edited since are skipped with the reason.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The focus session (run) UUID. |
| `proposalIds` | `array<string>` |  | Optional: only these proposals of the session should be undone. |
| `proposalId` | `string` |  | Optional: one proposal of the session — with opKey, one item of it. |
| `opKey` | `string` |  | Optional: one item of proposalId — its key in the proposal's materialized record (an op ref, or `<sourceRef>-><targetRef>:<type>` for a link). |

### synap_rerun_session

**Rerun a session** · Destructive

Re-analyse a finished session (a run) from its STORED sources with the CURRENT guidelines, as a NEW session spawned from it. ALWAYS call with dryRun:true first: it returns the counts (sources, what would be reverted, pending proposals left alone, estimated structure calls) and whether it is within the cap — nothing is written. mode 'add' re-analyses on top of the previous run; mode 'replace' reverts the previous run first and is a HUMAN decision (an agent gets `ok:false, reason:'replace_is_a_human_decision'` — hand the user the session room). Each re-analysed source files its writes through the governed capture/import doors, so an item outcome 'proposed' is normal, NOT an error. A still-open session is refused (cancel it first); refusals come back as \{ ok:false, reason, message \}.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The finished focus session (run) UUID to rerun. |
| `mode` | `enum` | yes | 'add' = re-analyse on top of the previous run. 'replace' = revert the previous run first (human only). One of: `add`, `replace`. |
| `dryRun` | `boolean` |  | true = counts + cap verdict only, nothing written. Do this first. |
| `sourceDocumentIds` | `array<string>` |  | Optional: rerun only these stored sources of the session (the ids in its run manifest). |
| `reasoning` | `string` |  | Why you are rerunning (e.g. which guideline changed) — recorded on the new session's run manifest for the reviewer. |

### synap_promote_session_to_playbook

**Save session as template**

Escalation L4 — crystallize AFTER a session succeeded and the process is clearly repeatable (not a one-off). Promotes a validated focus session into a reusable Playbook (runtime → config): re-grants capabilities used and records lineage. May propose; never promote failed or speculative sessions.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `sessionId` | `string` | yes | The focus session ID to promote into a playbook. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

## governance

Listed after `synap_load_skill('tools:governance')`.

### synap_list_proposals

**List proposals** · Read-only

List proposals — the audit trail of AI writes. AI writes return status 'proposed' when they require human approval — this is NOT an error. Filter by status: 'pending' (needs review), 'approved', 'rejected', 'auto_approved' (the write EXECUTED immediately under governance and filed this row as its receipt — use this to show the user what you did without asking), 'reverted', 'approval_failed', 'withdrawn', 'expired' (never decided — its session closed or its window lapsed). Pass sessionId to load the \*\*session review pack\*\* (proposals for one focus session). userId is auto-injected from the API key if not provided.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `userId` | `string` |  | User ID (optional, auto-injected from API key if not provided) |
| `workspaceId` | `string` |  |  |
| `sessionId` | `string` |  | Focus session UUID — list only proposals attributed to this session (the review pack). |
| `status` | `enum` |  | One of: `pending`, `approved`, `rejected`, `auto_approved`, `reverted`, `approval_failed`, `withdrawn`, `expired`, `all`. |
| `limit` | `number` |  |  |
| `detail` | `enum` |  | 'summary' (default) returns one compact row per proposal — id, type, target, provenance, and a one-line summary — which is what a LIST is for. 'full' includes the entire `data` payload of every row: only ask for it when you need to inspect a specific proposal's contents, and pair it with a small `limit`. One of: `summary`, `full`. |

### synap_get_proposal

**Get proposal** · Read-only

Read ONE proposal in full — the door for "why did my write not land?". Returns the row a reviewer sees: `status`, the whole `data` payload (the operations that would apply), `summary`, provenance, and — when an approve was attempted and FAILED (`status: 'approval_failed'`) — `rejectionReason` (the classified sentence) plus `data.failure` \{ errorClass, providerRef, missingFields \}: the fields naming what to fix. Use it after a write comes back `proposed` and the user reports a problem approving it, then REPAIR the same proposal with synap_revise_proposal (never file a second one pointing at the first), or withdraw it with synap_reject_proposal. A proposal still `pending` after a failed approve attempt recorded nothing — read `data` and check the ops yourself. The raw upstream error text is deliberately NOT returned.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `proposalId` | `string` | yes | Proposal UUID, as returned verbatim by synap_list_proposals or by the `proposalId` of a `proposed` write. A leading fragment of one also resolves, but pass the full id you were given. |

### synap_governance

**Get governance policy** · Read-only

Read workspace governance policy and count of pending proposals. Use before writes to understand auto-approve rules and whether proposals will be created. When called with an agent key it also returns `agentBudget`: YOUR pending-proposal cap, how many of it you are using, whether you are BLOCKED (at the cap your writes are refused — nothing is written), and the remedy, including the link to a cap-raise request already waiting for your owner's review. Read-only: it never files anything.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` | yes | Workspace UUID |

### synap_revise_proposal

**Revise proposal** · Destructive

Amend a pending proposal you authored (e.g. after user feedback): its summary, its reasoning, and/or the payload it will actually apply. Does not re-run the event pipeline, and does NOT approve it — a human still decides.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `proposalId` | `string` | yes | Proposal UUID, as returned verbatim by synap_list_proposals. A leading fragment of one also resolves, but pass the full id you were given — do not shorten it yourself. |
| `summary` | `string` |  | The human-readable summary a reviewer reads when deciding. |
| `reasoning` | `string` |  | Why this change is being requested. |
| `patch` | `object` |  | Amend WHAT WILL BE APPLIED, not just the narrative. Use this whenever you change the summary to describe different content — a summary that no longer matches the payload is worse than no revision, because the human reads the summary. |

### synap_reject_proposal

**Reject proposal** · Destructive

Reject a pending proposal so the queued change never lands — withdraw a write you now know is wrong (a duplicate, a superseded plan, a mistaken read) instead of leaving it in the user's review queue. Give a `reason`: it is recorded on the proposal and feeds your agent scorecard. There is deliberately NO approve tool: approval is the HUMAN step (an agent key that approved its own write would be self-approving) — point the user at the proposal's review link instead. Rejecting is safe by the same logic in reverse: it only PREVENTS a pending change from landing, so it carries no self-approval or undo risk. Only a still-pending proposal can be rejected.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `proposalId` | `string` | yes | Proposal UUID, as returned verbatim by synap_list_proposals. A leading fragment of one also resolves, but pass the full id you were given — do not shorten it yourself. |
| `reason` | `string` |  | Why you are rejecting it (recorded on the proposal). Always give one. |
| `reasonCode` | `enum` |  | Optional structured cause, one of the fixed set — recorded alongside `reason` and fed to the calibration/scorecard loop. Use `other` (or omit) when none fits and rely on the free-text `reason`. One of: `wrong_entity`, `duplicate`, `wrong_kind_or_facet`, `wrong_link_type`, `wrong_workspace`, `bad_data`, `not_relevant`, `other`. |

### synap_create_rule

**Create rule**

Turn a stated standing intent into a RULE — the COMPILED door, and the one to reach for whenever the user says 'always…', 'from now on…', 'never…', 'every Monday…', or states a preference that should outlive this conversation. Send their own words as `intent`.

• `intent` alone → a FACT rule: durable prose ('Acme prefers async') that any agent reads later while reasoning. Legitimate and complete.

• `intent` + `sentence` → BEHAVIOUR: the door compiles your WHEN/WHERE/THEN into a real automation, or refuses. A rule that describes something running is never stored as prose that cannot run.

USE THIS INSTEAD OF synap_create_automation / synap_create_skill for a standing intent. Those are raw primitives — they persist whatever you hand them, so a bad trigger installs 'active' and silently never fires, and an instruction skill only ever sits there as text. This door verifies the compiled artifact against the runtime BEFORE anything is saved. (Reach for synap_create_automation directly only when you are authoring a flow that is not a user-stated rule — a multi-step pipeline with its own data contract.)

WHAT IT REJECTS, and why each rejection is real: a WHEN naming an event no emitter produces (the automation could never match); a WHERE row with a field but no value (dropping it would widen the rule beyond what was written); a THEN with no configured action (a trigger wired to nothing); `run_command` (the command step has no receiver and throws every run — use an AI step). A refusal comes back as status='denied' with `failure.clause` (WHEN | WHERE | THEN) and a reason written for a human. FIX THAT CLAUSE AND RESEND — it is a verdict, not a transient error, and re-sending it unchanged will fail identically.

If the intent describes something that should run and you send no `sentence`, the receipt carries `needsBehaviour` — the rule was saved as prose and will not execute; say so rather than reporting it as in effect.

Scope: pod-wide by default; pass `workspaceId` for a domain rule, `projectId` for the cross-cutting lens. A `projectId` limit is only enforceable on WHEN events that carry a project — entity.\*, entity_facet.\*, external_message.\*, channel_message.\*, focus_session.\*, proposal.\* — and is REFUSED (status='denied', clause WHEN) on any other trigger (e.g. a schedule); drop the project or pick one of those events. `expiresAt` makes the rule stop applying — pass one whenever the intent is situational ('while we're in the launch push'), because a standing rule with no expiry is one the user must remember to revoke.

Governed like every write: status='proposed' is SUCCESS, not an error — surface the returned link as a markdown link and never report a proposed rule as already in effect.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `intent` | `string` | yes | The rule in the user's own words — this IS the rule an agent reads later, so keep their phrasing rather than paraphrasing it into a spec. |
| `sentence` | `object` |  | The rule's structured WHEN / WHERE / THEN. Send it when the rule should DO something — the door compiles it into a live automation or REFUSES naming the clause that failed. Omit it for a prose-only FACT rule. All three keys are required when you send it (`conditions` and `actions` may be empty arrays, but an empty `actions` is refused as 'no THEN'). |
| `scope` | `enum` |  | Where the rule applies. Defaults to 'workspace' when a workspaceId resolves, otherwise 'pod' (everywhere). One of: `pod`, `workspace`, `user`. |
| `workspaceId` | `string` |  | Optional workspace lens. Omit for a pod-wide rule; the ambient workspace focus is used when set. |
| `projectId` | `string` |  | Optional cross-cutting project lens — composes with the workspace lens. |
| `expiresAt` | `string` |  | ISO-8601 instant with offset (e.g. '2026-12-31T00:00:00Z') after which the rule stops applying. A non-instant is refused rather than stored. |

## schema

Listed after `synap_load_skill('tools:schema')`.

### synap_define_kind

**Define entity kind**

Define a new entity KIND — AND the door for ADDING A FIELD to a kind that already exists. It is SLUG-IDEMPOTENT: re-call it with an EXISTING slug and it returns that profile and adds only the `properties[]` that are not there yet (an existing field reports status 'unchanged' and is never rewritten). So to add `wedge_stage` to `question`, call this with slug 'question' and just that one field — there is NO separate add-property tool, and schema editing is NOT browser-only. A field takes `valueType`, `constraints` (an enum of allowed values lives here), `defaultValue`, `required`, and `overlay: true` to add it as a WORKSPACE OVERLAY instead of a base field on the kind — which is what you want for a field that only one domain should see, on a pod-wide kind like note/task/question. Call list_profiles + extend-first (load_skill system/synap-schema/extend-first) BEFORE minting a new slug. A hat on an existing kind — including item, task, deal — is a ROLE (synap_define_role / attach_facet), never a sibling kind. Prefer parentProfileSlug of the closest parent over a disconnected slug. A relationship-with-its-own-life (buyer×seller×price×stage) is its own kind (deal \*precedent\*), not a twin of CRM deal. POD-WIDE default. Governed: 'proposed' is success. Creates it on this pod only; an official template is written in the catalog and published with `synap catalog publish --official`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `slug` | `string` | yes | URL-safe kind slug (lowercase, digits, hyphens), e.g. 'podcast-episode'. |
| `displayName` | `string` | yes | Human-readable type name, e.g. 'Podcast Episode'. |
| `description` | `string` |  | Optional description of what this kind represents. |
| `icon` | `string` |  | Optional icon hint for the kind. |
| `entityScope` | `enum` |  | Where entities of this kind live. OMIT for the doctrine default 'pod' (kinds are pod-wide). Pass 'workspace' only for a kind that belongs to one app and should not appear pod-wide. One of: `pod`, `workspace`. |
| `properties` | `array<object>` |  | Optional FIELDS of this kind, created and linked after the profile exists. Each entry defines one property def. |
| `defaultValues` | `object` |  | Optional default property VALUES applied to new entities of this kind (distinct from `properties`, which defines the fields themselves). |
| `parentProfileSlug` | `string` |  | Optional slug of a parent kind to extend (e.g. 'note', 'person') — NOT a UUID. Resolved to the parent profile id server-side; use synap_list_profiles to discover slugs. |
| `workspaceId` | `string` | yes | Workspace to define the kind in. |
| `forceCreate` | `boolean` |  | Only after a CONFLICT naming an existing twin. The pod refuses an AI-initiated slug that already exists anywhere, because a twin silently splits a kind's properties in two. Read the candidates it named; set this ONLY if a separate one is genuinely right. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_define_role

**Define role type**

Define or WIDEN a role (facet): a hat on ANY entity kind, not only person/company. Example: applicableKinds \['item'\] means 'this item is an X'. Call ONLY after synap_list_profiles. If the slug already exists, extra applicableKinds are MERGED (widen only — never a second slug for the same hat). Prefer attach_facet; widen when kind_mismatch. NULL stored allowlist = any kind. Governed: may return 'proposed' — NEVER treat that as an error.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `slug` | `string` | yes | URL-safe role slug (lowercase, digits, hyphens), e.g. 'market-maker'. |
| `displayName` | `string` | yes | Human-readable role name, e.g. 'Market Maker'. |
| `applicableKinds` | `array<string>` |  | Kind slugs this hat can attach to (item, person, company, task, …). Merged on an existing role. Defaults to \['company','person'\] only when omitted on CREATE — that default is a convenience, not a model limit. |
| `description` | `string` |  | Optional description of what the role represents. |
| `icon` | `string` |  | Optional icon hint for the role. |
| `roleCategory` | `string` |  | Optional grouping key so an automation can select entities wearing ANY role in this category via entity.query \{ roleCategory \} — e.g. tag every supply role 'provider'. Future roles tagged the same category qualify with no query change. |
| `properties` | `object` |  | Optional default property values for facets of this role. |
| `workspaceId` | `string` | yes | Workspace to define the role in. |
| `forceCreate` | `boolean` |  | Only after a CONFLICT naming an existing twin. The pod refuses an AI-initiated slug that already exists anywhere, because a twin silently splits a kind's properties in two. Read the candidates it named; set this ONLY if a separate one is genuinely right. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_grant_profile_access

**Share kind with space**

Let another Space (workspace) see and use a SHARED kind (profile). Widens visibility, so it is always a proposal for an agent. Find ids with synap_list_profiles / synap_list_workspaces.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `profileId` | `string` | yes | Profile UUID. |
| `targetWorkspaceId` | `string` | yes | Workspace UUID that gains access. |
| `workspaceId` | `string` |  | Acting workspace UUID; omit to use the kind's home workspace. |
| `reasoning` | `string` |  | Why — shown to the reviewer. One line. |

### synap_change_kind_scope

**Change kind scope**

Change which Spaces see a kind (profile) and where its NEW records live. scope "shared" (+ allowedWorkspaceIds) makes the kind resolve in those spaces; "workspace" pins it to its home and revokes grants. entityScope "pod" makes NEW records pod-wide (existing records keep their space). Use it when another space needs a kind — never define a twin kind. Always a proposal for an agent; the kind's owner (or a pod admin for a system kind) approves. To add one space to an already-shared kind, use synap_grant_profile_access.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `profileId` | `string` | yes | Profile UUID. |
| `scope` | `enum` |  | New kind scope. One of: `shared`, `workspace`. |
| `allowedWorkspaceIds` | `array<string>` |  | Space UUIDs granted access. Only with scope "shared". |
| `entityScope` | `enum` |  | Whether NEW records of this kind are pod-wide or stay in one space. One of: `pod`, `workspace`. |
| `workspaceId` | `string` |  | Acting workspace UUID; omit to use the kind's home workspace. |
| `reasoning` | `string` |  | Why — shown to the reviewer. One line. |

### synap_retire_field

**Retire field** · Destructive

Propose RETIRING a field (a property definition) — including a kind's base field or a global field, which nothing else can remove. Always a proposal, for anyone: `proposed` is success, never a direct delete. REFUSED while the field still holds values (the answer lists them; move or clear them first). Find ids with synap_list_profiles. On approval the definition is deleted.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `propertyDefId` | `string` | yes | Property definition UUID. |
| `reasoning` | `string` |  | Why this field should go — shown to the reviewer. One line. |

## spaces

Listed after `synap_load_skill('tools:spaces')`.

### synap_set_workspace_focus

**Set space focus**

Pin your runtime workspace focus so every subsequent write (create/update/capture) with no explicit workspaceId lands there — the 'use the CRM workspace until I say otherwise' scenario. ADVISORY: a call that DOES pass an explicit workspaceId still overrides the focus; reads are unaffected. Sticky in THIS conversation until cleared — another conversation keeps its own (set the same focus there to share it); the reply's `focusScope` says which. Pass `workspace` as a name (matched against your workspaces) or an id; omit it (or pass 'none'/'clear') to clear the focus. Pinning a space also RETURNS its `brief` (purpose, the kinds it holds, its skills, its playbooks) — the same brief a pinned `synap_orient` serves, so focusing is also how you learn what the space knows before you work in it.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspace` | `string` |  | Workspace name or id to focus on. Omit, or pass 'none'/'clear', to clear the current focus. |

### synap_set_project_focus

**Set project focus**

DECLARE which project you are working on, so writes that don't pin their own project file into it — the 'everything I do now is for the Apollo migration' scenario. Sticky in THIS conversation until cleared — another conversation keeps its own (set the same focus there to share it); the reply's `focusScope` says which. A call that passes an explicit projectId still wins. Filing into a project GRANTS ACCESS to it, so this is a DECLARATION only: the project is verified to exist and be visible to you at set time, and nothing infers a project from what you write. Pass `project` as a name (matched against the projects you can see) or an id; omit it (or pass 'none'/'clear') to clear the focus.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `project` | `string` |  | Project name or id to declare. Omit, or pass 'none'/'clear', to clear the current focus. |

### synap_list_workspaces

**List spaces** · Read-only

List the workspaces (domain lenses) you can see, pod-wide — call this to find a workspace id instead of guessing from orient. Each row: id, name, description, workspaceType, your role, accessKind (member | pod_visible), archived, entityCount, and usedByProjectIds (projects that run through it — an INDEX, not an ACL). Link a project to one with synap_project_use_workspace. To learn what a space is FOR and what it knows — its purpose, the kinds it holds, its rules and its skills — PIN it: pass its workspaceId to `synap_orient`, or `synap_set_workspace_focus`; both return that space's `brief`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `includeArchived` | `boolean` |  | Include soft-archived workspaces (default false) |
| `appId` | `string` |  | Optional filter by settings.appId |

### synap_create_workspace

**Create space**

Escalation L3 (governed — may propose): create a workspace from a definition. Template-first: market.search(kind:template) + prefer install before freehand create. Only after workspace-design four conditions hold and no template fits. Capture never invents a workspace. Pass stable proposalId for idempotency. Creates it on this pod only; an official template is written in the catalog and published with `synap catalog publish --official`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Workspace display name |
| `definition` | `object` |  | Optional WorkspaceProposal definition fields |
| `proposalId` | `string` |  | Idempotency key (optional) |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_update_workspace

**Rename space**

Rename or re-describe an existing Space (the user's word for a workspace; settings are not editable here). Governed: always a proposal for an agent — `proposed` is success.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` | yes | Workspace UUID |
| `name` | `string` |  | New name (1-100 chars). |
| `description` | `string` |  |  |

### synap_update_space_brief

**Edit space brief**

Change a Space's brief (how agents work there: purpose, persona, anchors, what to collect). Only the fields you name change; null removes one. Governed: an agent always proposes, with a before/after the owner reviews — `proposed` is success.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` | yes | Workspace UUID |
| `brief` | `object` | yes | Fields to change: purpose, framing (persona), goal, doneWhen (strings); openingQuestions (string\[\]); expertise \{starters,blindSpots,bar\}; collect \[\{profileSlug,what,cardinality?,keyFields?,min?\}\]; anchors \[\{profileSlug,role:root\|context,entityId?,limit?\}\]; fetch \[\{profileSlug?,query?,note?\}\]. null removes a field. |
| `reasoning` | `string` |  | Why, in the person's words — shown to the reviewer. One line. |

### synap_archive_workspace

**Archive space** · Destructive

Archive a Space (the user's word for a workspace — ids and params keep `workspace`): hide it everywhere and PAUSE its space-scoped rules (automations); pod-wide rules are untouched. Or restore it with restore:true — restore re-enables NOTHING and lists the rules still paused so the person can choose. Owner or pod admin only; system spaces refused. Always a proposal for an agent — `proposed` is success, surface reviewUrl.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` | yes | Workspace UUID (from synap_list_workspaces). |
| `restore` | `boolean` |  | true = un-archive instead. Default false. |
| `reasoning` | `string` |  | Why, in the person's words — shown to the reviewer. One line. |

### synap_retire_workspace

**Move space off a retired template**

Move a Space (workspace) whose template was RETIRED (`synap_template_health` lifecycle `retired` with `replacedBy`) into the space of its replacement: installs the replacement if missing, moves every item whose kind has a home there, and archives the old space only if nothing is left (never deletes). Call with dryRun:true first to see the plan (what moves, what stays and why). Governed: always a proposal for an agent — `proposed` is success, surface reviewUrl.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` | yes | The space on the retired template (UUID). |
| `dryRun` | `boolean` |  | true = return the plan only. Default false. |
| `reasoning` | `string` |  | Why, in the person's words — shown to the reviewer. One line. |

### synap_declare_workspace_source

**Declare space source edge**

Declare a cross-workspace DATA EDGE on an EXISTING workspace by setting/merging its edge fields. `sourceRoles` = per-domain role this workspace plays (provider | consumer | provider-consumer), e.g. Marketing consumes Comms → \{ comms: 'consumer' \}. `defaultSources` = per-domain the workspace to READ that domain from (the source of truth), e.g. \{ comms: \{ workspaceId: '\<comms-ws-id\>' \} \}. MERGES per-domain — existing domains and all other settings are preserved, never clobbered. Use this to wire the enterprise graph (provides/consumes) instead of copying data between workspaces. Editor+ membership required.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` | yes | The workspace to declare the edge ON (the subject). |
| `sourceRoles` | `object` |  | Domain → role map. Values: 'provider' \| 'consumer' \| 'provider-consumer'. Example: \{ "comms": "consumer" \}. Merged per-domain into the existing map. |
| `defaultSources` | `object` |  | Domain → default source workspace to read that domain from. Each value: \{ workspaceId (required), capability?, profileSlug?, label? \}. Example: \{ "comms": \{ "workspaceId": "\<uuid\>" \} \}. Merged per-domain. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_list_projects

**List projects** · Read-only

List the projects (cross-cutting initiatives) you can see, pod-wide, newest first — call this to find a project id before reading, updating or linking it. Each row: id, name, description, status, phase, phaseCategory (legacy label; method position lives on tracks: synap_list_tracks), targetDate, homeWorkspaceId, usedWorkspaceIds (domains it runs through) and its subject. Paginated: \{ items, pagination: \{ hasMore, limit, offset \} \}. Full read: synap_get_project.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `status` | `enum` |  | Optional filter by project status One of: `active`, `archived`, `completed`. |
| `limit` | `number` |  | Page size (1-1000, default 50) |
| `offset` | `number` |  | Offset for pagination (default 0) |

### synap_get_project

**Get project** · Read-only

Read one project: name, description, home workspace, and usedWorkspaces (domains this engagement spans via the uses INDEX). Call after orient when you need the span map. Not an ACL.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project UUID |

### synap_create_project

**Create project**

Create a project — a cross-cutting lens for an initiative/venture that organizes entities across workspaces (a workspace is a domain lens; a project cuts across them). workspaceId = its HOME workspace (optional). A PROJECT IS A COMMITMENT WITH GRAVITY: never create one per git-repo, per-feature, or per-task — those are entities (task/plan/note). You MUST pass evidenceEntityIds: at least 5 existing entity ids that would belong to this project, or the create is rejected. If a same/similar project already exists it is reused (or surfaced) — reuse it instead of making a near-duplicate. An agent call lands as a proposal unless governance has widened it: status="proposed" is SUCCESS — say it is awaiting review, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Project display name |
| `description` | `string` |  |  |
| `workspaceId` | `string` |  | The project's HOME workspace (optional — falls back to the user's first workspace if omitted). |
| `evidenceEntityIds` | `array<string>` |  | REQUIRED for agents: ≥5 existing, visible entity ids that would belong to this project (its gravity). Fewer/invalid ⇒ rejected with guidance to store as an entity or reuse an existing project. |
| `usedWorkspaceIds` | `array<string>` |  | Optional: the spaces (workspace ids) this project runs through — its parts of the job. Each is linked like synap_project_use_workspace, once the project exists. |
| `colorSlot` | `number` |  | Optional: the project's colour as an identity-palette slot 1–12 (never a hex). |
| `icon` | `string` |  | Optional: the project's mark — ONE emoji (e.g. 🚀) or glyph:\<lucide-name\> (e.g. glyph:rocket). |

### synap_update_project

**Update project**

Update a project: rename, re-describe, change status, move its phase, set/clear its target date, or move its home workspace. Reuse this instead of creating a twin. Find the id with synap_list_projects. Governed: `proposed` is success — surface reviewUrl.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project UUID |
| `name` | `string` |  |  |
| `description` | `string` |  |  |
| `status` | `enum` |  | One of: `active`, `archived`, `completed`. |
| `phase` | `string \| null` |  | Legacy lifecycle label (free text, ≤120 chars). Superseded by tracks for method position: use synap_list_tracks / synap_advance_track. null clears it; omit to leave it. |
| `targetDate` | `string \| null` |  | Deadline as an ISO-8601 date (e.g. '2026-12-15'). null clears it; omit to leave it. |
| `homeWorkspaceId` | `string` |  | Move the project's HOME to this workspace (UUID from synap_list_workspaces). You must be able to write it. Entities and sessions stay where they are. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the reviewer. One line. |

### synap_project_use_workspace

**Project uses space** · Destructive

Stamp the INDEX edge project --uses--\> workspace: this engagement runs through that domain. NOT an ACL (does not grant workspace membership). packages/apply with projectId already stamps this; call this when the workspace already exists. Idempotent. Governed: may return proposed. remove:true removes the edge instead (always a proposal for an agent).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes |  |
| `workspaceId` | `string` | yes |  |
| `remove` | `boolean` |  | true = REMOVE this project's uses edge to the workspace (e.g. a retired domain). Default false = add. |
| `reasoning` | `string` |  | Why this project uses this domain, in the person's words. One line. |

### synap_file_into_project

**File into project** · Destructive

File EXISTING records into a project (entity --belongs_to_project--\> project), so the project's members see them. Use this — not link_entities (refused for this edge) and not a projectId property (it files nothing). You must be able to write every record; one refused record refuses the batch. Always a proposal for an agent, one for the whole batch — `proposed` is success, surface reviewUrl. remove:true un-files them instead.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project UUID (from synap_list_projects). |
| `entityIds` | `array<string>` | yes | Entity UUIDs to file (1-500). |
| `remove` | `boolean` |  | true = UN-FILE these records from the project. Default false = file. |
| `reasoning` | `string` |  | Why they belong to this project, in the person's words — shown to the reviewer. One line. |

### synap_export_project_pack

**Export project as suite pack** · Read-only

Serialize a live project for marketplace: returns \{ definition: thin suite, constituents: full workspace packages\[\] \}. Publish constituents first, then the suite (CLI --from-project does both). NOT live entity data. Empty uses-index → error. Read-only.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes |  |

## tracks

Listed after `synap_load_skill('tools:tracks')`.

### synap_list_tracks

**List tracks** · Read-only

List a project's TRACKS — the lasting goals it is pursuing (e.g. 'Grow the newsletter', 'Win the first ten clients' inside one project). Work in a track = synap_start_session with its trackId. A track holds a direction, instructions and a goal, and turns workers on; it has no stages and never completes. LEGACY: an old track also follows a project-scoped playbook (its method, with stages), pinned at the version it started with; a new track has none (playbookId null, no stages). Each row: id, name, playbookId, methodVersion, status (active|paused|completed|archived), pausedBy (check|human|null — why a paused track is held), currentStage and stages \[\{ key, name, position: done|active|not_started, addedAt? \}\] (addedAt marks a stage added after the start), direction (where it is heading, or null) and kpi (\{ label, unit?, target, current?, updatedAt? \} or null — current is STATED, updatedAt says when). Archived tracks are omitted unless includeArchived. Find the project id with synap_list_projects.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project UUID |
| `includeArchived` | `boolean` |  | Also list archived tracks. Default false. |

### synap_start_track

**Start track**

Start a TRACK on a project — a LASTING goal (e.g. 'Grow the newsletter'): pass a `name`, a one-line `direction` in the person's words and, only if the person names a number, a `kpi`. List the project's tracks first (synap_list_tracks) and resume a running one rather than starting a twin. Work in it = synap_start_session with its trackId, one session per sitting, for as long as the goal lasts; it is paused or archived, never completed for hitting a number. No method needed and never stages: playbookId (a project-scoped playbook with stages) is OPTIONAL and legacy — omit it. Repeatable work under it = sessions that follow a playbook, or workers a status starts. While the track is active, its agent may work in the spaces the project links. Idempotent per track NAME: a live track of that name comes back with status='exists'; a live track of another method holding the name is refused. Governed: status='proposed' is SUCCESS — say it awaits review, pass on reviewUrl, do not retry. Otherwise status='started' with the track.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project UUID |
| `playbookId` | `string` |  | OPTIONAL, legacy: UUID of a project-scoped playbook (a method with stages). Omit it — a track needs no method. |
| `name` | `string` |  | The track's name, in the person's words — REQUIRED without a playbookId (with one it defaults to the method's name). Its identity on the project. |
| `params` | `object` |  | Only with a playbookId: answers to the method's declared params, by param name. A mistyped value or an unknown name is refused; params with no method are refused. |
| `direction` | `string` |  | Optional: where the track is heading, ONE line in the person's words. May be non-verifiable — the verifiable part is each session's outcomes. |
| `kpi` | `object` |  | Optional number the track steers by: \{ label, unit?, target, current? \} (numbers). Only if the person names one; never invent a metric. |
| `reasoning` | `string` |  | Why you are starting it, in the person's words — shown to the reviewer. One line. |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess, and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |

### synap_start_stage_session

**Start stage session**

LEGACY — for a track with stages; to work in a track, prefer synap_start_session with trackId (on a track with no stages this tool does exactly that). Start the work session for ONE stage of a track — the session a stage OFFERS when the track enters it (synap_advance_track returns it as `offer`). Advancing a track never starts work; call this only when the person wants that step worked on now. Defaults to the track's CURRENT stage; the stage's goal is the session's brief, its expected outputs and criteria are copied on, and the track's params are passed, so a required param nobody answered becomes a question owed to the person. Idempotent: an open session already filed at that stage comes back with status='existing'. Governed: status='proposed' is SUCCESS — say it awaits review, pass on reviewUrl, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `trackId` | `string` | yes | Track UUID |
| `stageKey` | `string` |  | The stage KEY to work on. Omit for the track's current stage. |
| `title` | `string` |  | Optional one-line session name; defaults to the stage's name. |
| `goal` | `string` |  | Optional outcome in the person's words; defaults to the stage's own goal. |

### synap_advance_track

**Advance track**

LEGACY — only a track started with a method has stages; a track with none is never advanced (work in it = synap_start_session with trackId). Only with the user. Move a track to one of its method's stages — forward OR back (stages are re-enterable; any stage key synap_list_tracks shows). Refused for a stage the method does not declare, or a completed/archived track. A stage may carry a GATE: a human gate pauses the track and files an approval (`proposalId` on the reply); a check gate measures the sessions filed at the stage being left (at least one closed, every closed one passing its criteria) and otherwise holds the track paused (pausedBy='check', `check.reason` says why) until resumed with synap_set_track_status status='active'. Entering a stage does NOT start work: the reply's `offer` is the session that stage suggests — offer it to the person, and start it with synap_start_stage_session only if they want it. Governed: status='proposed' is SUCCESS — pass on reviewUrl, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `trackId` | `string` | yes | Track UUID |
| `toStage` | `string` | yes | The stage KEY to move to (not its display name). |
| `reasoning` | `string` |  | Why the work moves to this stage — shown to the reviewer. One line. |

### synap_set_track_status

**Set track status**

Pause, resume or archive a track. A track is LASTING: pause it while nobody should work on it (its agent loses access to the project's spaces until it resumes); archive it when the goal is dropped — archived is FINAL (start a new track with synap_start_track). Resuming (status='active') also releases a track a check gate held (pausedBy='check'). A move the table does not allow is refused with the reason. Governed: status='proposed' is SUCCESS — pass on reviewUrl, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `trackId` | `string` | yes | Track UUID |
| `status` | `enum` | yes | The status to move the track to. One of: `active`, `paused`, `archived`. |
| `reasoning` | `string` |  | Why the track changes status — shown to the reviewer. One line. |

### synap_set_track_params

**Set track params**

LEGACY (tracks started with a method) — answer (some of) a track's method params — the questions the method DECLARED when the track started (synap_list_tracks returns them as `declaredParams`, with each one's `type`, `required` and any `options`). MERGED onto the answers already given: only the keys you pass change, and a null value clears that answer. A key the method does not declare, or a value of the wrong type, is refused with the reason. Stage sessions started afterwards carry the answers; a required param still unanswered is a question owed to the person. Governed: status='proposed' is SUCCESS — pass on reviewUrl, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `trackId` | `string` | yes | Track UUID |
| `params` | `object` | yes | Answers keyed by the declared param `name`. null clears an answer. |
| `reasoning` | `string` |  | Why these answers — shown to the reviewer. One line. |

### synap_update_track

**Update track**

Set a track's DIRECTION (where it is heading, one line) and/or its KPI (the number it steers by). kpi is MERGED onto the stored one: pass \{ current \} to state the latest value (stamped with when and by whom — it is STATED, never measured), \{ target \} to move the goal, a whole \{ label, unit?, target, current? \} to define one, or null to clear it. direction null clears it. A KPI needs a label and a target. Reaching the target NUDGES the person to re-aim it (a new target) — the track keeps going; never move the target yourself. Governed: status='proposed' is SUCCESS — pass on reviewUrl, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `trackId` | `string` | yes | Track UUID |
| `direction` | `string \| null` |  | One line, in the person's words. null clears it. |
| `kpi` | `object \| null` |  | A KPI patch \{ label?, unit?, target?, current? \} (numbers), or null to clear. |
| `reasoning` | `string` |  | Why — shown to the reviewer. One line. |

### synap_add_track_stage

**Add track stage**

LEGACY — a track has no stages; only an old track started from a method does. Add a stage to such a RUNNING track — work the method did not foresee. Appended after the existing stages; adding never enters it (move there with synap_advance_track, then synap_start_stage_session). A stage carries a name and an optional goal; what it must yield is declared as `outcomes` on the session worked there, and that is what a check gate measures. A key already declared is refused. Governed: status='proposed' is SUCCESS — pass on reviewUrl, do not retry.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `trackId` | `string` | yes | Track UUID |
| `name` | `string` | yes | The stage's name. |
| `goal` | `string` |  | Optional: the brief of the session started at this stage. |
| `key` | `string` |  | Optional stable key (lowercase, digits, - or \_). Omit to derive one from the name. |
| `reasoning` | `string` |  | Why this step is needed — shown to the reviewer. |

## build

Listed after `synap_load_skill('tools:build')`.

### synap_create_view

**Create view**

Create a view in a workspace (recovery when the right view is missing, or proactive once data warrants it). Call synap_list_views first — don't duplicate. For a bento, call synap_list_widgets first and only place keys from that list (stat-card needs profileSlug; view needs a saved viewId, not a profileSlug). Type: one of sheet, list, grid, gallery, kanban, matrix, masonry, calendar, gantt, timeline, graph, flow, map, bento, whiteboard (sheet is the table door). profileId scopes to one entity type. For a whiteboard, pass initialContent to seed the shapes — it is the ONLY way to put content on a board, because content cannot be updated after create. Governed: may propose. On success the result includes `link` (`${PUBLIC_URL}/open/<id>`) — surface that URL to the user.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes |  |
| `type` | `enum` | yes | One of: `sheet`, `list`, `grid`, `gallery`, `kanban`, `matrix`, `masonry`, `calendar`, `gantt`, `timeline`, `graph`, `flow`, `map`, `bento`, `whiteboard`. |
| `workspaceId` | `string` |  | The workspace the view lives in. Required unless projectId is given. |
| `projectId` | `string` |  | Pin the view to a project: it shows only the project's members. Without workspaceId it reads across every workspace the project spans. |
| `profileId` | `string` |  | Profile UUID to scope the view (optional) |
| `config` | `object` |  | View configuration (groupBy, sortBy, filters, etc.) |
| `initialContent` | `object` |  | Whiteboard only — seeds the canvas: \{version:1, category:"canvas", store:\{\<tldrawRecordId\>: \<tldrawRecord\>\}\}. Shapes can ONLY be set here; there is no content-update door, so a board is seeded once at create. Read back with synap_list_views + the view detail. |
| `expectedLabel` | `string` |  | The declared output slot this fulfils, exactly as declared on the session. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_list_views

**List views** · Read-only

List views you own, optionally narrowed by workspaceId, type, or profileId. Call this BEFORE synap_create_view so you do not duplicate an existing board. Owner-only (same floor as hub listViews). Omit filters for the full catalog.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `detail` | `enum` |  | lean (default) = a digest to pick an id from (the heavy config/graph omitted, see `note`); full = complete rows — pair with a small limit. One of: `lean`, `full`. |
| `workspaceId` | `string` |  | Optional workspace UUID to narrow to. Omit for every view you own. |
| `type` | `string` |  | Optional view type filter (table, kanban, list, gallery, calendar, bento, masonry, flow). |
| `profileId` | `string` |  | Optional profile UUID to filter views scoped to that entity type. |

### synap_update_view_content

**Update whiteboard content**

Update the content of an existing whiteboard view (add/move/delete shapes). Goes through governance.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `viewId` | `string` | yes | View UUID to update |
| `store` | `object` | yes | Canonical tldraw payload |
| `elements` | `array` |  | Optional elements array |
| `embeddedEntities` | `array<string>` |  | Optional UUID array of embedded entities |
| `version` | `number` | yes | For optimistic locking |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_list_widgets

**List compose widgets** · Read-only

Live list of what you may place: in a bento (surface "bento", default) or embedded in a document (surface "document"). Call this BEFORE arranging a dashboard or embedding a cell in a document — never guess a key. Each entry has key, requiredConfig, propsSchema (JSON Schema), binding, exampleDirective (the document embed form) and fallback; it covers the built-ins (stat-card, entity-list, view, chart-\*, …) plus the cell:\<pkg\>:\<key\> / generated:\<slug\> cells installed on the pod. view needs a saved view UUID (viewId); a profileSlug is not enough. Counts use stat-card. With surface "document" it also returns fences: content languages written as a fenced block (```mermaid diagrams, ```math equations, code), each with its aiHint.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `surface` | `enum` |  | Where you will place it: bento (dashboard block, default) or document (an embed inside a document). One of: `bento`, `document`. |
| `workspaceId` | `string` |  | Optional workspace UUID. Include it so workspace-scoped generated cells appear. |

### synap_create_cell

**Define cell** · Destructive

Define (create or update) a ViewFrame cell from raw renderer source — idempotent upsert on typeKey (from `name`) + workspace (omit workspaceId for pod-global). An agent call ALWAYS lands as a proposal: status="proposed" is SUCCESS — say it is awaiting review, pass on reviewUrl, do not retry. Creates the definition ONLY: the cell is not surfaced anywhere until it is bound as a renderer for a kind, which is a separate step — ask the user to do it in the Synap app.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Human-readable cell name; also the basis for the generated typeKey (`generated:<slug>`). |
| `rendererSource` | `string` | yes | The cell's renderer source (HTML/JS frame source). |
| `workspaceId` | `string` |  | Optional workspace UUID. Omit for a pod-global cell (visible in all workspaces). |
| `description` | `string` |  | Optional short description of the cell. |
| `viewTypes` | `array<string>` |  | Optional view types this cell can RENDER (e.g. \["list","table"\]). Required for the cell to be selectable as a view renderer — a cell that declares none stays a plain widget and views fall back to the built-in adapter. |
| `contentKind` | `enum` |  | Optional renderer SLOT — WHAT this cell renders. 'entity-detail' = one entity's full page, 'entity-card' = one entity's small block, 'entity-profile' = a whole profile's dashboard, 'collection' = a view of many entities, 'widget' (the default) = generic and placeable only. A cell left at 'widget' is never offered when assigning a renderer to a profile, so declare the slot you actually want. One of: `entity-detail`, `entity-card`, `entity-profile`, `collection`, `widget`. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_promote_cell_to_renderer

**Promote cell to renderer** · Destructive

Escalation L4 — crystallize AFTER a cell has succeeded once as a good recurring presentation. Bind that cell as a profile's renderer for a slot (list | detail | card | dashboard). Durable/consequential → governed like any write (may propose). scope 'workspace' (default) = per-workspace overlay; scope 'pod' = profile system default. Never promote a speculative or unproven one-off.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `profileSlug` | `string` | yes | Slug of the profile to bind the renderer on. |
| `slot` | `enum` | yes | Which renderer slot to set. 'card' = the entity's small embeddable block (this is the ONLY door that reaches it besides Renderer Studio in Builder mode — most personalization requests from a user are this slot, not 'detail'). One of: `list`, `detail`, `card`, `dashboard`. |
| `cellKey` | `string` | yes | The cell typeKey to bind (e.g. from synap_create_cell). |
| `props` | `object` |  | Optional props passed to the cell renderer. |
| `scope` | `enum` |  | 'workspace' (default) = per-workspace overlay; 'pod' = profile system default. Workspace scope requires workspaceId. One of: `workspace`, `pod`, `user`. |
| `workspaceId` | `string` |  | Workspace UUID (required for scope 'workspace'). |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_list_playbooks

**List templates** · Read-only

List playbooks (reusable process/session templates) visible to you across the pod — every member workspace plus pod-wide templates. Call this to discover what already exists BEFORE improvising a new process. Optional workspaceId only narrows (still includes pod-wide). Launch one via synap_run_playbook.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` |  | Optional workspace narrow — only that workspace's playbooks plus pod-wide ones. Omit for the full user-visible catalog. |
| `status` | `enum` |  | Optional status filter. One of: `draft`, `active`, `paused`, `archived`. |
| `limit` | `number` |  | Page size (1–100, default 50). |

### synap_match_playbooks

**Match templates** · Read-only

Suggest playbooks for what the user wants — text-first, suggest-and-confirm, NEVER auto-run the top hit. Pass intentText (what they said — that exact spelling) and/or profileSlug (the kind of thing, e.g. 'post') and/or entityId. At least ONE is required: a call with no signal is REFUSED (without one, every active playbook ties at the same score; to just see what exists, call synap_list_playbooks). Read-only. Returns ranked candidates best first (\{ id, name, scope, templateKind, goalTemplate, subjectProfileSlug, params, executor, score, reason, signals \}); \[\] when none. `scope: 'project'` (templateKind 'Track template') is a method a project runs as a TRACK — start it with synap_start_track; `'session'` ('Work template') is one working session. Pass projectId (or name the project/track in intentText) and a track template ranks above its same-name session twin. Show the reason when you suggest; wait for confirmation before launching via synap_run_playbook (or, on doors that expose sessions, by opening the template as a working session with the chosen entity as its subject). Each of your words is weighted by how rare it is among the candidates, so a specific word outranks one most playbooks share. A playbook is returned only with a reason about THIS request (one of your words, the entity's kind, or one of its roles): a playbook with no subject (e.g. Plan Next Content) comes back only when your words match it. When profileSlug is omitted, every active visible playbook is a candidate.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `profileSlug` | `string` |  | Optional entity profile slug to match against (e.g. 'post', 'deal', 'lead', 'competitor'). Omit to search all active visible playbooks by intentText alone. |
| `entityId` | `string` |  | Optional UUID of the specific entity — round-trip it as the launch's subject once a playbook is chosen. Widens the match with that entity's live facet-role slugs. |
| `workspaceId` | `string` |  | Workspace ID to scope the lookup (optional — falls back to the user's first workspace). Pod-wide playbooks match regardless. |
| `intentText` | `string` |  | What the user said they want. Sufficient on its own — ranks the candidates and each result carries `score` and a human-readable `reason`. A kind or role match is returned without a word match; a playbook with no subject needs one. Show the reason when you suggest a playbook; never run one without the user's confirmation. |
| `projectId` | `string` |  | Optional project you are working in. Never filters — it ranks a track template (scope project) above its same-name session twin. |

### synap_create_playbook

**Create template**

Create a reusable playbook (staged process/session template) for a repeatable workflow — discoverable via synap_list_playbooks, launched via synap_run_playbook. goalTemplate may reference a declared param as \{name\} — a playbook run fills it; an undeclared name, or \{\{name\}\}, is refused. The default scope "session" is a template for one focus session; scope "project" (a staged method a track runs) is LEGACY — a new track needs none. Declare params to make the method self-onboarding: required params become questions for the person when the track starts. A PROCESS on a kind or role: set subjectProfileSlug; it FOLLOWS the lifecycle the profile declares (omit statusProperty, or name that same property — never a second status field). Give each stage the subjectStatus it moves the thing to (an existing option), gates at the commitments, onFail loops, typed expectedOutputs, plus workers (activators: on, size, mode) — load_skill system/synap/processes (model: system/synap/work-model) first. An agent call ALWAYS lands as a proposal: status="proposed" is SUCCESS — say it is awaiting review, pass on reviewUrl, do not retry. Creates it on this pod only; an official template is written in the catalog and published with `synap catalog publish --official`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Playbook display name. |
| `goalTemplate` | `string` | yes | The session goal this playbook instantiates (e.g. 'Analyze competitors for \{market\}', with market declared in params). Reference a param as \{name\}, never \{\{name\}\}. |
| `description` | `string` |  | What this playbook is for. |
| `stages` | `array<object>` |  | Ordered stages. Each: \{ key, name, category, description?, goal?, suggestedTasks?, position?, indefinite?, subjectStatus?, gate?, onFail?, expectedOutputs?, criteria? \}. `category` is REQUIRED — it is the closed rollup axis a cross-playbook board groups on, since your stage `key`s are this playbook's own vocabulary and no other playbook shares them. Stage keys must be unique within the playbook. |
| `status` | `enum` |  | Defaults to 'active' (immediately runnable). One of: `draft`, `active`, `paused`, `archived`. |
| `workspaceId` | `string` |  | Home workspace (optional — falls back to the user's first workspace). |
| `subjectProfileSlug` | `string` |  | Slug of the kind or role this playbook runs ON (e.g. 'crm-lead'), so synap_match_playbooks can offer it for that kind. A slug that resolves to no profile is refused — define the kind first. |
| `statusProperty` | `string` |  | Slug of the subject's lifecycle property (e.g. 'post-status'). Omit it: the process follows the lifecycle the kind or role declares. If you name it, name that same property. Needs subjectProfileSlug. |
| `activators` | `array<object>` |  | Workers: when the process starts on its subject: \[\{ on: 'created' \| 'enters_status' \| 'count' (status+min, one run over the set) \| 'schedule' (cron) \| 'manual', status?, size?: 'per_thing' (default) \| 'per_moment' (count/schedule always are), mode: 'propose' (default) \| 'run', maxPendingApprovals? (default 20, throttles) \}\]. `run` only when cheap and safe. Needs subjectProfileSlug; compiled to a governed rule the owner can pause, fired once per thing per transition. |
| `humanOnlyStatuses` | `array<string>` |  | LEGACY coverage: status options no stage owns, set only by a person (e.g. Idea, Archived). The lifecycle's own humanOnly flag is what the status door enforces; with the stages' subjectStatus values this list should cover every option. |
| `expectedOutputs` | `array<object>` |  | What every run of this playbook must produce (playbook-level slots): each \{ kind, label, relationToSubject? \}. |
| `forceCreate` | `boolean` |  | Only after a CONFLICT naming an overlapping playbook. The pod refuses a near-twin name so processes don't silently fork. Read the candidates it named; set this ONLY if a separate playbook is genuinely right. |
| `scope` | `enum` |  | What the playbook instantiates. 'session' (default): a template for ONE bounded focus session, which a session or a worker follows. 'project': LEGACY — a staged method a track runs (synap_start_track); a new track needs none. One of: `session`, `project`. |
| `params` | `array<object>` |  | What the method needs to know. A required param left unanswered when synap_start_track runs becomes a question owed to the person (answer later with synap_set_track_params); optional ones fall back to their default. |
| `criteria` | `array<object>` |  | Binary acceptance criteria every session of this playbook is graded against. Each: \{ key (lowercase slug), statement, required?, check: \{ kind: evidence\|capability\|judge\|human, capability? (kind 'capability'), evidenceKey? (kind 'evidence'), hint? \}, stageKey? \}. |
| `sessionId` | `string` |  | OPTIONAL focus-session override. Leave it out and the write is attributed automatically — you do NOT normally pass this. Send it ONLY to disambiguate: when two or more of your focus sessions are open, automatic attribution deliberately declines rather than guess, and this is the only way to say which session the write belongs to. Also the way to JOIN a session another conversation started — automatic attribution never shares one across conversations. A session that isn't yours is ignored, not an error. |

### synap_run_playbook

**Run template** · Destructive

LAUNCH a playbook via its executor (discover via synap_list_playbooks) — instantiates a session + run channel + playbook_run and dispatches to the playbook's executor (is-agent | external-agent | hybrid). Pass playbookId OR an unambiguous playbookName/name (multi-match returns candidates — never a silent pick). Write home: workspaceId if set, else the playbook's workspace, else subject/session; pod-wide playbooks with no home reject (pass workspaceId). This LAUNCHES the executor — it is not the same as opening a working session from the template. Governed, with THREE normal outcomes: status="proposed" (an agent launch awaits approval — SUCCESS, pass on reviewUrl, do not retry), status="blocked" (nothing ran: the playbook uses skills that are not enabled yet; `unenabledSkills` names them and `enableProposals` carries ONE enable request per pack — relay the message and run it again only after that is approved), status="running" (it started; `run` + `session` are returned), status="skipped" (nothing ran and nothing is queued — the record's last run failed recently and is cooling down, or no pinned record was ready; read `message`, do not retry in a loop and do not report it as started).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `playbookId` | `string` |  | UUID of the playbook to run. Provide this OR playbookName/name. |
| `playbookName` | `string` |  | Public name of the playbook (case-insensitive). Must be unique among playbooks you can see; ambiguous names return candidates with id + workspaceId. |
| `name` | `string` |  | Alias for playbookName. |
| `params` | `object` |  | Values for the playbook's declared params, filled into its goalTemplate's \{name\} placeholders (e.g. \{ market: 'EU fintech' \}). |
| `subjectId` | `string` |  | Optional entity UUID to bind this run to (the run's subject lens). Also used as write-home fallback for pod-wide playbooks. |
| `agentIds` | `array<string>` |  | Optional agent ids to staff the run with. |
| `reasoning` | `string` |  | Why you're launching this — surfaced on the review proposal. |
| `workspaceId` | `string` |  | Write workspace for the run. Optional when the playbook is already workspace-scoped; required for pod-wide playbooks unless subject/session supplies a home. Never falls back to an arbitrary membership. |
| `projectId` | `string` |  | Optional project UUID to file the run into. Must be a project you can see. |
| `trackId` | `string` |  | Optional track UUID (synap_list_tracks) — the method, inside a project, this run advances. Implies its project; a different projectId is refused. |

### synap_create_skill

**Create tool (skill)**

Author a skill: Documentation (always) + OPTIONAL code. Send `body` alone for a TEACHING skill — reusable prose (a process, a checklist, a house style) that any agent later pulls with synap_load_skill; a teaching skill needs a `slug`, because that is the ref load_skill resolves. Send `code` for a runnable sandboxed executable, or both. `kind` is derived from code presence — do not guess it. For a deterministic provider HTTP call on an already-installed tool use synap_create_verb instead. Governed like every write: an agent create returns status='proposed'. An agent-authored skill is born UNAPPROVED either way and does NOT load or run until the owner approves it — code executes, and instruction prose lands in a future agent's system prompt, so both need a deliberate human OK. Creates it on this pod only; an official template is written in the catalog and published with `synap catalog publish --official`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Stable skill name (e.g. 'normalize_phone_numbers'). |
| `description` | `string` |  | One line: what it does + when to use it. |
| `slug` | `string` |  | Stable ref that synap_load_skill resolves, lowercase path segments (e.g. 'biz/business-plan'). REQUIRED for a documentation-only skill — without it the skill is authored but unreachable. Pod-wide unique; 'system/' is reserved for seeded skills. |
| `code` | `string` |  | Optional executable source (runs sandboxed). Present ⇒ the skill is runnable; absent ⇒ it is a teaching skill. |
| `body` | `string` |  | Markdown documentation — for a teaching skill this IS the skill: the process/checklist/style an agent should follow. For a code skill, how it works, inputs/outputs, when to use it. Required unless `code` is given. |
| `parameters` | `object` |  | Optional runtime parameter schema (shorthand types, e.g. \{ input: 'string', count: 'number?' \}). |
| `workspaceId` | `string` |  | Optional workspace to scope the skill to (default: pod-wide). |

### synap_update_skill

**Update skill**

Revise an existing skill on this pod — its `body` (the teaching prose), `description` or `name` — by `slug` (the ref synap_load_skill resolves) or `id`. Use it when a skill you authored goes stale; never create a second skill under a new slug as a twin. Governed like every write: status='proposed' is success. An agent rewrite of a skill's body is UNAPPROVED until the owner approves it again — that prose lands in future agents' prompts — so do not report it as live.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `slug` | `string` |  | The skill's slug (e.g. 'biz/business-plan'). |
| `id` | `string` |  | The skill's UUID — instead of `slug`. |
| `body` | `string` |  | The full new Markdown body — it REPLACES the old one, so send the whole text, not a diff. |
| `description` | `string` |  | One line: what it does + when to use it. |
| `name` | `string` |  | New display name. |

## capabilities

Listed after `synap_load_skill('tools:capabilities')`.

### synap_list_capabilities

**List tools** · Read-only

Discover what the user can actually DO. Returns a SECTIONED view: `integrations` (their connected services + tools, one per name, each with its `verbs` nested — e.g. google → gmail_send, gmail_search, calendar_list; the verb `id` is what you pass to synap_run_capability), `skills` (standalone runnable skills), and `commands`. Each integration shows `connection.connected` (needs connecting if false) and each verb shows `granted`/`effectiveExecMode` (a not-yet-granted verb must be enabled in Settings → Capabilities). Core built-in tools (already available to you directly as MCP tools) and teaching docs are folded out of this actionable view BY DEFAULT and only COUNTED under `excluded` — pass kind:"builtin-tool" or kind:"teaching-doc" to list them here instead, or synap_load_skill("catalog") for every teaching doc grouped by topic. Pass `query` to search across integrations/verbs/skills (e.g. query:"send email").

| Parameter | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | `string` |  | Workspace UUID |
| `query` | `string` |  | Search text (e.g. 'send email', 'calendar'). Ranks + narrows the result instead of returning the full unfiltered dump. |
| `kind` | `string` |  | Optional exact kind filter: tool \| skill \| command \| source-provider \| builtin-tool \| teaching-doc. |
| `containerId` | `string` |  | Only rows belonging to this capability pack (the `containerId` on integrations/skills, or `builtInPack.containerId`). The default view summarises Synap Core's first-party verbs as ONE `builtInPack` line — pass its containerId to list them. |
| `intent` | `string` |  | Reverse lookup by INTENT slug — what you want to DO, without knowing the vendor. A registry slug (the seed includes search_external, send_message, capture_into_pod, and the rest of the original 13). Returns the concrete verb ids that serve it (pass one to synap_run_capability). Takes precedence over `query`/`kind`/`limit`. An unknown slug is an error from the registry, not an empty list. An empty match list means the slug is known and nothing installed declares it yet — scan without `intent` before concluding the pod cannot do this. |
| `limit` | `number` |  | Max entries to return (default 20 when `query` is set; unset otherwise). |

### synap_run_capability

**Run tool** · Destructive

Run a registered capability verb (discover via synap_list_capabilities) with dynamic inputs — e.g. send an email, search Gmail, create a calendar event. Pass verbId + parameters. A DRAFT (un-enabled) capability is refused — ask the user to enable it first. Governed like any write: an ungranted verb comes back as `{ kind: "proposed", proposalId, reviewUrl }` instead of running — that is SUCCESS, not an error. You MUST surface the `reviewUrl` as a clickable link in your reply (never just say 'proposed' with no link) — e.g. "Queued that email send for your review: \[Review proposal\](\<reviewUrl\>)" — and continue the conversation without waiting for approval.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `verbId` | `string` |  | The capability name from synap_list_capabilities (e.g. 'gmail_send'). Alternatively pass skillId. |
| `skillId` | `string` |  | Direct backing-skill UUID (alternative to verbId). |
| `parameters` | `object` |  | The capability's inputs — e.g. \{ to, subject, body \} for gmail_send, \{ query \} for gmail_search. |
| `workspaceId` | `string` | yes | Workspace UUID |
| `connectionSelector` | `object` |  | Which of the capability's connections this run uses, when it has more than one. `connectionId` names a specific connection; `contextObjectId` names the connection bound to that context object. Omit to use the only/default connection — a selector matching nothing fails the run. |
| `idempotencyKey` | `string` |  | Optional: a stable key that correlates retries of THIS run. Note: a direct capability run has NO automatic content dedup — a retried call CAN produce a second external effect (e.g. a second send). Pass a key when the effect must be at-most-once; the key is recorded on the run. |

### synap_create_verb

**Create verb**

Add a new DECLARATIVE verb (a deterministic provider HTTP call — no code execution) to an ALREADY-INSTALLED, already-credentialed tool — e.g. teach 'apify_api' a new 'apify_search_reddit_actors' verb without a dev-session/redeploy. Creates a kind='declarative' skill only; rejects any request implying code/instruction/builtin execution. `toolName` MUST already exist (installed + visible to the caller) or this is refused — it never creates a new tool/connection as a side effect. Governed the same as every other write: may return status='proposed' for review. Discover installed tools with synap_list_capabilities first.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `toolName` | `string` | yes | The NAME of the already-installed tool this verb calls (e.g. 'apify_api'). Must already exist — this tool never creates a new tool/connection. |
| `verbName` | `string` | yes | Stable name for the new verb/skill (e.g. 'apify_search_reddit_actors'). |
| `description` | `string` |  | What the verb does and when to use it. |
| `method` | `enum` | yes | HTTP method the provider call uses. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. |
| `pathTemplate` | `string` | yes | Request path with \{\{param\}\} interpolation, e.g. '/v2/acts/\{\{actorId\}\}/runs'. |
| `query` | `object` |  | Query params; values may be '\{\{param\}\}'; arrays become repeated query keys. |
| `body` | `object` |  | Request body template; values may be '\{\{param\}\}'. |
| `responseShape` | `object` |  | How to shape the provider's raw response (collectionPath, item, scalar, headers dot-paths). |
| `parameters` | `object` |  | The verb's own runtime parameter schema using the shorthand type system, e.g. \{ query: "string", limit: "number?" \}. |
| `workspaceId` | `string` |  | Optional workspace UUID to scope the new verb to (default: pod-wide). |

### synap_list_automations

**List rules** · Read-only

List automations (WHEN-triggered flows) visible in a workspace — the reactive rules a user has set up (e.g. 'on new lead → draft a follow-up', 'every morning → recap yesterday'). Each entry has its id (pass to synap_trigger_automation), name, triggerType (event | cron | webhook | manual), and status (active | draft | paused | error). Read-only. Call this to discover what already reacts BEFORE creating a new automation. Omit workspaceId to list everything accessible.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `detail` | `enum` |  | lean (default) = a digest to pick an id from (the heavy config/graph omitted, see `note`); full = complete rows — pair with a small limit. One of: `lean`, `full`. |
| `workspaceId` | `string` |  | Optional workspace UUID to narrow to (pod-wide automations are always included). Omit for all accessible. |
| `status` | `enum` |  | Optional status filter. One of: `draft`, `active`, `paused`, `error`. |
| `limit` | `number` |  | Max entries to return (default 50). |

### synap_trigger_automation

**Run rule now** · Destructive

Run an existing automation NOW, on demand (discover ids via synap_list_automations) — e.g. fire a 'daily client recap' immediately instead of waiting for its schedule. Pass the automation id; optionally a payload injected as the run's trigger.payload. This is a RUN, not a proposal: it returns \{ status: 'triggered', runId \} once enqueued (gated by your write access to the automation's workspace). Any entity writes the run then performs are separately reviewed under the automation's own governance. A draft automation is runnable on demand this way; paused/error non-manual ones are refused.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | UUID of the automation to trigger. |
| `payload` | `object` |  | Optional data injected as trigger.payload in the run context (e.g. \{ entityId \} to bind the run to a subject). |
| `workspaceId` | `string` |  | Optional workspace lens. When set, only automations in that workspace can be triggered; omit to trigger a pod-wide automation. |
| `reasoning` | `string` |  | Why you are making this write, in the person's words — shown to the human who reviews it. One line. |

### synap_create_automation

**Create rule flow (advanced)**

Create an automation — a WHEN→THEN flow that reacts to a trigger (event | cron | webhook | manual) by running a flow of steps. Use for repeatable reactions ('every morning recap each client', 'on new deal notify the channel'). Governed the same as every write: an agent create returns status='proposed' for review; on approval it becomes ACTIVE (live) — not a stuck draft. Provide the trigger, a flowDefinition (\{ nodes, edges \}), and a dataContract — the contract is REQUIRED for agent-authored automations (which this always is) and the create door rejects the automation without a valid one. For a cron trigger put the schedule in triggerConfig.expression (5-field cron). Discover what already exists with synap_list_automations first. Creates it on this pod only; an official template is written in the catalog and published with `synap catalog publish --official`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Automation display name. |
| `description` | `string` |  | What this automation does. |
| `triggerType` | `enum` | yes | WHEN it fires: 'event' (something happened, e.g. entity created), 'cron' (schedule — put the 5-field expression in triggerConfig.expression), 'webhook' (external call), 'manual' (only via synap_trigger_automation). One of: `event`, `cron`, `webhook`, `manual`. |
| `triggerConfig` | `object` |  | Trigger settings. cron → \{ expression: '0 9 \* \* \*' \}. event → \{ eventPattern: 'entity.create.completed', filters: \{ ... \} \}. FILTERS GRAMMAR (enforced — a filter the runtime cannot evaluate is REJECTED, because an automation with an unevaluable filter installs as 'active' and then never fires): each key is a dot-notation path into the event data ('profileSlug', 'channel.contextObjectType'); each value is EITHER a plain string/number/boolean/null (exact match) OR an operator object — $eq, $ne, $in (non-empty array), $gt, $gte, $lt, $lte (numeric). E.g. \{ profileSlug: 'person' \}, \{ profileSlug: \{ $in: \['person','contact'\] \} \}, \{ score: \{ $gt: 30 \} \}. ENTITY PROPERTIES ARE FLAT: 'entity.update.completed' spreads the entity's changed properties onto event data under their BARE slug, so the key is 'score' — NOT 'properties.score', which resolves to undefined and silently never matches. A bare array value and a nested object value are BOTH rejected: use $in for several values, and a dot-notation key to reach nested data. |
| `flowDefinition` | `object` | yes | The THEN flow: \{ nodes: \[...\], edges: \[...\] \}. Nodes are the steps; edges wire them in order. A step of type 'capability' MUST name a verb that actually exists — data.verbId is the backing skill's NAME (e.g. 'ai.generate'), not a UUID, and the create is REJECTED if it does not resolve for you (data.capabilityId is optional; omit it and the verb alone resolves). Check with synap_list_capabilities before authoring capability steps. |
| `status` | `enum` |  | Defaults to 'active' (live once approved). One of: `draft`, `active`, `paused`, `error`. |
| `resultRouting` | `enum` |  | Optional: where run results are posted. 'per_entity' = the subject entity's own channel, 'per_type' = one channel per entity type, 'trigger' = the triggering channel. One of: `per_type`, `per_entity`, `trigger`. |
| `metadata` | `object` | yes | Metadata bag. REQUIRED here: `dataContract` — this tool is always agent-authored, and automations.create REJECTS an agent-authored automation whose metadata carries no valid contract. Any other keys you add are kept as-is. |
| `workspaceId` | `string` |  | Optional workspace to scope to (default pod-wide). |
