@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,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: synap-schema
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill when the user needs to extend Synap's data model — defining a
|
|
5
|
+
new type of thing (entity profile) or adding/modifying fields (property defs)
|
|
6
|
+
on existing types. Triggers: "I need a new type for podcasts/recipes/workouts/
|
|
7
|
+
clients/books/plants/investments", "add a priority field to tasks", "create a
|
|
8
|
+
property for tracking budget", "I want to track Y and it's not in my current
|
|
9
|
+
schema", defining custom relations, setting up a profile for a specific domain
|
|
10
|
+
(real estate, music production, research, fitness, cooking). Also triggers on:
|
|
11
|
+
"do I already have a profile for X?", "what fields does <profile> have?",
|
|
12
|
+
extending an existing profile with workspace-specific overlay properties.
|
|
13
|
+
Do NOT use this skill for creating instances of existing types — the core
|
|
14
|
+
`synap` skill handles that. This skill changes the SCHEMA, not the data.
|
|
15
|
+
metadata:
|
|
16
|
+
openclaw:
|
|
17
|
+
requires:
|
|
18
|
+
env: [SYNAP_HUB_API_KEY, SYNAP_POD_URL]
|
|
19
|
+
primaryEnv: SYNAP_HUB_API_KEY
|
|
20
|
+
homepage: https://synap.live
|
|
21
|
+
capabilities: [schema, profiles, properties]
|
|
22
|
+
os: [macos, linux, windows]
|
|
23
|
+
userInvocable: false
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Synap — schema extension
|
|
27
|
+
|
|
28
|
+
You extend a user's Synap pod schema: new entity profiles, new properties on existing profiles, overlay properties scoped to one workspace. The core rule: **reuse before extending.** Creating duplicate profiles fractures the graph.
|
|
29
|
+
|
|
30
|
+
## Before you touch anything
|
|
31
|
+
|
|
32
|
+
Always inventory first. Never assume the schema is empty.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
GET /api/hub/profiles?userId={userId}&workspaceId={workspaceId}
|
|
36
|
+
→ [{ slug, displayName, entityScope, parentProfileSlug,
|
|
37
|
+
properties: [{ slug, valueType, constraints, uiHints, targetProfileSlug? }] }]
|
|
38
|
+
|
|
39
|
+
GET /api/hub/property-defs?userId={userId}&workspaceId={workspaceId}&profileId={profileId}
|
|
40
|
+
→ [{ slug, valueType, constraints, uiHints, workspaceId /* null=base, uuid=overlay */ }]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `synap` skill's `scripts/orient.sh` already fetches profiles — reuse its output.
|
|
44
|
+
|
|
45
|
+
## Discover profiles — never assume
|
|
46
|
+
|
|
47
|
+
**Always call `GET /api/hub/profiles?workspaceId={workspaceId}` first.** Profiles are dynamic — every pod and workspace has a different set depending on what was seeded and what the user created. Never assume a profile slug exists without verifying.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
GET /api/hub/profiles?workspaceId={workspaceId}
|
|
51
|
+
→ [{ slug, displayName, entityScope, parentSlug,
|
|
52
|
+
properties: [{ slug, valueType, required, ... }] }]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Read the response:
|
|
56
|
+
|
|
57
|
+
- `entityScope: "pod"` — visible across all workspaces
|
|
58
|
+
- `entityScope: "workspace"` — scoped to this workspace only (custom types, CRM profiles, template-seeded types)
|
|
59
|
+
- `parentSlug` — inheritance chain (e.g. `contact` extends `person`)
|
|
60
|
+
|
|
61
|
+
### Commonly seeded profiles (verify before using)
|
|
62
|
+
|
|
63
|
+
Standard pods typically include: `note`, `task`, `project`, `event`, `person`, `contact`, `company`, `bookmark`, `website`, `article`, `capture`, `file`, `anchor`, `decision`, `question`, `research`
|
|
64
|
+
|
|
65
|
+
CRM workspaces additionally have: `deal`, `client` — but **only** when a CRM workspace was created.
|
|
66
|
+
Custom workspace templates (devplane, content, etc.) add their own profiles entirely.
|
|
67
|
+
|
|
68
|
+
Before creating a new profile: **does one of these already fit?** A podcast episode is arguably an `article`. A meeting is an `event`. A book to read is an `article` or `bookmark`. Err on the side of reuse + extension, not creation.
|
|
69
|
+
|
|
70
|
+
## When to extend an existing profile vs. create a new one
|
|
71
|
+
|
|
72
|
+
Extend when:
|
|
73
|
+
|
|
74
|
+
- The thing fits an existing category (a "client" is a kind of `contact` — consider inheritance)
|
|
75
|
+
- The user needs 1–3 extra fields on an existing type
|
|
76
|
+
- The new fields are workspace-specific (use an overlay, see below)
|
|
77
|
+
|
|
78
|
+
Create new when:
|
|
79
|
+
|
|
80
|
+
- None of the system profiles fits
|
|
81
|
+
- The thing has a clearly distinct set of properties (10+ fields)
|
|
82
|
+
- The domain model genuinely calls for a new first-class type (recipe, workout, podcast episode, investment, plant, medication, vehicle, property listing…)
|
|
83
|
+
|
|
84
|
+
Prefer **inheritance** over new profiles when a system parent fits. `contact extends person` is the pattern — a new `client` profile can `parentProfileSlug: "contact"` and only add the fields that differ.
|
|
85
|
+
|
|
86
|
+
## Creating a new profile
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
POST /api/hub/profiles
|
|
90
|
+
{
|
|
91
|
+
"userId": "{userId}",
|
|
92
|
+
"workspaceId": "{workspaceId}",
|
|
93
|
+
"slug": "podcast_episode", // snake_case, unique
|
|
94
|
+
"displayName": "Podcast Episode",
|
|
95
|
+
"description": "An episode of a podcast the user listens to",
|
|
96
|
+
"parentProfileId": "<id of article or null>", // optional, enables inheritance
|
|
97
|
+
"defaultValues": { "status": "queued" },
|
|
98
|
+
"uiHints": { "icon": "mic", "color": "purple" }
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Then add properties one by one (see below). You do NOT declare properties inline on the profile — they're separate rows.
|
|
103
|
+
|
|
104
|
+
## Creating properties
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
POST /api/hub/property-defs
|
|
108
|
+
{
|
|
109
|
+
"userId": "{userId}",
|
|
110
|
+
"workspaceId": "{workspaceId}",
|
|
111
|
+
"profileId": "{profileId}", // the profile this property belongs to
|
|
112
|
+
"slug": "durationMinutes",
|
|
113
|
+
"valueType": "number",
|
|
114
|
+
"constraints": { "min": 0, "max": 600 },
|
|
115
|
+
"uiHints": { "format": "compact", "displayName": "Duration" }
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Value types: `string`, `number`, `boolean`, `date`, `entity_id`, `array`, `object`, `secret`. Full reference in **`property-types.md`**.
|
|
120
|
+
|
|
121
|
+
For linking properties, always use `entity_id` with `targetProfileSlug` in constraints:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"slug": "hostId",
|
|
126
|
+
"valueType": "entity_id",
|
|
127
|
+
"constraints": { "targetProfileSlug": "person" },
|
|
128
|
+
"uiHints": { "displayName": "Host" }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This enables auto-sync (see `../synap/linking.md`) — the property becomes a typed link that shows up in graph traversals automatically.
|
|
133
|
+
|
|
134
|
+
## Workspace overlay properties
|
|
135
|
+
|
|
136
|
+
New concept (2026-04-10+). A single profile (say `task`) can have **different fields in different workspaces** without copying the profile. Set `overlay: true` on `POST /property-defs`:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
POST /api/hub/property-defs
|
|
140
|
+
{
|
|
141
|
+
"userId": "{userId}",
|
|
142
|
+
"workspaceId": "{workspaceId}",
|
|
143
|
+
"profileId": "{taskProfileId}",
|
|
144
|
+
"slug": "billableHours",
|
|
145
|
+
"valueType": "number",
|
|
146
|
+
"overlay": true // this property only appears in this workspace
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use cases:
|
|
151
|
+
|
|
152
|
+
- "My consulting workspace needs `billableHours` on tasks; my personal workspace doesn't."
|
|
153
|
+
- "Our sales workspace wants a `dealSize` on contacts; engineering doesn't."
|
|
154
|
+
|
|
155
|
+
Overlays don't leak: workspace A can't see workspace B's overlay properties even though both share the profile. For the full scope model, read **`property-types.md`** §Overlay.
|
|
156
|
+
|
|
157
|
+
## Entity scope (pod-wide vs. workspace-scoped)
|
|
158
|
+
|
|
159
|
+
Profiles have an `entityScope` that determines where entities of that type live:
|
|
160
|
+
|
|
161
|
+
- `entityScope: "pod"` — entities are pod-wide, visible in every workspace the user can access. Good for people, companies, notes, articles — things that cross contexts.
|
|
162
|
+
- `entityScope: "workspace"` — entities live in the workspace they were created in. Good for deals, files, workspace-specific artifacts.
|
|
163
|
+
|
|
164
|
+
Defaults to `workspace` if not set on the profile. The user can toggle this per profile in ProfileEditor Settings. If you're creating a profile for something clearly pod-wide (a person, a podcast the user follows, a book in their library), set `entityScope: "pod"` explicitly.
|
|
165
|
+
|
|
166
|
+
## Custom relations
|
|
167
|
+
|
|
168
|
+
If the user wants a typed edge that isn't in the convention list (see `../synap/linking.md`), you can define it:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
POST /api/hub/relation-defs
|
|
172
|
+
{
|
|
173
|
+
"userId": "{userId}",
|
|
174
|
+
"workspaceId": "{workspaceId}",
|
|
175
|
+
"slug": "mentored_by",
|
|
176
|
+
"displayName": "Mentored by",
|
|
177
|
+
"sourceProfileSlug": "person",
|
|
178
|
+
"targetProfileSlug": "person",
|
|
179
|
+
"bidirectional": false
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Defining a relation def is rarely worth it — `related_to` + a property usually suffices. Only create one when the relationship is semantic enough that UI should treat it specially (e.g., show "mentored by Jane" on a person's profile card).
|
|
184
|
+
|
|
185
|
+
## Worked example — "I want to track podcasts I listen to"
|
|
186
|
+
|
|
187
|
+
1. Inventory. `GET /profiles` → no `podcast` profile, no `podcast_episode`.
|
|
188
|
+
2. Decide scope. Podcasts/episodes are pod-wide (same podcast across workspaces).
|
|
189
|
+
3. Create two profiles (podcast + episode) or one (episode only, with the show as a string property)? Decide based on the user's intent. If they want to group episodes by show → two profiles. If one-level is enough → one.
|
|
190
|
+
4. For two profiles:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
POST /profiles { slug: "podcast", displayName: "Podcast", entityScope: "pod", uiHints: { icon: "radio" } }
|
|
194
|
+
POST /profiles { slug: "podcast_episode", displayName: "Podcast Episode", parentProfileId: <articleId>, entityScope: "pod" }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
5. Add properties on `podcast`:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
POST /property-defs { slug: "host", valueType: "string" }
|
|
201
|
+
POST /property-defs { slug: "rssUrl", valueType: "string", uiHints: { inputType: "url" } }
|
|
202
|
+
POST /property-defs { slug: "category", valueType: "string" }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
6. Add properties on `podcast_episode`:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
POST /property-defs { slug: "podcastId", valueType: "entity_id", constraints: { targetProfileSlug: "podcast" } }
|
|
209
|
+
POST /property-defs { slug: "durationMinutes", valueType: "number" }
|
|
210
|
+
POST /property-defs { slug: "listenedAt", valueType: "date" }
|
|
211
|
+
POST /property-defs { slug: "rating", valueType: "number", constraints: { min: 1, max: 5 } }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
7. Tell the user. "I added `podcast` and `podcast_episode` to your pod with linking between them. You can create your first episode now, or want me to also add a view for it?" (Hand off to the `synap-ui` skill if they say yes.)
|
|
215
|
+
|
|
216
|
+
## Common mistakes
|
|
217
|
+
|
|
218
|
+
1. **Creating a profile that already exists (e.g., `meeting` when `event` fits).** Always inventory first.
|
|
219
|
+
2. **Declaring properties inline on the profile object.** Properties are separate rows; use `POST /property-defs`.
|
|
220
|
+
3. **Using `string` for what should be `entity_id`.** If the field refers to another entity (host of a podcast), use `entity_id` + `targetProfileSlug` — enables auto-sync and link UX.
|
|
221
|
+
4. **Using `array` of strings for tags.** Tags are a built-in concept; reuse the `tags` property on `note`/`project` instead of creating a parallel field.
|
|
222
|
+
5. **Forgetting `entityScope`.** Defaults to `workspace`. If the thing is pod-wide (people, books, podcasts), set it explicitly.
|
|
223
|
+
6. **Creating an overlay when a base property is wanted.** Overlays only appear in one workspace. If the user wants the field everywhere, don't set `overlay: true`.
|
|
224
|
+
7. **Creating a custom profile when extension would work.** `client extends contact` is cleaner than a parallel `client` profile.
|
|
225
|
+
|
|
226
|
+
## When you need more
|
|
227
|
+
|
|
228
|
+
- Full `valueType` reference + constraints + uiHints → **`property-types.md`**
|
|
229
|
+
- Inheritance, overlay scope semantics, pod-wide caveats → **`property-types.md`** §Scope
|
|
230
|
+
- Creating views over the new profile → install the **`synap-ui`** skill
|
|
231
|
+
- Creating instances of the new profile → use the core **`synap`** skill
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Property types — reference
|
|
2
|
+
|
|
3
|
+
Every property on a profile has a `valueType`. This is the authoritative list, what each type stores, and how to configure it.
|
|
4
|
+
|
|
5
|
+
## Value types
|
|
6
|
+
|
|
7
|
+
### `string`
|
|
8
|
+
|
|
9
|
+
Plain text.
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"slug": "email",
|
|
14
|
+
"valueType": "string",
|
|
15
|
+
"constraints": { "maxLength": 200, "pattern": "^.+@.+\\..+$" },
|
|
16
|
+
"uiHints": { "inputType": "email" }
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**uiHints.inputType variants:** `email`, `phone`, `url`, `person`, `richtext`, `select`, `datetime`, `datetime-local`.
|
|
21
|
+
**uiHints.displayAs variants:** `status`, `priority`, `progress`, `person` — used by list/table views for special badges.
|
|
22
|
+
|
|
23
|
+
### `number`
|
|
24
|
+
|
|
25
|
+
Integer or float.
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"slug": "price",
|
|
30
|
+
"valueType": "number",
|
|
31
|
+
"constraints": { "min": 0, "max": 1000000 },
|
|
32
|
+
"uiHints": { "format": "currency", "displayName": "Price" }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**uiHints.format:** `locale`, `currency`, `percent`, `compact`.
|
|
37
|
+
|
|
38
|
+
### `boolean`
|
|
39
|
+
|
|
40
|
+
True/false.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"slug": "isArchived",
|
|
45
|
+
"valueType": "boolean",
|
|
46
|
+
"uiHints": { "displayName": "Archived" }
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `date`
|
|
51
|
+
|
|
52
|
+
ISO 8601 date or datetime.
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"slug": "dueDate",
|
|
57
|
+
"valueType": "date",
|
|
58
|
+
"uiHints": { "includeTime": false, "displayName": "Due" }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Set `includeTime: true` for datetime; false for date-only. Don't store durations as dates — use `number` in seconds/minutes.
|
|
63
|
+
|
|
64
|
+
### `entity_id`
|
|
65
|
+
|
|
66
|
+
**The important one.** A reference to another entity by UUID. Triggers the auto-sync behaviour (see `../synap/linking.md`).
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"slug": "projectId",
|
|
71
|
+
"valueType": "entity_id",
|
|
72
|
+
"constraints": { "targetProfileSlug": "project" },
|
|
73
|
+
"uiHints": { "linkedProfileSlug": "project", "displayName": "Project" }
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Always set `targetProfileSlug`** (or `targetProfileSlugs: [...]` for multi-target). Without it, UI can't render a picker and auto-sync defaults to `related_to`.
|
|
78
|
+
|
|
79
|
+
For linking to users (workspace members), use:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"slug": "assignee",
|
|
84
|
+
"valueType": "entity_id",
|
|
85
|
+
"constraints": { "targetProfileSlug": "person" },
|
|
86
|
+
"uiHints": { "inputType": "person", "linkedTable": "workspace_members" }
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `array`
|
|
91
|
+
|
|
92
|
+
List of primitives OR list of entity references.
|
|
93
|
+
|
|
94
|
+
For tags / free-text lists:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"slug": "tags",
|
|
99
|
+
"valueType": "array",
|
|
100
|
+
"uiHints": { "itemValueType": "string" }
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For multi-entity lists (e.g., event attendees):
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"slug": "attendees",
|
|
109
|
+
"valueType": "array",
|
|
110
|
+
"uiHints": { "itemValueType": "entity_id", "linkedProfileSlug": "person" }
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Arrays of entity IDs **don't auto-sync** — if you want bidirectional linking for each item, create explicit relations.
|
|
115
|
+
|
|
116
|
+
### `object`
|
|
117
|
+
|
|
118
|
+
Arbitrary JSON. Only for data that genuinely has no fixed schema.
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "slug": "metadata", "valueType": "object" }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Avoid when possible. Objects don't index well and can't be filtered in views. Prefer breaking out properties.
|
|
125
|
+
|
|
126
|
+
### `secret`
|
|
127
|
+
|
|
128
|
+
Like `string` but masked in UI, encrypted at rest, never returned in list responses.
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{ "slug": "apiToken", "valueType": "secret" }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Use for credentials stored on an entity (e.g., a connector's OAuth refresh token). Never put actual user data here — use `string`.
|
|
135
|
+
|
|
136
|
+
## Constraints
|
|
137
|
+
|
|
138
|
+
The `constraints` object is value-type specific:
|
|
139
|
+
|
|
140
|
+
| valueType | Supported constraints |
|
|
141
|
+
| ----------- | ------------------------------------------------------------- |
|
|
142
|
+
| `string` | `minLength`, `maxLength`, `pattern` (regex), `enum: string[]` |
|
|
143
|
+
| `number` | `min`, `max`, `integer: true`, `enum: number[]` |
|
|
144
|
+
| `date` | `min` (ISO), `max` (ISO) |
|
|
145
|
+
| `entity_id` | `targetProfileSlug`, `targetProfileSlugs` (multi) |
|
|
146
|
+
| `array` | `minItems`, `maxItems`, `uniqueItems: true` |
|
|
147
|
+
| `object` | (none — prefer breaking into typed properties) |
|
|
148
|
+
|
|
149
|
+
Violations return a 400 from `POST /entities` with `{ error: "validation", field: "slug", issue: "..." }`.
|
|
150
|
+
|
|
151
|
+
## uiHints reference
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
{
|
|
155
|
+
displayName?: string // Label in UI forms and views
|
|
156
|
+
placeholder?: string // Input placeholder
|
|
157
|
+
inputType?: "email" | "phone" | "url" | "person" | "richtext"
|
|
158
|
+
| "datetime" | "datetime-local" | "select"
|
|
159
|
+
displayAs?: "status" | "priority" | "progress" | "person"
|
|
160
|
+
format?: "locale" | "currency" | "percent" | "compact"
|
|
161
|
+
includeTime?: boolean // date only
|
|
162
|
+
linkedProfileSlug?: string // entity_id rendering
|
|
163
|
+
linkedTable?: "workspace_members" | "free_text"
|
|
164
|
+
itemValueType?: "string" | "number" | "boolean" | "date" | "entity_id" | "url"
|
|
165
|
+
pluginHints?: Record<string, unknown> // freeform for custom cells
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
uiHints are cosmetic — they change rendering, not validation. If you need enforcement, use `constraints`.
|
|
170
|
+
|
|
171
|
+
## Scope — three layers
|
|
172
|
+
|
|
173
|
+
Every property def has two scope dimensions:
|
|
174
|
+
|
|
175
|
+
| `profileId` | `workspaceId` | Layer | Who sees it |
|
|
176
|
+
| ----------- | ------------- | ----------------- | -------------------------------------- |
|
|
177
|
+
| null | null | Global | Every workspace, every profile |
|
|
178
|
+
| set | null | Profile-base | Every workspace that uses this profile |
|
|
179
|
+
| set | set | Workspace overlay | Only that workspace |
|
|
180
|
+
|
|
181
|
+
The rendering rule for a given `(profile, workspace)`:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
visible = global ∪ profile-base ∪ this-workspace's overlays on this profile
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Other workspaces' overlays are filtered out at SQL level.
|
|
188
|
+
|
|
189
|
+
### When to use each
|
|
190
|
+
|
|
191
|
+
- **Global** (rare): properties that apply to all entity types (e.g., `createdBy`, `tags`). System only.
|
|
192
|
+
- **Profile-base** (default): the property should appear for everyone using this profile. Creating a property for a workspace-owned profile → profile-base.
|
|
193
|
+
- **Overlay**: the property is specific to one workspace extending a shared profile. Pass `overlay: true` in `POST /property-defs`.
|
|
194
|
+
|
|
195
|
+
If you're extending a pod-wide profile (like `task` or `contact`) with a workspace-specific field, you **must** use overlay — otherwise you'd pollute every workspace.
|
|
196
|
+
|
|
197
|
+
Entities can carry overlay property values even in workspaces that don't see the overlay — the values are preserved but invisible. This is by design.
|
|
198
|
+
|
|
199
|
+
## Inheritance
|
|
200
|
+
|
|
201
|
+
If a profile has `parentProfileSlug`, it inherits the parent's property defs. Own properties override parent properties by slug.
|
|
202
|
+
|
|
203
|
+
Example: `contact` has `parentProfileSlug: "person"`. A contact entity has both `person.email` and `contact.role`. Set `email` on a contact and you satisfy the parent schema.
|
|
204
|
+
|
|
205
|
+
When creating a child profile, don't re-declare the parent's properties. Only add what's new.
|
|
206
|
+
|
|
207
|
+
## Validation semantics
|
|
208
|
+
|
|
209
|
+
On `POST /entities` and `PATCH /entities`:
|
|
210
|
+
|
|
211
|
+
1. Compute effective properties = profile-base ∪ global ∪ workspace overlays.
|
|
212
|
+
2. For each property in the request body, check the def exists and the value passes `valueType` + `constraints`.
|
|
213
|
+
3. Unknown properties (not in any layer) are stored in JSONB but never rendered. Avoid — they rot.
|
|
214
|
+
|
|
215
|
+
Properties not in the request body are not cleared. Use `PATCH` with `{ "properties": { "x": null } }` to explicitly unset.
|
|
216
|
+
|
|
217
|
+
## Performance note
|
|
218
|
+
|
|
219
|
+
Every entity_id property is indexed via `entity_property_index.value_entity_id` for fast reverse-lookup ("find all entities whose `projectId` is X"). This is what makes Way 1 cheap at scale. Inventing a string property with UUIDs inside will work but won't be indexed — use `entity_id`.
|
|
220
|
+
|
|
221
|
+
## Common mistakes
|
|
222
|
+
|
|
223
|
+
- Using `string` for what should be `entity_id` (loses auto-sync + reverse-lookup).
|
|
224
|
+
- Omitting `constraints.targetProfileSlug` on `entity_id` props (breaks pickers, breaks auto-sync routing).
|
|
225
|
+
- Creating an `array` of entity_ids and expecting bidirectional linking (it doesn't — use explicit relations).
|
|
226
|
+
- Using `object` when two `string` properties would suffice.
|
|
227
|
+
- Reusing `tags` as an entity_id list (tags is free-text by convention; make a new `relatedProjects` property instead).
|
|
228
|
+
- Overriding parent profile properties with weaker types (child `email` as `object` when parent is `string`) — creates validation drift.
|