@synap-core/cli 1.2.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/commands/agents.d.ts +31 -0
- package/dist/commands/agents.js +478 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/connect.d.ts +18 -1
- package/dist/commands/connect.js +154 -74
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connections.d.ts +9 -0
- package/dist/commands/connections.js +161 -0
- package/dist/commands/connections.js.map +1 -0
- package/dist/commands/data.d.ts +43 -0
- package/dist/commands/data.js +387 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/finish.js +41 -8
- package/dist/commands/finish.js.map +1 -1
- package/dist/commands/infra.d.ts +21 -0
- package/dist/commands/infra.js +262 -0
- package/dist/commands/infra.js.map +1 -0
- package/dist/commands/init.js +188 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/knowledge.d.ts +36 -0
- package/dist/commands/knowledge.js +123 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/openclaw.d.ts +2 -0
- package/dist/commands/openclaw.js +300 -23
- package/dist/commands/openclaw.js.map +1 -1
- package/dist/commands/pods.d.ts +17 -0
- package/dist/commands/pods.js +371 -0
- package/dist/commands/pods.js.map +1 -0
- package/dist/commands/status.d.ts +14 -1
- package/dist/commands/status.js +78 -220
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts +11 -2
- package/dist/commands/update.js +116 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +370 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/agents-config.d.ts +20 -0
- package/dist/lib/agents-config.js +45 -0
- package/dist/lib/agents-config.js.map +1 -0
- package/dist/lib/auth.d.ts +4 -0
- package/dist/lib/auth.js +4 -0
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/browser-auth.d.ts +35 -0
- package/dist/lib/browser-auth.js +170 -0
- package/dist/lib/browser-auth.js.map +1 -0
- package/dist/lib/hub-client.d.ts +17 -0
- package/dist/lib/hub-client.js +115 -0
- package/dist/lib/hub-client.js.map +1 -0
- package/dist/lib/openclaw.js +30 -19
- package/dist/lib/openclaw.js.map +1 -1
- package/dist/lib/pod.d.ts +32 -1
- package/dist/lib/pod.js +121 -9
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/skills-installer.d.ts +18 -0
- package/dist/lib/skills-installer.js +97 -0
- package/dist/lib/skills-installer.js.map +1 -0
- package/dist/lib/targets.d.ts +65 -0
- package/dist/lib/targets.js +673 -0
- package/dist/lib/targets.js.map +1 -0
- package/package.json +5 -3
- package/skills/README.md +91 -0
- package/skills/synap/README.md +76 -0
- package/skills/synap/SKILL.md +882 -0
- package/skills/synap/capture.md +170 -0
- package/skills/synap/governance.md +206 -0
- package/skills/synap/linking.md +128 -0
- package/skills/synap/scripts/orient.sh +28 -0
- package/skills/synap-schema/SKILL.md +231 -0
- package/skills/synap-schema/property-types.md +228 -0
- package/skills/synap-ui/SKILL.md +295 -0
- package/skills/synap-ui/bento-recipes.md +608 -0
- package/skills/synap-ui/view-types.md +259 -0
- package/skills/synap-ui/widget-catalog.md +305 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: synap
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill whenever the user wants to capture, remember, find, or structure
|
|
5
|
+
information in their Synap data pod. Triggers: creating a task, note, person,
|
|
6
|
+
company, project, event, contact, or deal; saving an article or webpage;
|
|
7
|
+
storing a fact about someone ("Alice prefers async"); searching the user's
|
|
8
|
+
knowledge ("find my notes on X", "who did I meet last week"); linking entities;
|
|
9
|
+
logging a meeting or a contact; capturing unstructured text into structured
|
|
10
|
+
entities; reading what's in the user's pod before answering questions about
|
|
11
|
+
their life, work, or projects; posting to their personal AI channel. The pod
|
|
12
|
+
is the user's sovereign source of truth — prefer it over your own context
|
|
13
|
+
when the user asks about their own data. Do NOT use this skill for extending
|
|
14
|
+
the schema (use synap-schema) or building dashboards and views (use synap-ui).
|
|
15
|
+
metadata:
|
|
16
|
+
openclaw:
|
|
17
|
+
requires:
|
|
18
|
+
env: [SYNAP_HUB_API_KEY, SYNAP_POD_URL]
|
|
19
|
+
optional_env:
|
|
20
|
+
[SYNAP_WORKSPACE_ID, SYNAP_USER_ID, SYNAP_DEFAULT_CHANNEL_ID]
|
|
21
|
+
primaryEnv: SYNAP_HUB_API_KEY
|
|
22
|
+
homepage: https://synap.live
|
|
23
|
+
capabilities: [memory, knowledge-graph, channels]
|
|
24
|
+
os: [macos, linux, windows]
|
|
25
|
+
userInvocable: false
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Synap — core data operations
|
|
29
|
+
|
|
30
|
+
You are connected to a **Synap Data Pod** at `{SYNAP_POD_URL}`. All requests use `Authorization: Bearer {SYNAP_HUB_API_KEY}`.
|
|
31
|
+
|
|
32
|
+
**If you have Bash access** (Claude Code, agent with tools): use the `synap` CLI — see **CLI Data Operations** below. Auth is automatic, `--json` gives clean output, no manual header management.
|
|
33
|
+
|
|
34
|
+
**If you only have HTTP access**: use the REST endpoints documented below. Your `userId` is in `{SYNAP_USER_ID}` (set by `synap connect`). If it's missing, call `GET /api/hub/users/me` → `.id` once.
|
|
35
|
+
|
|
36
|
+
Your job is to turn unstructured input into a **connected** knowledge graph. Isolated entities are anti-value. Every entity you create should link to at least one other entity.
|
|
37
|
+
|
|
38
|
+
## Mental model
|
|
39
|
+
|
|
40
|
+
Synap is a typed knowledge graph. Six layers you need:
|
|
41
|
+
|
|
42
|
+
| Layer | What it is | When to use |
|
|
43
|
+
| ------------- | ---------------------------------------------- | -------------------------------------------------------- |
|
|
44
|
+
| **Entities** | Typed structured nodes (task, person, …) | Anything worth filtering, sorting, or linking |
|
|
45
|
+
| **Relations** | Typed edges between entities | Making the graph traversable |
|
|
46
|
+
| **Documents** | Long-form markdown attached to an entity | Meeting notes, research writeups, articles |
|
|
47
|
+
| **Memory** | Atomic facts, no structure | Preferences, context, ephemeral notes |
|
|
48
|
+
| **Threads** | Channel conversations, optional entity context | Posting to the user's personal AI channel |
|
|
49
|
+
| **Proposals** | Writes queued for human approval | Governance for some mutations (not an error — see below) |
|
|
50
|
+
|
|
51
|
+
## Synap-first operating mode
|
|
52
|
+
|
|
53
|
+
> **MCP clients** (Claude Desktop, Raycast, OpenClaw with MCP): use `synap_*` tool names — they wrap auth and governance automatically. **REST / HTTP clients**: use the endpoints below.
|
|
54
|
+
|
|
55
|
+
These five rules override default assistant behavior when connected to a Synap pod:
|
|
56
|
+
|
|
57
|
+
**1. Orient before acting**
|
|
58
|
+
Run `scripts/orient.sh` or call these endpoints at the start of every session — before searching, before creating, before answering any question about the user's data:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
GET /api/hub/manifest
|
|
62
|
+
→ static capability map: view types, bento block kinds, inline patterns, browser-native cells
|
|
63
|
+
|
|
64
|
+
GET /api/hub/users/me
|
|
65
|
+
→ { id, email, name } ← your userId
|
|
66
|
+
|
|
67
|
+
GET /api/hub/workspaces
|
|
68
|
+
→ [{ id, name, role }] ← workspaces[0].id if only one
|
|
69
|
+
|
|
70
|
+
GET /api/hub/profiles?userId={userId}&workspaceId={workspaceId}
|
|
71
|
+
→ [{ slug, displayName, entityScope, properties }]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`entityScope: "pod"` = visible across all workspaces (note, task, project, person, company, bookmark, event, contact, article, website).
|
|
75
|
+
`entityScope: "workspace"` = scoped to one workspace (deal, file, capture, custom profiles).
|
|
76
|
+
|
|
77
|
+
**2. Search before answering**
|
|
78
|
+
Before answering any question about the user's projects, tasks, contacts, decisions, or anything they might have captured — search Synap first. Do not answer from your training or context window when Synap may have the authoritative answer.
|
|
79
|
+
|
|
80
|
+
**3. Save proactively — without waiting to be asked**
|
|
81
|
+
When the user shares a decision, task, meeting outcome, contact, or any durable information: save it. Don't ask "should I save this?" for obviously important information. Use:
|
|
82
|
+
|
|
83
|
+
- entities for structured data (tasks, people, projects, decisions)
|
|
84
|
+
- `remember_fact` / `POST /api/hub/memory` for preferences, context, loose facts
|
|
85
|
+
- documents for long-form notes (meeting notes, research, writeups)
|
|
86
|
+
|
|
87
|
+
**4. Link everything**
|
|
88
|
+
An isolated entity has no value in a knowledge graph. When creating entities, immediately link them to related entities. A task belongs to a project. A note belongs to a meeting or a person. A decision belongs to a project and may supersede another decision.
|
|
89
|
+
|
|
90
|
+
**5. Persist facts, not just conversation**
|
|
91
|
+
Facts about the user — preferences, team, working style, recurring context — belong in Synap memory, not in your context window. Memory survives sessions and is accessible across all AI surfaces. Context does not.
|
|
92
|
+
|
|
93
|
+
Properties with `valueType: "entity_id"` are typed links to other entities — see **Linking** below.
|
|
94
|
+
|
|
95
|
+
## CLI Data Operations (Bash tool)
|
|
96
|
+
|
|
97
|
+
When Claude Code (or any agent with Bash access) is using this skill, prefer the `synap` CLI over raw HTTP calls — auth is automatic, output is clean JSON, no spinners in `--json` mode.
|
|
98
|
+
|
|
99
|
+
**Session context — set once, never repeat:**
|
|
100
|
+
|
|
101
|
+
The CLI reads the active pod and workspace from `~/.synap/config.json`. Set context once at the start of a session; all subsequent commands inherit it automatically. Do NOT pass `--pod-url`, `--api-key`, or `--workspace` on every command.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
synap pods use <profile-name> # switch active pod
|
|
105
|
+
synap use <workspace-id> # switch active workspace
|
|
106
|
+
synap workspace provision-agent --json # provision your agent workspace + auto-sets it active
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Always orient first:**
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
synap orient --json
|
|
113
|
+
# Returns: userId, podUrl, workspaces[{id, name, slug}]
|
|
114
|
+
# Never hardcode workspace IDs — discover them here.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Search (Typesense-powered, cross-collection):**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
synap search "project ideas" --json
|
|
121
|
+
synap search "Antoine" --type=entity --workspace=<id> --json
|
|
122
|
+
synap search "meeting notes" --type=doc --limit=5 --json
|
|
123
|
+
# Omit --workspace to search pod-wide. Include it to scope to one workspace.
|
|
124
|
+
# Use search for name/keyword queries. For semantic/conceptual search, use Hub Protocol memory endpoints.
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Read entities:**
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
synap list workspaces --json
|
|
131
|
+
synap list entities --workspace=<id> --json
|
|
132
|
+
synap list entities --profile=task --workspace=<id> --json
|
|
133
|
+
synap get entity <id> --json
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Episodic memory (session facts, loose context):**
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
synap remember "Key decision: use Typesense for search" --json
|
|
140
|
+
synap recall "Typesense" --limit=5 --json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Structured knowledge (durable, typed, searchable — preferred for engineering learnings):**
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Capture a gotcha, lesson, decision, or reference into your agent workspace
|
|
147
|
+
synap capture --type gotcha --claim "Hono static routes must come before /:id" \
|
|
148
|
+
--why "First-match routing; dynamic routes eat static ones" \
|
|
149
|
+
--tags "repo:synap-backend,layer:routing" --json
|
|
150
|
+
|
|
151
|
+
synap capture --type lesson --claim "code-read ≠ runtime-true for library APIs" \
|
|
152
|
+
--evidence "tldraw 2.4.6 binding API changed silently from props.start.boundShapeId"
|
|
153
|
+
|
|
154
|
+
# Recall across your knowledge base with full-text search
|
|
155
|
+
synap recall "hono routing" --structured --json
|
|
156
|
+
synap recall "tldraw" --structured --type gotcha --json
|
|
157
|
+
|
|
158
|
+
# Prerequisite (run once): provision your agent workspace and set it active
|
|
159
|
+
synap workspace provision-agent --json
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`synap capture` / `synap recall --structured` uses the `engineering_knowledge` entity profile.
|
|
163
|
+
`synap remember` / `synap recall` (without `--structured`) uses the ephemeral `/memory` store.
|
|
164
|
+
Use structured knowledge for anything worth remembering across sessions and projects.
|
|
165
|
+
|
|
166
|
+
**Write:**
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
synap create entity --profile=note --name="Meeting notes" --workspace=<id> --json
|
|
170
|
+
synap set entity <id> --props='{"status":"done"}' --json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Multi-agent:** If `SYNAP_AGENT` env var is set, the CLI uses that named identity's API key from `~/.synap/config.json` instead of the default pod credentials. Use `synap agents list` to see configured identities.
|
|
174
|
+
|
|
175
|
+
**Rules:**
|
|
176
|
+
|
|
177
|
+
- Always use `--json` when calling from code — clean stdout, no spinners, machine-parseable
|
|
178
|
+
- Run `synap orient` first to discover workspace IDs — never hardcode them
|
|
179
|
+
- Omit `--workspace` to operate pod-wide; include it to scope to a specific workspace
|
|
180
|
+
- `synap search` is Typesense (fast keyword/name search). Semantic search → Hub Protocol `GET /api/hub/memory?query=…`
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Scope — default pod-wide
|
|
185
|
+
|
|
186
|
+
**Default: pod-wide.** 13 of 17 system profiles (`note`, `task`, `project`, `event`, `person`, `contact`, `company`, `bookmark`, `article`, `website`, `decision`, `question`, `research`) are pod-scoped — entities you create show up in _every_ workspace the user owns. The backend handles this automatically when the profile is pod-scoped: you don't need to pass `workspaceId`.
|
|
187
|
+
|
|
188
|
+
**Scope a creation to one workspace only when:**
|
|
189
|
+
|
|
190
|
+
1. The user explicitly says "in my `X` workspace" / "inside this space".
|
|
191
|
+
2. You're inside a clear workspace context (the user is on a project page, discussing that project — new tasks go into that workspace).
|
|
192
|
+
3. The profile is workspace-scoped by definition (`deal`, `file`, `capture`, and custom profiles). The backend already uses the user's active workspace when you don't pass one — usually this is what you want.
|
|
193
|
+
|
|
194
|
+
**Rule of thumb:** don't pass `workspaceId` unless the user's intent specifically narrows to one workspace. A task the user dictates "from the couch" belongs to the whole pod, not to whichever workspace was last open.
|
|
195
|
+
|
|
196
|
+
When you do scope to a workspace, pass `workspaceId` in the create body — the backend respects it. Never pass `workspaceId: null` explicitly to force pod-wide; the profile's `entityScope` decides.
|
|
197
|
+
|
|
198
|
+
## The work flow — question → research → decision → action
|
|
199
|
+
|
|
200
|
+
AI-assisted work has a shape. When the user is actually _thinking about something_, it flows through four structural nodes. Each is a first-class entity. None of these are optional "nice-to-have" labels — they're the graph that makes the work _durable_ and transferrable between AIs.
|
|
201
|
+
|
|
202
|
+
| Stage | Entity | What it captures | Typical trigger |
|
|
203
|
+
| ----------- | ---------- | -------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
204
|
+
| Inquiry | `question` | What the user is trying to figure out | "I'm wondering about X" / "Should we Y or Z?" / "What's the best…" |
|
|
205
|
+
| Exploration | `research` | Investigation: sources consulted, conclusion, confidence | Reading articles, comparing options, summarizing findings |
|
|
206
|
+
| Resolution | `decision` | What was chosen + rationale + alternatives | "We decided to…" / "Let's go with…" / "I'm going with…" |
|
|
207
|
+
| Execution | `task` | Concrete action items that follow the decision | "Now I need to…" / "TODO: ship Y by…" |
|
|
208
|
+
|
|
209
|
+
**Link each stage to the next:**
|
|
210
|
+
|
|
211
|
+
- `question.answeredByDecisionId` → the decision that closed it
|
|
212
|
+
- `research.questionId` → the question it investigates
|
|
213
|
+
- `decision.projectId` → the project it affects (same for question / research)
|
|
214
|
+
- Use `POST /relations type=source` to link research to its sources (articles, websites, documents)
|
|
215
|
+
|
|
216
|
+
Traversing in either direction gives the user answers like:
|
|
217
|
+
|
|
218
|
+
- "What am I currently exploring about Project Eve?" → `GET /entities?profileSlug=question&…` filtered by open
|
|
219
|
+
- "What decisions have we made on this project?" → filtered by `projectId`
|
|
220
|
+
- "What was the research behind this decision?" → reverse-lookup from `decision` via the research entities that reference the same `projectId` and question
|
|
221
|
+
|
|
222
|
+
### When to create each
|
|
223
|
+
|
|
224
|
+
**`question` — substantive inquiries only.** The test: _would the user want to find this later?_ "What's the weather" = no, don't create. "Should we use LangGraph or CrewAI?" = yes, create. Casual chitchat never becomes a question.
|
|
225
|
+
|
|
226
|
+
**`research` — when you investigate.** Any time you go off and read articles / websites / past notes to answer something, that's research. Create the entity upfront (`status: "ongoing"`), link sources as you pull them (`POST /relations type=source`), set `conclusion` when you're done (`status: "concluded"`).
|
|
227
|
+
|
|
228
|
+
**`decision` — when the user picks a path.** Already covered in the memory-vs-entity section above. Link back to the question it answers (set `question.answeredByDecisionId`).
|
|
229
|
+
|
|
230
|
+
**`task` — when the decision implies concrete work.** Link with `projectId` if not already inferred.
|
|
231
|
+
|
|
232
|
+
### Worked example
|
|
233
|
+
|
|
234
|
+
User: _"I'm trying to figure out whether we should build our own orchestrator or standardize on OpenClaude's. Can you help me think through it?"_
|
|
235
|
+
|
|
236
|
+
1. Create the question:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
POST /api/hub/entities
|
|
240
|
+
{ "profileSlug": "question",
|
|
241
|
+
"title": "Build custom orchestrator or use OpenClaude native?",
|
|
242
|
+
"properties": {
|
|
243
|
+
"questionStatus": "exploring",
|
|
244
|
+
"askedAt": "2026-04-20",
|
|
245
|
+
"projectId": "ent_project_eve",
|
|
246
|
+
"description": "Weighing separation-of-concerns vs. out-of-the-box capability."
|
|
247
|
+
} }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
2. As you investigate, create a research entity and link sources:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
POST /api/hub/entities
|
|
254
|
+
{ "profileSlug": "research",
|
|
255
|
+
"title": "LangGraph vs CrewAI capability survey",
|
|
256
|
+
"properties": {
|
|
257
|
+
"researchStatus": "ongoing",
|
|
258
|
+
"questionId": "ent_question_1",
|
|
259
|
+
"projectId": "ent_project_eve"
|
|
260
|
+
} }
|
|
261
|
+
|
|
262
|
+
POST /api/hub/relations
|
|
263
|
+
{ "sourceEntityId": "ent_research_1", "targetEntityId": "ent_article_langgraph_docs", "type": "source" }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
3. When you reach a conclusion, update the research:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
PATCH /api/hub/entities/ent_research_1
|
|
270
|
+
{ "properties": {
|
|
271
|
+
"researchStatus": "concluded",
|
|
272
|
+
"conclusion": "LangGraph separates orchestration brain from UX. CrewAI adds agent abstractions but couples to its runtime.",
|
|
273
|
+
"researchConfidence": "high"
|
|
274
|
+
} }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
4. When the user picks, create a decision linked to the question:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
POST /api/hub/entities
|
|
281
|
+
{ "profileSlug": "decision",
|
|
282
|
+
"title": "Use LangGraph orchestrator over OpenClaude native",
|
|
283
|
+
"properties": {
|
|
284
|
+
"decisionStatus": "accepted",
|
|
285
|
+
"decidedAt": "2026-04-22",
|
|
286
|
+
"rationale": "Separates Orchestration Brain from UX.",
|
|
287
|
+
"alternatives": "Standardize on OpenClaude's multi-agent logic.",
|
|
288
|
+
"projectId": "ent_project_eve"
|
|
289
|
+
} }
|
|
290
|
+
|
|
291
|
+
PATCH /api/hub/entities/ent_question_1
|
|
292
|
+
{ "properties": {
|
|
293
|
+
"questionStatus": "answered",
|
|
294
|
+
"answeredByDecisionId": "ent_decision_1"
|
|
295
|
+
} }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
5. Tasks follow as usual, linked to the project.
|
|
299
|
+
|
|
300
|
+
**The payoff:** six months later, any AI (or the user alone) can reconstruct the reasoning by traversing from the project → question → research → decision → tasks. That's the durability Synap provides on top of chat.
|
|
301
|
+
|
|
302
|
+
### Creation is silent by default
|
|
303
|
+
|
|
304
|
+
Don't interrupt the conversation to ask "should I log this as a question?" — just do it and add a one-line trailer at the end of your response:
|
|
305
|
+
|
|
306
|
+
> (Logged as question on Project Eve. Review: https://studio.synap.live/proposals/…)
|
|
307
|
+
|
|
308
|
+
If the creation was auto-approved (entity.create is on the whitelist), there's no proposal; just show a link to the entity:
|
|
309
|
+
|
|
310
|
+
> (Logged as question → https://studio.synap.live/entities/ent_question_1)
|
|
311
|
+
|
|
312
|
+
## Linking — the core principle
|
|
313
|
+
|
|
314
|
+
**Never create orphan entities.** A task alone is near-useless. A task linked to a project, an assignee, and the source document shows up in traversals, context panels, and downstream queries.
|
|
315
|
+
|
|
316
|
+
Two ways to connect. Pick one:
|
|
317
|
+
|
|
318
|
+
**Way 1 — entity_id properties (fast path, auto-syncs).** Set the property when creating the entity. For system profiles this auto-creates a row in the relations table.
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
POST /api/hub/entities
|
|
322
|
+
{
|
|
323
|
+
"userId": "{userId}",
|
|
324
|
+
"workspaceId": "{workspaceId}",
|
|
325
|
+
"profileSlug": "task",
|
|
326
|
+
"title": "Design new onboarding flow",
|
|
327
|
+
"properties": {
|
|
328
|
+
"status": "todo",
|
|
329
|
+
"priority": "high",
|
|
330
|
+
"projectId": "ent_abc", // auto-creates belongs_to_project relation
|
|
331
|
+
"assignee": "usr_def" // auto-creates assigned_to relation
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Way 2 — explicit relations.** For custom links, after-the-fact connections, or anything without a matching entity_id property.
|
|
337
|
+
|
|
338
|
+
```json
|
|
339
|
+
POST /api/hub/relations
|
|
340
|
+
{
|
|
341
|
+
"userId": "{userId}",
|
|
342
|
+
"sourceEntityId": "ent_task",
|
|
343
|
+
"targetEntityId": "ent_document",
|
|
344
|
+
"type": "references"
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
For auto-sync mapping, conventional relation types, and edge cases, read **`linking.md`**.
|
|
349
|
+
|
|
350
|
+
## Writing — governance in one paragraph
|
|
351
|
+
|
|
352
|
+
Every write returns a `status` field:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
"approved" → done, use { id }
|
|
356
|
+
"proposed" → queued for user approval; response also carries { proposalId, summary, reasoning, reviewPath, reviewUrl } — surface the link
|
|
357
|
+
"denied" → blocked, explain reason to user
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**`"proposed"` is not an error.** It's the governance system queueing your change. When you get it:
|
|
361
|
+
|
|
362
|
+
1. Tell the user exactly what was queued — use the `summary` field **verbatim**. Don't paraphrase.
|
|
363
|
+
2. Give them the link to review — `reviewUrl` opens the proposal in Synap Studio. Show the link as-is.
|
|
364
|
+
3. Move on with the conversation. Don't wait or poll.
|
|
365
|
+
|
|
366
|
+
Example response to the user:
|
|
367
|
+
|
|
368
|
+
> I queued **Delete task "Q2 plan review"** for your review. Destructive actions need your approval. Open it: https://studio.synap.live/proposals/prp_abc
|
|
369
|
+
|
|
370
|
+
Auto-approved by default (for agent API keys): `entity.create`, `entity.update`, `document.create`, `relation.create`, `view.create`, `profile.create`, `property_def.create`, `channel.create`, `memory.*`, all reads. Destructive actions (`delete`, `archive`, `purge`) always propose in agent-owned workspaces.
|
|
371
|
+
|
|
372
|
+
For the full whitelist, agent-user semantics, and workspace overrides, read **`governance.md`**.
|
|
373
|
+
|
|
374
|
+
## Core writes
|
|
375
|
+
|
|
376
|
+
### Create an entity (always with links)
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
POST /api/hub/entities
|
|
380
|
+
{
|
|
381
|
+
"userId": "{userId}",
|
|
382
|
+
"workspaceId": "{workspaceId}",
|
|
383
|
+
"profileSlug": "task", // from /profiles — never guess
|
|
384
|
+
"title": "Weekly team sync",
|
|
385
|
+
"properties": { "status": "todo", "projectId": "ent_..." }
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Update an entity
|
|
390
|
+
|
|
391
|
+
```json
|
|
392
|
+
PATCH /api/hub/entities/{entityId}
|
|
393
|
+
{ "title": "…", "properties": { "status": "done" } }
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Create a document (markdown, attached to an entity)
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
POST /api/hub/documents
|
|
400
|
+
{
|
|
401
|
+
"userId": "{userId}",
|
|
402
|
+
"workspaceId": "{workspaceId}",
|
|
403
|
+
"title": "Meeting notes — 2026-04-20",
|
|
404
|
+
"content": "# Attendees\n- …\n\n# Decisions\n- …",
|
|
405
|
+
"entityId": "ent_event_..." // attach to an entity for context
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The reverse lookup is `entities WHERE documentId = ?`. Always attach the document to a meaningful entity (the meeting event, the project, the person) — a floating document is another orphan.
|
|
410
|
+
|
|
411
|
+
### Store a fact (memory) — use sparingly
|
|
412
|
+
|
|
413
|
+
```json
|
|
414
|
+
POST /api/hub/memory
|
|
415
|
+
{ "userId": "{userId}", "fact": "User prefers async communication over meetings" }
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Always auto-approved. **Memory is for loose, unstructured, hard-to-title facts only.** The seductive thing about memory is it has zero friction — no dedup, no linking, no proposals. That makes it easy to misuse.
|
|
419
|
+
|
|
420
|
+
**The test:** if the user later asked "show me all X," can memory answer? Memory can only keyword-match — it has no structure. So:
|
|
421
|
+
|
|
422
|
+
| Input | Use |
|
|
423
|
+
| ------------------------------------------------------- | --------------------------------------------------------------- |
|
|
424
|
+
| "User prefers async communication" | memory — it's a preference |
|
|
425
|
+
| "Garage code is 4321" | memory — throwaway fact |
|
|
426
|
+
| "Should we use LangGraph or CrewAI for Eve?" | **entity `question`** — substantive inquiry, start of flow |
|
|
427
|
+
| "Here's what I found comparing LangGraph and CrewAI…" | **entity `research`** — investigation with sources + conclusion |
|
|
428
|
+
| "We decided to use LangGraph over OpenClaude's native…" | **entity `decision`** — has title, rationale, project |
|
|
429
|
+
| "Key insight: tasks need better retry logic" | **entity `note` with tag "insight"** + link to project |
|
|
430
|
+
| "John is now head of engineering at Acme" | **update `contact` entity** — that's a property change |
|
|
431
|
+
| "Launch date moved to May 15" | **update `project` entity** — change the startDate |
|
|
432
|
+
| "Action item from meeting: ship MVP by Friday" | **entity `task`** linked to the `event` (meeting) |
|
|
433
|
+
| "Agreed with Sarah: we'll split backend & frontend" | **entity `decision`** linked to Sarah + the project |
|
|
434
|
+
|
|
435
|
+
**Rule of thumb:** if it has a title-worthy noun OR context to link to (a project, a person, a meeting) OR a lifecycle (status/supersession) — it's an entity, not memory. Memory is the fallback, not the default.
|
|
436
|
+
|
|
437
|
+
**For decisions specifically** — use the `decision` system profile:
|
|
438
|
+
|
|
439
|
+
```json
|
|
440
|
+
POST /api/hub/entities
|
|
441
|
+
{
|
|
442
|
+
"userId": "{userId}",
|
|
443
|
+
"profileSlug": "decision",
|
|
444
|
+
"title": "Use LangGraph orchestrator over OpenClaude native",
|
|
445
|
+
"properties": {
|
|
446
|
+
"decisionStatus": "accepted",
|
|
447
|
+
"decidedAt": "2026-04-20",
|
|
448
|
+
"summary": "Dedicated orchestrator service; OpenClaude CLI as UX",
|
|
449
|
+
"rationale": "Separates the Orchestration Brain (LangGraph) from the UX (OpenClaude CLI).",
|
|
450
|
+
"alternatives": "Standardize entirely on OpenClaude's multi-agent logic.",
|
|
451
|
+
"projectId": "ent_project_eve"
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
This creates a first-class decision entity linked to Project Eve. It shows up in traversals, can be superseded later (`supersededBy: newDecisionId`), and survives governance. Memory can't do any of that.
|
|
457
|
+
|
|
458
|
+
### Post to the user's personal channel
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
GET /api/hub/channels/personal?userId={userId}&workspaceId={workspaceId}
|
|
462
|
+
→ { id, name, … } (get-or-create, needs hub-protocol.write scope)
|
|
463
|
+
|
|
464
|
+
POST /api/hub/threads/{threadId}/messages
|
|
465
|
+
{ "userId": "{userId}", "role": "user", "content": "…" }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Reading
|
|
469
|
+
|
|
470
|
+
Graph-based, not semantic. Type filter → relations → neighborhood.
|
|
471
|
+
|
|
472
|
+
```
|
|
473
|
+
# Keyword search across everything (entities, documents, views, threads)
|
|
474
|
+
GET /api/hub/search?query={query}&userId={SYNAP_USER_ID}&workspaceId={id}
|
|
475
|
+
|
|
476
|
+
# Entities of a specific type (q= is the param for entities endpoint)
|
|
477
|
+
GET /api/hub/entities?q={query}&profileSlug={slug}&workspaceId={id}
|
|
478
|
+
|
|
479
|
+
# Recent entities
|
|
480
|
+
GET /api/hub/entities?sort=updatedAt:desc&limit=20&workspaceId={id}
|
|
481
|
+
|
|
482
|
+
# The full connected neighborhood of an entity (prefer this)
|
|
483
|
+
GET /api/hub/entities/{id}/connections?userId={userId}&workspaceId={id}
|
|
484
|
+
→ { connections: [{ entityId, entity, label, direction,
|
|
485
|
+
source: "graph"|"property"|"thread" }],
|
|
486
|
+
counts: { total, graph, structural, threads } }
|
|
487
|
+
|
|
488
|
+
# BFS traversal (expensive at depth 3+)
|
|
489
|
+
GET /api/hub/graph/traverse?entityId={id}&maxDepth=2&workspaceId={id}
|
|
490
|
+
|
|
491
|
+
# Memory facts (keyword)
|
|
492
|
+
GET /api/hub/memory?userId={userId}&query={keywords}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
No SQL joins. The graph is the join.
|
|
496
|
+
|
|
497
|
+
## Multi-entity capture from free-form text
|
|
498
|
+
|
|
499
|
+
When the user pastes a block of unstructured content (a meeting transcript, an email, a LinkedIn bio), use the capture pipeline instead of chaining manual creates:
|
|
500
|
+
|
|
501
|
+
```
|
|
502
|
+
POST /api/hub/capture/structure → returns proposals + relations
|
|
503
|
+
POST /api/hub/capture/execute → commits (after user confirms)
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The pipeline extracts multiple entities with their relations in one LLM call. Read **`capture.md`** for the full flow.
|
|
507
|
+
|
|
508
|
+
## Worked examples
|
|
509
|
+
|
|
510
|
+
### Example 1 — "Remind me to send the proposal to Acme on Friday"
|
|
511
|
+
|
|
512
|
+
1. Search for the Acme entity: `GET /entities?q=Acme&profileSlug=company` → got `ent_acme`
|
|
513
|
+
2. Search for an existing task: `GET /entities?q=proposal&profileSlug=task&workspaceId=…` → none
|
|
514
|
+
3. Create the task with links:
|
|
515
|
+
|
|
516
|
+
```json
|
|
517
|
+
POST /api/hub/entities
|
|
518
|
+
{ "userId": "{userId}", "workspaceId": "{wsId}",
|
|
519
|
+
"profileSlug": "task",
|
|
520
|
+
"title": "Send proposal to Acme",
|
|
521
|
+
"properties": {
|
|
522
|
+
"status": "todo", "priority": "high",
|
|
523
|
+
"dueDate": "2026-04-24"
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
4. Link to Acme (Acme is not an entity_id property on task — use Way 2):
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
POST /api/hub/relations
|
|
532
|
+
{ "userId": "{userId}",
|
|
533
|
+
"sourceEntityId": "ent_new_task",
|
|
534
|
+
"targetEntityId": "ent_acme",
|
|
535
|
+
"type": "related_to" }
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
5. Confirm: "Task created and linked to Acme, due Friday."
|
|
539
|
+
|
|
540
|
+
### Example 2 — "Who's Sarah at Acme?"
|
|
541
|
+
|
|
542
|
+
1. Search person: `GET /entities?q=Sarah&profileSlug=person` → `ent_sarah`
|
|
543
|
+
2. Pull her connections: `GET /entities/ent_sarah/connections` → company=Acme, 3 recent emails, 1 meeting
|
|
544
|
+
3. Answer from the returned data, not from your own context.
|
|
545
|
+
|
|
546
|
+
### Example 3 — "Save this article for later: https://…"
|
|
547
|
+
|
|
548
|
+
1. Search for existing bookmark: `GET /entities?q=<url>&profileSlug=article` → none
|
|
549
|
+
2. Create an article entity:
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
POST /api/hub/entities
|
|
553
|
+
{ "userId": "{userId}", "workspaceId": "{wsId}",
|
|
554
|
+
"profileSlug": "article",
|
|
555
|
+
"title": "<page title>",
|
|
556
|
+
"properties": { "url": "<url>", "domain": "<host>" }
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
3. If the user said why ("interesting for the onboarding project"), also create a relation to that project — never drop the reason as a plain comment, turn it into a link.
|
|
561
|
+
|
|
562
|
+
## CRM Workspaces — 4-Entity Model
|
|
563
|
+
|
|
564
|
+
Some workspaces use a CRM data structure with four entities: `person` and `company` (identity records), `deal` (pipeline record), and `client` (post-win relationship marker). Understand this pattern when proposing lead captures, deal updates, or campaign membership.
|
|
565
|
+
|
|
566
|
+
**The model:**
|
|
567
|
+
|
|
568
|
+
- **`person` + `company`** — Identity only, no sales state. Persist across deals.
|
|
569
|
+
- **`deal`** — Pipeline record with `dealStage` property (lead, contacted, qualifying, proposal, negotiating, won, lost, inactive). Represents what people often call a "lead" (when stage=lead). Linked to person/company via `linked_to_deal` relation.
|
|
570
|
+
- **`client`** — Post-win relationship marker. Created automatically when deal transitions to stage=won. Status: active, paused, or churned. Linked via `is_client` (party → client) and `produced_by_deal` (deal → client).
|
|
571
|
+
- **`journey`** — Documents anchored to a deal (not a person). Linked via `has_journey`.
|
|
572
|
+
|
|
573
|
+
**AI behavior — lead capture:**
|
|
574
|
+
|
|
575
|
+
When the user describes a lead (inbound person, prospect, or company lead), propose the full bundle:
|
|
576
|
+
|
|
577
|
+
1. Create `person` entity (if not exists) with email, role, company name
|
|
578
|
+
2. Create `company` entity (if not exists)
|
|
579
|
+
3. Create `deal` entity with `dealStage: "lead"` and `estimatedValue` (if known)
|
|
580
|
+
4. Create `linked_to_deal` relation connecting person/company to deal
|
|
581
|
+
|
|
582
|
+
Never create a person with a sales-state flag. The deal is the lead container.
|
|
583
|
+
|
|
584
|
+
```json
|
|
585
|
+
POST /api/hub/entities
|
|
586
|
+
{ "userId": "{userId}", "workspaceId": "{wsId}",
|
|
587
|
+
"profileSlug": "person",
|
|
588
|
+
"title": "Alice Johnson",
|
|
589
|
+
"properties": { "email": "alice@acme.com", "role": "VP Engineering" }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
POST /api/hub/entities
|
|
593
|
+
{ "userId": "{userId}", "workspaceId": "{wsId}",
|
|
594
|
+
"profileSlug": "deal",
|
|
595
|
+
"title": "Acme prospect",
|
|
596
|
+
"properties": { "dealStage": "lead", "estimatedValue": 50000 }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
POST /api/hub/relations
|
|
600
|
+
{ "userId": "{userId}",
|
|
601
|
+
"sourceEntityId": "ent_person_alice",
|
|
602
|
+
"targetEntityId": "ent_deal_acme",
|
|
603
|
+
"type": "linked_to_deal"
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**AI behavior — moving deals to won:**
|
|
608
|
+
|
|
609
|
+
When a deal transitions to `dealStage: "won"`, also propose client creation if not yet linked:
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
PATCH /api/hub/entities/ent_deal_acme
|
|
613
|
+
{ "properties": { "dealStage": "won" } }
|
|
614
|
+
|
|
615
|
+
POST /api/hub/entities
|
|
616
|
+
{ "userId": "{userId}", "workspaceId": "{wsId}",
|
|
617
|
+
"profileSlug": "client",
|
|
618
|
+
"title": "Acme (active)",
|
|
619
|
+
"properties": { "clientStatus": "active" }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
POST /api/hub/relations
|
|
623
|
+
{ "userId": "{userId}",
|
|
624
|
+
"sourceEntityId": "ent_person_alice",
|
|
625
|
+
"targetEntityId": "ent_client_acme",
|
|
626
|
+
"type": "is_client"
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
POST /api/hub/relations
|
|
630
|
+
{ "userId": "{userId}",
|
|
631
|
+
"sourceEntityId": "ent_deal_acme",
|
|
632
|
+
"targetEntityId": "ent_client_acme",
|
|
633
|
+
"type": "produced_by_deal"
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**AI behavior — campaign membership:**
|
|
638
|
+
|
|
639
|
+
When the user describes campaign members (segment for outreach, tracking, or automation), use polymorphic `member_of` relations. Members can be persons, companies, or deals:
|
|
640
|
+
|
|
641
|
+
```json
|
|
642
|
+
POST /api/hub/relations
|
|
643
|
+
{ "userId": "{userId}",
|
|
644
|
+
"sourceEntityId": "ent_person_alice",
|
|
645
|
+
"targetEntityId": "ent_campaign_enterprise",
|
|
646
|
+
"type": "member_of"
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Same relation type, different entity type
|
|
650
|
+
POST /api/hub/relations
|
|
651
|
+
{ "userId": "{userId}",
|
|
652
|
+
"sourceEntityId": "ent_deal_acme",
|
|
653
|
+
"targetEntityId": "ent_campaign_enterprise",
|
|
654
|
+
"type": "member_of"
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
**Property names:**
|
|
659
|
+
|
|
660
|
+
- `dealStage` (not `crmStatus` or `status`) — values: lead, contacted, qualifying, proposal, negotiating, won, lost, inactive
|
|
661
|
+
- `clientStatus` (post-win only) — values: active, paused, churned
|
|
662
|
+
- Identities (person, company) carry no sales state
|
|
663
|
+
|
|
664
|
+
**Why separate identity from state:**
|
|
665
|
+
|
|
666
|
+
This model enables renewals (new deal linking to existing client), multi-stakeholder deals (multiple `linked_to_deal` relations per deal), campaigns with mixed entity types (persons + companies + deals as members), and clean churn tracking. It matches Synap's core pattern: entities + relations = graph.
|
|
667
|
+
|
|
668
|
+
## Common mistakes
|
|
669
|
+
|
|
670
|
+
1. **Creating orphan entities.** Always connect to at least one other entity on creation. Search first; if nothing links, reconsider whether this should be memory.
|
|
671
|
+
2. **Guessing profile slugs.** Always `GET /profiles` first. `deal`, `capture`, and custom profiles may not exist in this workspace.
|
|
672
|
+
3. **Using the deprecated `type` field.** Always `profileSlug`.
|
|
673
|
+
4. **Treating `"proposed"` as an error.** It's a governance queue.
|
|
674
|
+
5. **Forcing `source` to bypass governance.** Governance is determined by the agent user + whitelist, not by `source`. Don't set it.
|
|
675
|
+
6. **Not knowing your userId.** Use `{SYNAP_USER_ID}` from the env (set by `synap connect`). Or call `GET /api/hub/users/me` → `.id` once and cache it. Never hardcode or guess.
|
|
676
|
+
7. **Skipping the search step.** Duplicates degrade the graph more than missing data.
|
|
677
|
+
8. **Forgetting that `GET /channels/personal` needs `hub-protocol.write`** scope — it's get-or-create, not a pure read.
|
|
678
|
+
|
|
679
|
+
## AI Inline Patterns — reference entities in your replies
|
|
680
|
+
|
|
681
|
+
When the user is interacting with Synap's AI Companion (the in-browser chat panel), you can embed **inline chips** directly in your reply text. These render as clickable buttons the user can tap to open entities, views, or documents without leaving the conversation.
|
|
682
|
+
|
|
683
|
+
### Syntax
|
|
684
|
+
|
|
685
|
+
| Pattern | Renders as | Effect |
|
|
686
|
+
| ---------------------------- | --------------------------- | --------------------------------- |
|
|
687
|
+
| `[[entity:UUID\|Name]]` | Purple entity chip | Opens entity detail in side panel |
|
|
688
|
+
| `[[view:UUID\|Name]]` | Blue view chip | Opens view |
|
|
689
|
+
| `[[open:side\|view:UUID]]` | Amber "Open in side" button | Opens view in side panel |
|
|
690
|
+
| `[[open:main\|view:UUID]]` | Amber "Open" button | Opens view in main panel |
|
|
691
|
+
| `[[open:side\|entity:UUID]]` | Amber "Open in side" button | Opens entity in side panel |
|
|
692
|
+
| `[[run:UUID\|Label]]` | Green "Run" button | Navigates to automation entity |
|
|
693
|
+
| `[[doc:UUID\|Name]]` | Gray doc chip | Opens document |
|
|
694
|
+
|
|
695
|
+
### Rules
|
|
696
|
+
|
|
697
|
+
- **Always use real IDs.** Never hallucinate UUIDs. Only emit patterns for entities/views you just created or retrieved via Hub Protocol.
|
|
698
|
+
- **Emit after creation.** When you create a view or entity, immediately reference it: `"Created your pipeline → [[view:abc123|Active Tasks]]"`
|
|
699
|
+
- **Prefer side panel.** Use `[[open:side|view:UUID]]` so the user keeps their current context.
|
|
700
|
+
- **Only in Companion replies.** These patterns are silently ignored in non-companion channels, documents, and memory. Do not use them there.
|
|
701
|
+
- **Combine with prose.** Don't lead with a chip — embed it naturally: `"Here are your open deals → [[view:xyz|Deals Pipeline]] · [[open:side|view:xyz]]"`
|
|
702
|
+
|
|
703
|
+
## When you need more
|
|
704
|
+
|
|
705
|
+
- Linking conventions, auto-sync table, relation types → **`linking.md`**
|
|
706
|
+
- Full governance whitelist, proposal lifecycle, agent users → **`governance.md`**
|
|
707
|
+
- Unstructured capture pipeline → **`capture.md`**
|
|
708
|
+
- Extending the data model (new profiles, new properties) → install the **`synap-schema`** skill
|
|
709
|
+
- Building views, dashboards, and bento layouts → install the **`synap-ui`** skill
|
|
710
|
+
|
|
711
|
+
## ViewFrame Cells — Custom View Generation
|
|
712
|
+
|
|
713
|
+
ViewFrame is the standard way to create custom data visualizations in Synap. Use it whenever an existing cell (table, kanban, list, chart) does not cover the needed chart type, 3D layout, map, or bespoke AI-generated UI.
|
|
714
|
+
|
|
715
|
+
### When to Use ViewFrame
|
|
716
|
+
|
|
717
|
+
| Situation | Action |
|
|
718
|
+
| ---------------------------------------------------------------------- | ----------------------------- |
|
|
719
|
+
| An existing cell or view type covers the need | Use the existing cell or view |
|
|
720
|
+
| User asks for a specific chart type, map, 3D scene, or custom layout | Generate a ViewFrame widget |
|
|
721
|
+
| User says "show X as a [funnel / heatmap / treemap / scatter / globe]" | Generate a ViewFrame widget |
|
|
722
|
+
|
|
723
|
+
### What ViewFrame Is
|
|
724
|
+
|
|
725
|
+
- A sandboxed iframe that renders any ES module (React component or plain JS module)
|
|
726
|
+
- Dependencies resolved at runtime via **esm.sh import maps** — no build step
|
|
727
|
+
- The host injects a `SynapWidget` bridge for data access (same postMessage protocol as iframe widgets)
|
|
728
|
+
- Security: `sandbox="allow-scripts allow-modals allow-popups"`, no `allow-same-origin`, no cookies, no pod token
|
|
729
|
+
|
|
730
|
+
### Register a Widget via the Hub Protocol
|
|
731
|
+
|
|
732
|
+
List installed widgets:
|
|
733
|
+
|
|
734
|
+
```
|
|
735
|
+
GET /api/hub/widget-definitions?workspaceId={workspaceId}
|
|
736
|
+
Authorization: Bearer {SYNAP_HUB_API_KEY}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Install a new ViewFrame widget (creates a proposal for user review):
|
|
740
|
+
|
|
741
|
+
```
|
|
742
|
+
POST /api/hub/widget-definitions
|
|
743
|
+
Authorization: Bearer {SYNAP_HUB_API_KEY}
|
|
744
|
+
Content-Type: application/json
|
|
745
|
+
|
|
746
|
+
{
|
|
747
|
+
"userId": "{SYNAP_USER_ID}",
|
|
748
|
+
"workspaceId": "{workspaceId}",
|
|
749
|
+
"typeKey": "deal-stage-funnel",
|
|
750
|
+
"name": "Deal Stage Funnel",
|
|
751
|
+
"description": "Funnel chart of deal pipeline stages",
|
|
752
|
+
"rendererType": "iframe",
|
|
753
|
+
"rendererSource": "<full HTML document — see template below>",
|
|
754
|
+
"defaultSize": { "w": 8, "h": 6 },
|
|
755
|
+
"category": "visualization"
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
`typeKey` must be kebab-case matching `/^[a-z][a-z0-9-]+$/`. Use a descriptive name specific to the widget's content.
|
|
760
|
+
|
|
761
|
+
The response carries `status: "ok"` (installed immediately) or `status: "proposed"` (queued for user review — surface `reviewUrl` to the user).
|
|
762
|
+
|
|
763
|
+
### The SynapWidget Bridge (inside the iframe)
|
|
764
|
+
|
|
765
|
+
`window.SynapWidget` is injected automatically — do NOT import or `<script>` it.
|
|
766
|
+
|
|
767
|
+
```js
|
|
768
|
+
SynapWidget.onInit(async ({ config, context }) => {
|
|
769
|
+
// context: { workspaceId, viewId?, entityId?, sdkVersion }
|
|
770
|
+
const items = await SynapWidget.query("list_entities", {
|
|
771
|
+
profileSlug: "deal", // or 'task', 'person', 'company', any custom slug
|
|
772
|
+
limit: 200,
|
|
773
|
+
});
|
|
774
|
+
render(items ?? []);
|
|
775
|
+
SynapWidget.resize(document.body.scrollHeight);
|
|
776
|
+
});
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
All `query()` calls return a Promise. Entity shape: `{ id, title, profileSlug, properties, createdAt, … }`.
|
|
780
|
+
|
|
781
|
+
Navigation and notifications:
|
|
782
|
+
|
|
783
|
+
```js
|
|
784
|
+
SynapWidget.navigate({ entityId: "entity-uuid" }); // opens entity detail
|
|
785
|
+
SynapWidget.toast("Done!", "success"); // 'success' | 'error' | 'info'
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Common Dependency Patterns (esm.sh import map)
|
|
789
|
+
|
|
790
|
+
```html
|
|
791
|
+
<script type="importmap">
|
|
792
|
+
{
|
|
793
|
+
"imports": {
|
|
794
|
+
"react": "https://esm.sh/react@19",
|
|
795
|
+
"react-dom/client": "https://esm.sh/react-dom@19/client",
|
|
796
|
+
"react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime",
|
|
797
|
+
"recharts": "https://esm.sh/recharts@2.12.0"
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
</script>
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Common library choices:
|
|
804
|
+
|
|
805
|
+
| Category | Packages |
|
|
806
|
+
| -------- | -------------------------------------------------------------- |
|
|
807
|
+
| Data viz | `recharts@2.12.0`, `d3@7`, `chart.js@4`, `observable-plot@0.6` |
|
|
808
|
+
| Tables | `@tanstack/react-table@8` |
|
|
809
|
+
| 3D | `three@0.165.0`, `@react-three/fiber@8`, `@react-three/drei@9` |
|
|
810
|
+
| Maps | `leaflet@1.9.4`, `react-leaflet@4` |
|
|
811
|
+
|
|
812
|
+
### Minimal Widget Template
|
|
813
|
+
|
|
814
|
+
```html
|
|
815
|
+
<!DOCTYPE html>
|
|
816
|
+
<html>
|
|
817
|
+
<head>
|
|
818
|
+
<meta charset="utf-8" />
|
|
819
|
+
<style>
|
|
820
|
+
* {
|
|
821
|
+
box-sizing: border-box;
|
|
822
|
+
margin: 0;
|
|
823
|
+
}
|
|
824
|
+
body {
|
|
825
|
+
font-family: -apple-system, sans-serif;
|
|
826
|
+
padding: 16px;
|
|
827
|
+
background: transparent;
|
|
828
|
+
}
|
|
829
|
+
</style>
|
|
830
|
+
<script type="importmap">
|
|
831
|
+
{
|
|
832
|
+
"imports": {
|
|
833
|
+
"react": "https://esm.sh/react@19",
|
|
834
|
+
"react-dom/client": "https://esm.sh/react-dom@19/client",
|
|
835
|
+
"react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime"
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
</script>
|
|
839
|
+
</head>
|
|
840
|
+
<body>
|
|
841
|
+
<div id="root"></div>
|
|
842
|
+
<script type="module">
|
|
843
|
+
import { createRoot } from "react-dom/client";
|
|
844
|
+
|
|
845
|
+
SynapWidget.onInit(async ({ context }) => {
|
|
846
|
+
const items = await SynapWidget.query("list_entities", {
|
|
847
|
+
profileSlug: "deal",
|
|
848
|
+
limit: 200,
|
|
849
|
+
}).catch(() => []);
|
|
850
|
+
|
|
851
|
+
// Build your UI here. Plain DOM or React both work.
|
|
852
|
+
document.getElementById("root").textContent =
|
|
853
|
+
`Loaded ${(items ?? []).length} deals`;
|
|
854
|
+
|
|
855
|
+
SynapWidget.resize(document.body.scrollHeight);
|
|
856
|
+
});
|
|
857
|
+
</script>
|
|
858
|
+
</body>
|
|
859
|
+
</html>
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Rules
|
|
863
|
+
|
|
864
|
+
- **Always call `SynapWidget.onInit()`** — the host will not send data until you register this handler.
|
|
865
|
+
- **Call `SynapWidget.resize()`** after rendering to prevent clipping.
|
|
866
|
+
- **Handle errors** — `query()` can fail; always `.catch()`.
|
|
867
|
+
- **Transparent background** — `background: transparent` on `body` inherits the host surface color.
|
|
868
|
+
- **No external fetch** — the sandbox has no cross-origin access; all data must go through `SynapWidget`.
|
|
869
|
+
- **Inline all styles** — no external CSS imports; CDN JS via import map is fine.
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## Authentication
|
|
874
|
+
|
|
875
|
+
```
|
|
876
|
+
Authorization: Bearer {SYNAP_HUB_API_KEY}
|
|
877
|
+
X-Workspace-Id: {workspaceId} (optional; also pass in body/query)
|
|
878
|
+
|
|
879
|
+
Scopes:
|
|
880
|
+
hub-protocol.read → most GET endpoints
|
|
881
|
+
hub-protocol.write → all writes AND GET /channels/personal
|
|
882
|
+
```
|