@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.
Files changed (74) hide show
  1. package/README.md +26 -0
  2. package/dist/commands/agents.d.ts +31 -0
  3. package/dist/commands/agents.js +478 -0
  4. package/dist/commands/agents.js.map +1 -0
  5. package/dist/commands/connect.d.ts +18 -1
  6. package/dist/commands/connect.js +154 -74
  7. package/dist/commands/connect.js.map +1 -1
  8. package/dist/commands/connections.d.ts +9 -0
  9. package/dist/commands/connections.js +161 -0
  10. package/dist/commands/connections.js.map +1 -0
  11. package/dist/commands/data.d.ts +43 -0
  12. package/dist/commands/data.js +387 -0
  13. package/dist/commands/data.js.map +1 -0
  14. package/dist/commands/finish.js +41 -8
  15. package/dist/commands/finish.js.map +1 -1
  16. package/dist/commands/infra.d.ts +21 -0
  17. package/dist/commands/infra.js +262 -0
  18. package/dist/commands/infra.js.map +1 -0
  19. package/dist/commands/init.js +188 -10
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/knowledge.d.ts +36 -0
  22. package/dist/commands/knowledge.js +123 -0
  23. package/dist/commands/knowledge.js.map +1 -0
  24. package/dist/commands/openclaw.d.ts +2 -0
  25. package/dist/commands/openclaw.js +300 -23
  26. package/dist/commands/openclaw.js.map +1 -1
  27. package/dist/commands/pods.d.ts +17 -0
  28. package/dist/commands/pods.js +371 -0
  29. package/dist/commands/pods.js.map +1 -0
  30. package/dist/commands/status.d.ts +14 -1
  31. package/dist/commands/status.js +78 -220
  32. package/dist/commands/status.js.map +1 -1
  33. package/dist/commands/update.d.ts +11 -2
  34. package/dist/commands/update.js +116 -5
  35. package/dist/commands/update.js.map +1 -1
  36. package/dist/index.js +370 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/agents-config.d.ts +20 -0
  39. package/dist/lib/agents-config.js +45 -0
  40. package/dist/lib/agents-config.js.map +1 -0
  41. package/dist/lib/auth.d.ts +4 -0
  42. package/dist/lib/auth.js +4 -0
  43. package/dist/lib/auth.js.map +1 -1
  44. package/dist/lib/browser-auth.d.ts +35 -0
  45. package/dist/lib/browser-auth.js +170 -0
  46. package/dist/lib/browser-auth.js.map +1 -0
  47. package/dist/lib/hub-client.d.ts +17 -0
  48. package/dist/lib/hub-client.js +115 -0
  49. package/dist/lib/hub-client.js.map +1 -0
  50. package/dist/lib/openclaw.js +30 -19
  51. package/dist/lib/openclaw.js.map +1 -1
  52. package/dist/lib/pod.d.ts +32 -1
  53. package/dist/lib/pod.js +121 -9
  54. package/dist/lib/pod.js.map +1 -1
  55. package/dist/lib/skills-installer.d.ts +18 -0
  56. package/dist/lib/skills-installer.js +97 -0
  57. package/dist/lib/skills-installer.js.map +1 -0
  58. package/dist/lib/targets.d.ts +65 -0
  59. package/dist/lib/targets.js +673 -0
  60. package/dist/lib/targets.js.map +1 -0
  61. package/package.json +5 -3
  62. package/skills/README.md +91 -0
  63. package/skills/synap/README.md +76 -0
  64. package/skills/synap/SKILL.md +882 -0
  65. package/skills/synap/capture.md +170 -0
  66. package/skills/synap/governance.md +206 -0
  67. package/skills/synap/linking.md +128 -0
  68. package/skills/synap/scripts/orient.sh +28 -0
  69. package/skills/synap-schema/SKILL.md +231 -0
  70. package/skills/synap-schema/property-types.md +228 -0
  71. package/skills/synap-ui/SKILL.md +295 -0
  72. package/skills/synap-ui/bento-recipes.md +608 -0
  73. package/skills/synap-ui/view-types.md +259 -0
  74. 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.