@eventmodelers/cli 1.0.80 → 1.0.84

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 (40) hide show
  1. package/cli.js +2 -2
  2. package/package.json +1 -1
  3. package/shared/skills/connect/SKILL.md +6 -4
  4. package/shared/skills/learn-eventmodelers-api/SKILL.md +24 -21
  5. package/shared/skills/request-feedback/SKILL.md +0 -2
  6. package/shared/skills/update-slice-status/SKILL.md +0 -2
  7. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +2 -2
  8. package/stacks/modeling-kit/templates/.claude/skills/attributes/references/api-fallback.md +1 -3
  9. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +19 -17
  10. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/references/api-fallback.md +0 -2
  11. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +0 -4
  12. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/api-fallback.md +0 -4
  13. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-automation-chains/SKILL.md +9 -8
  14. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-automation-chains/references/api-fallback.md +0 -1
  15. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +9 -9
  16. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/references/api-fallback.md +0 -2
  17. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +10 -9
  18. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/references/api-fallback.md +0 -2
  19. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +14 -13
  20. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/api-fallback.md +4 -4
  21. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +2 -2
  22. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +2 -2
  23. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +11 -11
  24. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/references/api-fallback.md +0 -2
  25. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +1 -3
  26. package/stacks/modeling-kit/templates/.claude/skills/examples/references/api-fallback.md +1 -3
  27. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +2 -2
  28. package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +8 -8
  29. package/stacks/modeling-kit/templates/.claude/skills/html-screen/references/api-fallback.md +0 -1
  30. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +31 -26
  31. package/stacks/modeling-kit/templates/.claude/skills/place-element/references/api-fallback.md +5 -9
  32. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +18 -15
  33. package/stacks/modeling-kit/templates/.claude/skills/storyboard/references/api-fallback.md +0 -1
  34. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +0 -1
  35. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/references/api-fallback.md +0 -1
  36. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +4 -8
  37. package/stacks/modeling-kit/templates/.claude/skills/timeline/references/api-fallback.md +1 -7
  38. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +19 -7
  39. package/stacks/modeling-kit/templates/kit/AGENTS.md +2 -1
  40. package/stacks/modeling-kit/templates/kit/CLAUDE.md +1 -1
package/cli.js CHANGED
@@ -3595,11 +3595,11 @@ program
3595
3595
  ...agentHeaders(cfg),
3596
3596
  },
3597
3597
  body: JSON.stringify([{
3598
+ // The event id is the caller's and required; boardId/timestamp are the server's —
3599
+ // it derives both from the request and ignores anything sent here.
3598
3600
  id: randomUUID(),
3599
3601
  eventType: 'node:changed',
3600
3602
  nodeId: slice.id,
3601
- boardId: cfg.boardId,
3602
- timestamp: Date.now(),
3603
3603
  changedAttributes: ['sliceStatus'],
3604
3604
  meta: { sliceStatus: newStatus },
3605
3605
  }]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.80",
3
+ "version": "1.0.84",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -283,15 +283,17 @@ The same discipline applies to writes: `submit_node_events` takes `events[]`, so
283
283
 
284
284
  Where per-node calls genuinely can't be avoided, issue them together in one message so they run concurrently rather than in sequence.
285
285
 
286
- ### Ids and timestamps
286
+ ### Ids
287
287
 
288
- Elements you create carry client-side ids, and every `node:created` event carries a timestamp. Mint them **once per turn, in a single call**, and take from that pool as you assemble the event array:
288
+ Elements you create carry client-side ids one `nodeId` per new node — and **every event carries its own `id`**, a fresh uuid per event, never the node's. Mint them **once per turn, in a single call**, and take from that pool as you assemble the event array:
289
289
 
290
290
  ```bash
291
- for i in $(seq 5); do uuidgen; done; echo $(( $(date +%s) * 1000 ))
291
+ for i in $(seq 8); do uuidgen; done
292
292
  ```
293
293
 
294
- Nothing in that depends on anything you're about to read, so splitting it across three shells is three round trips bought for nothing. Never reach for GNU-only `date` specifiers (`%N`, `%3N`) here: BSD/macOS `date` prints them literally instead of failing, so the malformed timestamp survives until something downstream rejects it.
294
+ Nothing in that depends on anything you're about to read, so splitting it across several shells is round trips bought for nothing.
295
+
296
+ The event `id` is **required** — an event without one is rejected as an invalid shape. It is what keys the returned `hashes` map, and what makes a resubmit idempotent: the same event sent twice under the same id writes one board event, not two. An event's `boardId` and `timestamp`, by contrast, are **not** yours to send — the server takes the board from the call and stamps the time itself, and ignores both if you send them.
295
297
 
296
298
  ---
297
299
 
@@ -17,51 +17,53 @@ You now have complete knowledge of the eventmodelers platform API. This is a ref
17
17
 
18
18
  Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `orgId` (resolved from the token) or `x-user-id` (the server attributes writes to the authenticated principal). REST section numbers below give the underlying implementation for tools that wrap a single endpoint 1:1.
19
19
 
20
+ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connections[]`, …) and there are no separate plural tools — put several items in one call rather than looping. The array form always answers `{results: [...]}`, one entry per item in request order, even for a single item. The older flat form (e.g. `delete_node { nodeId }`) still works and answers with just that one result, but use the array form in new instructions.
21
+
20
22
  | Tool | Args | Purpose | REST equivalent |
21
23
  |---|---|---|---|
22
24
  | `list_boards` | — | List boards for the org | §1 `GET /api/boards` (org-scoped) |
23
- | `get_nodes` | `boardId`, `type?`, `name?`, `chapterId?`, `nodeIds?`, `projection?` (`"line"`) | List nodes, optionally by type and/or a partial case-insensitive title match. `chapterId` scopes to one timeline — prefer this over an unscoped board-wide call whenever the step is working within one chapter (the common case); `nodeIds` fetches a known, scattered subset in one call (e.g. re-verifying exactly the nodes just touched by a batch write) instead of a full `type` refetch; `projection: "line"` maps each match to `{id, type, title}` only | §3 `GET .../nodes` |
24
- | `get_node` | `boardId`, `nodeId`, `projection?` (`"cells"` \| `"edges"`) | Get one node. `projection: "cells"` (CHAPTER nodes only) returns just `{rows, columns, cells}` instead of the full `timelineData` — use whenever only the grid/occupancy is needed, not the whole chapter; `projection: "edges"` returns just that node's inbound/outbound connections instead of `findNodeById`'s full record. Both are opt-in — omitting `projection` is the unchanged full response | §3 `GET .../nodes/:nodeId` |
25
+ | `get_nodes` | `boardId`, `type?`, `name?`, `chapterId?`, `nodeIds?`, `projection?` (`"line"`) | List nodes, optionally by type and/or a partial case-insensitive title match. `chapterId` scopes to one timeline — prefer this over an unscoped board-wide call whenever the step is working within one chapter (the common case); `nodeIds` fetches a known, scattered subset in one call (e.g. re-verifying exactly the nodes just touched by a batch write) instead of a full `type` refetch; `projection: "line"` maps each match to `{id, type, title}` only. Every node carries `cellName`, its cell address in the chapter it is placed in (absent when unplaced) — read the cell names the cell-addressed tools need from here, never derive or invent one | §3 `GET .../nodes` |
26
+ | `get_node` | `boardId`, `nodeId`, `projection?` (`"cells"` \| `"edges"`) | Get one node — the full response carries `cellName`, its cell address in the chapter it is placed in. `projection: "cells"` (CHAPTER nodes only) returns just `{rows, columns, cells}` instead of the full `timelineData` — use whenever only the grid/occupancy is needed, not the whole chapter; `projection: "edges"` returns just that node's inbound/outbound connections instead of `findNodeById`'s full record. Both are opt-in — omitting `projection` is the unchanged full response | §3 `GET .../nodes/:nodeId` |
25
27
  | `get_node_comments` | `boardId`, `nodeId` | List comments on a node | §1 `GET .../nodes/:nodeId/comments` |
26
28
  | `get_board_events` | `boardId` | All board events, in sequence | §1 `GET .../events` |
27
29
  | `search_board_events` | `boardId`, `name` | Search events by node name | §1 `GET .../events/search` |
28
- | `submit_node_events` | `boardId`, `events[]`, `autoConnect?`, `compact?` | Create/update nodes (raw `NodeChangeEvent`/edge events). Every event property is described on the tool's own `events[]` schema — read that rather than this skill when all you need is the event shape. `autoConnect: false` places freshly-created nodes without wiring them to their own/previous-column neighbors (avoids a stray nearest-left edge); `compact: true` returns `{persisted: <count>}` instead of the per-node hash map | §3 `POST .../nodes/events` |
29
- | `delete_node` | `boardId`, `nodeId` | Delete a node. Deleting a chapter (timeline) cascades — every node placed in one of its cells, plus any node parented to it (e.g. SLICE_BORDER), is deleted too, along with all their edges | (via `node:deleted` event, §3) |
30
- | `create_drawing` / `create_drawings` | `boardId`, `kind`, `x`, `y`, `width`, `height`, ... (plural: `drawings[]`) | Freehand canvas annotation (path/rect/text/sticky) — never placed in a cell. Use the plural form whenever an annotation is more than one stroke (a loop plus its arrows and label is one annotation, not three calls) | — (REST `POST .../drawing/draw` accepts a single drawing or an array) |
30
+ | `submit_node_events` | `boardId`, `events[]`, `autoConnect?`, `compact?` | Create/update nodes (raw `NodeChangeEvent`/edge events). Every event property is described on the tool's own `events[]` schema — read that rather than this skill when all you need is the event shape. An event needs its own `id` (required — a fresh uuid per event, never the node id) and its `eventType`, plus the keys of its kind (`nodeId`/`meta`/`node`, or `edgeId`/`source`/`target`). The `id` keys the returned `hashes` map and makes a resubmit idempotent — the same event sent twice under one id writes a single board event. Don't send `boardId` or `timestamp` per event — the server takes the board from the call and stamps the time itself, and ignores both if sent. `autoConnect: false` places freshly-created nodes without wiring them to their own/previous-column neighbors (avoids a stray nearest-left edge); `compact: true` returns `{persisted: <count>}` instead of the per-node hash map | §3 `POST .../nodes/events` |
31
+ | `delete_node` | `boardId`, `nodeIds[]` | Delete nodes, applied in order — one failure doesn't stop the rest; answers `{results}`. Deleting a chapter (timeline) cascades — every node placed in one of its cells, plus any node parented to it (e.g. SLICE_BORDER), is deleted too, along with all their edges | (via `node:deleted` event, §3) |
32
+ | `create_drawing` | `boardId`, `drawings[]` (each `kind`, `x`, `y`, `width`, `height`, ...) | Freehand canvas annotation (path/rect/text/sticky) — never placed in a cell. An annotation is rarely one stroke: a loop plus its arrows and label is one call with several `drawings` entries, applied in order, not three calls | — (REST `POST .../drawing/draw` accepts a single drawing or an array) |
31
33
  | `find_nodes_in_drawing` | `boardId`, `drawingId` | Nodes fully contained inside a drawing's bounding box | — (no REST equivalent; MCP-only) |
32
34
  | `create_chapter` | `boardId`, `x?`, `y?`, `title?`, `columns?`, `lanes?: [{type, label?, height?}]` | Create a timeline. Omitting `x`/`y` auto-stacks it below the lowest existing chapter (by its *actual current* row-height total, not the height it was created with — safe even after `add_lane` growth), plus a fixed margin. Pass `title` to name it, `columns` for a known column count, and `lanes` to create named lanes (or several of one type, e.g. one swimlane per context) — none of the three needs a follow-up call, and the lanes are sorted into the required order automatically. The response carries every `columnId` and lane id, so the chapter needn't be read back before placing into it | §2 `POST .../chapters` |
33
35
  | `get_chapter_bounds` | `boardId` | Absolute canvas bounding box `{id, title, x, y, width, height}` of every chapter on the board — width/height derived from each chapter's current row/column layout, not a guessed default. Use before picking explicit `x`/`y` for `create_chapter` (e.g. placing below the chapter with the largest `y + height`) to avoid overlapping one that grew since it was created | §2 `GET .../chapters/bounds` |
34
36
  | `add_column` | `boardId`, `timelineId`, `index?`, `beforeNodeId?`, `afterNodeId?`, `count?` | Add one or more columns in one call. `count` inserts that many contiguously starting at the insertion point (default 1). Position with at most one of `index` (0-based), `beforeNodeId`, or `afterNodeId` (resolves the index from where that already-placed node currently sits) — omit all three to append | §2 `POST .../timelines/:id/columns` |
35
- | `delete_column` | `boardId`, `timelineId`, `columnId` | Delete a column | §2 `DELETE .../columns/:columnId` |
36
- | `add_lane` / `add_lanes` | `boardId`, `timelineId`, `type`, `label?`, `index?`, `height?` (plural: `lanes[]`) | Add a lane/row. Use the plural form for more than one; for a chapter that doesn't exist yet, pass `lanes` to `create_chapter` instead — that needs no lane call at all | §2 `POST .../timelines/:id/lanes` (accepts an array too) |
37
+ | `delete_column` | `boardId`, `timelineId`, `columnIds[]` | Delete columns, applied in order — the "cannot delete the last column" rule is checked after each deletion; answers `{results}` | §2 `DELETE .../columns/:columnId` |
38
+ | `add_lane` | `boardId`, `timelineId`, `lanes[]` (each `type`, `label?`, `index?`, `height?`) | Add lanes/rows, applied in order — each `index` resolves against the grid the previous entry left; answers `{results}`. For a chapter that doesn't exist yet, pass `lanes` to `create_chapter` instead — that needs no lane call at all | §2 `POST .../timelines/:id/lanes` (accepts an array too) |
37
39
  | `remove_lane` | `boardId`, `timelineId`, `rowId` | Remove a lane | — (extends §2; no direct REST route) |
38
- | `move_node_in_timeline` | `boardId`, `timelineId`, `movedNodeId`, `toCellId` | Move a placed node to another cellits previous cell is automatically cleared | — (MCP-only convenience) |
40
+ | `move_node_in_timeline` | `boardId`, `timelineId`, `moves[]` (each `movedNodeId`, `toCellId`) | Move placed nodes to other cellseach previous cell is automatically cleared; moves are applied in order and independently; answers `{results}` | — (MCP-only convenience) |
39
41
  | `move_timeline_structure` | `boardId`, `timelineId`, `kind` (`'column'\|'lane'`), `id`, `toIndex` | Reorder a column or lane (row) — `kind` picks which `id` refers to | — (MCP-only convenience) |
40
42
  | `move_timeline_position` | `boardId`, `timelineId`, `x`, `y` | Move a chapter node on canvas | — (MCP-only convenience) |
41
43
  | `drop_node_to_cell` | `boardId`, `timelineId`, `cellId`, `nodeId`, `nodeType` | Place an existing node into a cell — if it was already placed elsewhere on this timeline, that cell is automatically cleared | §2 `POST .../cells/:cellId/drop` |
42
44
  | `clear_cell` | `boardId`, `timelineId`, `cellId` | Unassign the node from a cell without deleting it — the cell becomes empty and the node survives (unplaced); no-op if already empty. Use `delete_node` to remove the node entirely | — (MCP-only convenience) |
43
45
  | `create_slice` | `boardId`, `timelineId`, `type`, `index?`, `nodes?: {actor?, interaction?, swimlane?}` (each `{rowId?, title?, fields?}`), `status?` | Create a full slice (column + nodes + SLICE_BORDER). `rowId` targets a specific lane when the chapter has more than one lane of that type (e.g. several actor lanes); omit to use the first matching lane. `fields` writes that node's attributes in the same call, and `status` gives the SLICE_BORDER its `sliceStatus` on creation — neither needs a follow-up write | §5 `POST .../slices` |
44
- | `create_slice_definition` | `boardId`, `timelineId`, `columnId`, `title`, `status?`, `data?`, `meta?` | Create a SLICE_BORDER over an existing column. `status` sets its `sliceStatus` straight away instead of a follow-up `update_slice_status` | §5 `POST .../slice-definitions` |
45
- | `place_element` / `place_elements` | `boardId`, `timelineId`, `elementType`, `title`, `fields?`, `lane?`, `columnIndex?`, `compact?`, `autoConnect?` (plural: `elements[]`) | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT. `fields` writes the element's attributes in the same call — don't follow a placement with a `submit_node_events` just to set them. The plural form places a whole slice's or column run's worth in one call, applied in order so each entry sees the columns the previous one added. `autoConnect: false` places without wiring to timeline neighbors — wire the edges yourself | — (MCP-only convenience; composes §2+§3) |
46
+ | `create_slice_definition` | `boardId`, `timelineId`, `slices[]` (each `columnId`, `title`, `status?`, `data?`, `meta?`) | Create SLICE_BORDERs over existing columns, one per entry, applied in order; answers `{results}`. `status` sets its `sliceStatus` straight away instead of a follow-up `update_slice_status` | §5 `POST .../slice-definitions` |
47
+ | `place_element` | `boardId`, `timelineId`, `elements[]` (each `elementType`, `title`, `fields?`, `lane?`, `columnIndex?`, `autoConnect?`), `compact?` | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT/AUTOMATION/MARKDOWN/TABLE. `fields` writes the element's attributes in the same call — don't follow a placement with a `submit_node_events` just to set them. A whole slice's or column run's worth goes in one call, applied in order so each entry sees the columns the previous one added; answers `{results}` (`compact: true` → `{nodeId, cellName, columnIndex}` per entry). `autoConnect: false` places without wiring to timeline neighbors — wire the edges yourself | — (MCP-only convenience; composes §2+§3) |
46
48
  | `list_slices` | `boardId` | List slices (id, title, status) | §8 `GET .../slicedata/slices` |
47
49
  | `get_slice_rework` | `boardId`, `contextId` | How much each slice of one context has been reworked: changes, steps backwards, and reopens after Done, most reworked first, plus planning metrics over them. `contextId` is a MODEL_CONTEXT or a timeline (a timeline resolves to the context it belongs to). Always per context — there is no board-wide form | §8 `GET .../reporting/rework/contexts/:contextId` |
48
50
  | `update_slice_status` | `boardId`, `newStatus`, plus exactly one of `sliceId` / `sliceTitle` / `columnId` | Change a SLICE_BORDER's `sliceStatus`. With a title or column id there is no need to call `list_slices` first; an ambiguous title comes back with its candidates. A slice being created takes its status from `create_slice`/`create_slice_definition` instead | — (via `node:changed` event, §3) |
49
51
  | `get_slice_data` | `boardId`, `contextName?`, `contextId?`, `sliceId?` | Full element graph for slices in a context | §8 `GET /slicedata` |
50
52
  | `get_spec_info` | `boardId`, `timelineId`, `elementTypes?` | EVENT/COMMAND/READMODEL nodes valid in GWT steps. Pass `elementTypes` (subset of `EVENT`/`COMMAND`/`READMODEL`) to avoid pulling the full element list when only one or two types are needed — filtered server-side, not just after a full fetch | §6 `GET .../spec-info` |
51
- | `get_board_outline` | `boardId`, `chapterId` | One chapter's structure, compact: per-column node lists (`{id, type, title, lane}`) + a flat edge list, no HTML pages / field bodies / meta. The cheap "what is where and how is it wired" read — prefer over `get_nodes` (no projection) for orientation checks | — (MCP-only convenience) |
53
+ | `get_board_outline` | `boardId`, `chapterId` | One chapter's structure, compact: per-column node lists (`{id, type, title, lane, cellName}`) + a flat edge list, no HTML pages / field bodies / meta. `cellName` is each node's real cell address (column letter + row number) — the one read that gives you every address in a chapter at once. The cheap "what is where and how is it wired" read — prefer over `get_nodes` (no projection) for orientation checks | — (MCP-only convenience) |
52
54
  | `get_connected_nodes` | `boardId`, `nodeId`, `chapterId?`, `direction?` (`inbound`/`outbound`/`both`), `depth?`, `types?`, `includeFields?` | Neighbours of **one** node — what feeds it and what it feeds. Answers from a single anchor, unlike `get_attribute_chain` (which needs both ends of the chain as cell names up front). `depth` follows a whole chain; `types` filters the result only, never the traversal. Each neighbour carries `via`: `"edge"` for a real connection, `"layout"` when the node has none in that direction and the neighbour was inferred from the grid using auto-connect's own window (own column + adjacent one, forward-only pairs). Real edges always win. The `layout` fallback is what makes hand-built/imported chapters — which routinely carry **zero** edges — readable instead of falsely empty | — (MCP-only convenience) |
53
55
  | `validate_model` | `boardId`, `chapterId`, `checks?[]` | Server-side Event Modeling structural checklist over one chapter — compact `findings` only. Checks: unplaced nodes, backward arrows (with the todo-list `EVENT→READMODEL` exception), zero/multi-issuer commands, sourceless read models, two-screens-in-a-column, missing scenarios. Replaces the manual per-type `get_nodes` + `get_node projection=edges` validation pass | — (MCP-only convenience) |
54
56
  | `add_scenario` | `boardId`, `timelineId`, `columnId`, `scenarios[]`, `compact?` | Append GWT scenario(s) to a column's spec node — created automatically, and a scenario `id` is generated when omitted. A given/when/then step may be addressed by `{title, type}` instead of a node id, resolved against that timeline, so no `get_spec_info` call is needed first (an ambiguous title is reported with its candidates). `compact: true` returns `{specNodeId, added, scenarioCount, isNewNode}` instead of echoing every scenario back | §6 `POST .../scenarios` |
55
57
  | `add_storyline` | `boardId`, `timelineId`, `columnId`, `storylines[]`, `compact?` | Append storyline(s) (ordered, branchable beats over existing elements) to a column's spec node. Use whenever `eventmodeling-elaborating-scenarios`'s GWT-vs-storyline decision rule calls for one (e.g. a todo list's open→close lifecycle) — not only when a user explicitly names "storyline"; that skill's own per-read-model judgment is the trigger, this catalog entry isn't a stricter gate on top of it. `compact: true` suppresses the full storyline echo | §6 `POST .../storylines` |
56
- | `set_connection` | `boardId`, `source`, `target`, `action` (`'connect'\|'remove'`) | Add or remove a type-checked directed edge. Batch form `set_connections` takes `connections[]` (applied in order) plus `compact?` `compact: true` returns a `{connected, existed, removed, notFound, failed, errors}` tally instead of one row per edge | — (via `edges` on §3 events) |
57
- | `auto_connect_node` | `boardId`, `nodeId` | Re-run auto-connect for a node | §3 `POST .../nodes/:nodeId/auto-connect` |
58
+ | `set_connection` | `boardId`, `connections[]` (each `source`, `target`, `action` `'connect'\|'remove'`), `compact?` | Add or remove type-checked directed edges, applied in order (a later entry can rely on an earlier edge); answers `{results}`, or with `compact: true` a `{connected, existed, removed, notFound, failed, errors}` tally | — (via `edges` on §3 events) |
59
+ | `auto_connect_node` | `boardId`, `nodeIds[]` | Re-run auto-connect for nodes incompatible neighbours come back in `skipped`, not as an error; answers `{results}` | §3 `POST .../nodes/:nodeId/auto-connect` |
58
60
  | `link_element` | `boardId`, `nodeId`, plus either `targetNodeId` or `timelineId` (+ `columnIndex?`, `lane?`) | Turn a node into a linked copy of `nodeId` — it receives a full copy of that node's meta plus `meta.linkedTo`. Name an existing `targetNodeId`, or pass `timelineId` to have the copy placed and linked in this one call (inheriting the original's type and title), which is what a translation or automation chain wants | §3 `POST .../nodes/:nodeId/link` |
59
- | `add_comment` / `add_comments` | `boardId`, `nodeId`, `text`, `type?` (`'COMMENT'\|'TASK'\|'QUESTION'`), `author?` (plural: `comments[]`, each with its own `nodeId`) | Add a comment — `QUESTION` is the type for a gap/edge case raised during review. Use the plural form for a review that has a question per element: all of them go in one call | §1 `POST .../boards/:boardId/comments` (batch) |
61
+ | `add_comment` | `boardId`, `comments[]` (each `nodeId`, `text`, `type?` `'COMMENT'\|'TASK'\|'QUESTION'`, `author?`) | Add comments on any nodes of one board — `QUESTION` is the type for a gap/edge case raised during review, and a review posts all its questions in one call; answers `{results}` in request order | §1 `POST .../boards/:boardId/comments` (batch) |
60
62
  | `update_comment` | `boardId`, `nodeId`, `commentId`, `action` (`'resolve'\|'delete'`) | Resolve or delete a comment | — (via comment events) |
61
- | `create_screen` | `boardId`, `contentType` (`'image'\|'sketch'\|'html'`), `nodeId?`, `chapterId`, `cellId?`/`cellName?`, plus content fields (`imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`), `title?`, `description?`, `fields?`, `autoConnect?` | Create + place a new screen node (SCREEN or HTML_SCREEN) atomically, in one call. `title` names the node (`meta.title`) in the same call — no follow-up `node:changed` just to label the screen; `create_screens` takes it per entry. Batch form `create_screens` takes `screens[]` (HTML only) + `autoConnect?`. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
63
+ | `create_screen` | `boardId`, `screens[]` (each `contentType` `'image'\|'sketch'\|'html'`, `chapterId`, `cellId?`/`cellName?`, `nodeId?`, content fields `imageBase64`/`mimeType`, `elements[]`, or `pages[]`/`backgroundColor`, `title?`, `description?`, `fields?`), `autoConnect?` | Create + place new screen nodes (SCREEN or HTML_SCREEN) with their content, in one call; entries applied in order, one failure doesn't stop the rest; answers `{results}`. `title` names the node (`meta.title`) in the same call — no follow-up `node:changed` just to label the screen. `autoConnect: false` places without wiring to timeline neighbors | §4 `POST .../images/:id/sketch` + `image-nodes` |
62
64
  | `render_screen` | `boardId`, `nodeId`, `elements[]?` (SCREEN) or `pages[]?`+`backgroundColor?` (HTML_SCREEN), `description?` | Update an existing screen's content — exactly one of `elements`/`pages` | §4 `POST .../images/:id/sketch` + `image-nodes` |
63
65
  | `add_field_examples` | `boardId`, `nodeId?`, `name?`, `cellName?`, `timelineId?` | Fill empty field examples using linked-node context | — (MCP-only convenience) |
64
- | `get_attribute_chain` | `boardId`, `timelineId`, `targetCellName`, `sourceCellName` | Resolve every node between two cells, ordered target→source | — (MCP-only convenience) |
66
+ | `get_attribute_chain` | `boardId`, `timelineId`, `targetCellName`, `sourceCellName` | Resolve every node between two cells, ordered target→source. Both cell names must come from a node's reported `cellName` (`get_board_outline`, `get_nodes`, `get_node`) — a guessed address that happens to exist silently resolves the wrong chain | — (MCP-only convenience) |
65
67
  | `verify_screen` | `boardId`, `nodeId` | Check a screen node exists and has rendered content — works for both SCREEN and HTML_SCREEN, dispatching on the node's actual type | — (MCP-only convenience) |
66
68
  | `get_image_snapshot_description` | `boardId`, `nodeId` | Load the `{elements:[...]}` sketch description from storage | — (reads what §4 sketch endpoints write) |
67
69
  | `validate_slice_data` | `sliceData` | Offline validation of a `SliceDataOutput` payload — no board access | — (MCP-only, pure function) |
@@ -337,8 +339,6 @@ Place a free-text markdown note in that lane the same way any other node is plac
337
339
  "id": "<event-uuid>",
338
340
  "eventType": "node:created",
339
341
  "nodeId": "<node-uuid>",
340
- "boardId": "<boardId>",
341
- "timestamp": 1234567890,
342
342
  "chapterId": "<chapterId>",
343
343
  "cellId": "<feedbackRowId>-<columnId>",
344
344
  "meta": {
@@ -370,13 +370,16 @@ A `node:deleted` event cascades: if the deleted node is a chapter (timeline), ev
370
370
 
371
371
  **Request body**: `NodeChangeEvent[]`
372
372
 
373
+ Every event carries its own `id` — **required**, a fresh uuid per event and never the node id. An event without one is rejected (`400`, invalid event shape). It keys the response's `hashes` map, and it is what makes a resubmit safe: the board-event insert ignores a conflicting id, so the same batch sent twice writes one history row per event rather than two.
374
+
375
+ Don't send `boardId` or `timestamp` per event: the board is the path's (the one this request was authorized for) and the timestamp is the server's. Both are ignored if sent — older payloads that include them still work.
376
+
373
377
  ```typescript
374
378
  interface NodeChangeEvent {
375
- id: string // uuid
379
+ id: string // required — fresh uuid per event, NOT the node id; keys the response's hashes map
376
380
  eventType: 'node:created' | 'node:changed' | 'node:deleted'
377
381
  nodeId: string
378
- boardId: string
379
- timestamp: number // unix ms
382
+ // boardId / timestamp: don't send them — server-derived, ignored if present
380
383
  userId?: string
381
384
  hash?: string // content hash
382
385
  changedAttributes?: string[] // dot-paths e.g. 'meta.title'
@@ -128,8 +128,6 @@ curl -s -X POST "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/events" \
128
128
  "id": "<new-random-uuid>",
129
129
  "eventType": "node:changed",
130
130
  "nodeId": "<SLICE_NODE_ID>",
131
- "boardId": "<BOARD_ID>",
132
- "timestamp": <Date.now()>,
133
131
  "changedAttributes": ["sliceStatus"],
134
132
  "meta": {
135
133
  "sliceStatus": "Blocked"
@@ -89,8 +89,6 @@ curl -s -X POST "<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/events" \
89
89
  "id": "<new-random-uuid>",
90
90
  "eventType": "node:changed",
91
91
  "nodeId": "<SLICE_NODE_ID>",
92
- "boardId": "<BOARD_ID>",
93
- "timestamp": <Date.now()>,
94
92
  "changedAttributes": ["sliceStatus"],
95
93
  "meta": {
96
94
  "sliceStatus": "<newStatus>"
@@ -42,6 +42,8 @@ mcp__eventmodelers__get_attribute_chain {
42
42
 
43
43
  The result gives you the ordered chain directly — save it as the chain used in Step 4, and skip the manual walk in 2a–3c below. Continue with the fallback only if MCP isn't connected.
44
44
 
45
+ **Both cell names must be real.** When the task named a node (an id, a title, a comment on it) instead of a cell, read that node's `cellName` off `get_board_outline` (every node in the chapter, with its address), `get_nodes`, or `get_node` — all three report it. Never compute an address from where a node appeared in a list, and never hand `get_attribute_chain` an address you have not read: a cell that exists but holds a different node resolves a wrong chain without any error.
46
+
45
47
  ### Fallback (no MCP) — resolve both cells to nodes
46
48
 
47
49
  For each cell (target and source), resolve it to a node using the exact same cell-resolution steps as the `examples` skill's "2c — Cell name" section (fetch chapters, fetch the chapter fresh to decode the grid, decode the cell name into a `CELL_ID`, then always fetch the cell live — `get_nodes` has no `cellId` filter) — see there for the full mechanics, substituting `x-user-id: attributes-skill`.
@@ -149,8 +151,6 @@ mcp__eventmodelers__submit_node_events {
149
151
  "id": "<uuid>",
150
152
  "eventType": "node:changed",
151
153
  "nodeId": "<NODE_ID_1>",
152
- "boardId": "$BOARD_ID",
153
- "timestamp": <epoch-ms>,
154
154
  "changedAttributes": ["meta.fields"],
155
155
  "meta": { "fields": "<updated_fields_array_1>" }
156
156
  },
@@ -15,13 +15,11 @@ Build the payload with Python to avoid JSON escaping issues, then POST it:
15
15
 
16
16
  ```bash
17
17
  python3 - <<EOF > /tmp/attributes_payload.json
18
- import json, time, uuid
18
+ import json, uuid
19
19
  payload = [{
20
20
  "id": str(uuid.uuid4()),
21
21
  "eventType": "node:changed",
22
22
  "nodeId": "<NODE_ID>",
23
- "boardId": "<BOARD_ID>",
24
- "timestamp": int(time.time() * 1000),
25
23
  "changedAttributes": ["meta.fields"],
26
24
  "meta": {
27
25
  "fields": <updated_fields_as_python_list>
@@ -232,8 +232,6 @@ mcp__eventmodelers__submit_node_events {
232
232
  "id": "<uuid>",
233
233
  "eventType": "node:changed",
234
234
  "nodeId": "<CHAPTER_ID>",
235
- "boardId": "<BOARD_ID>",
236
- "timestamp": <NOW_MS>,
237
235
  "changedAttributes": ["meta.title"],
238
236
  "meta": { "type": "CHAPTER", "title": "<flow name>" },
239
237
  "node": { "id": "<CHAPTER_ID>", "data": {} }
@@ -305,13 +303,15 @@ Reconstruct the captured screen as a real HTML/CSS fragment from `screen.capture
305
303
  ```
306
304
  mcp__eventmodelers__create_screen {
307
305
  "boardId": "<BOARD_ID>",
308
- "contentType": "html",
309
- "nodeId": "<SCREEN_NODE_ID>",
310
- "chapterId": "<CHAPTER_ID>",
311
- "cellId": "<CELL_ID>",
312
- "title": "<screen.title>",
313
- "pages": ["<reconstructed HTML fragment for this screen>"],
314
- "description": "<screen.description 'Shows X. Arrived via: Y. Actions: user can do A, user can do B.'>"
306
+ "screens": [{
307
+ "contentType": "html",
308
+ "nodeId": "<SCREEN_NODE_ID>",
309
+ "chapterId": "<CHAPTER_ID>",
310
+ "cellId": "<CELL_ID>",
311
+ "title": "<screen.title>",
312
+ "pages": ["<reconstructed HTML fragment for this screen>"],
313
+ "description": "<screen.description — 'Shows X. Arrived via: Y. Actions: user can do A, user can do B.'>"
314
+ }]
315
315
  }
316
316
  ```
317
317
 
@@ -322,14 +322,16 @@ mcp__eventmodelers__create_screen {
322
322
  ```
323
323
  mcp__eventmodelers__create_screen {
324
324
  "boardId": "<BOARD_ID>",
325
- "contentType": "image",
326
- "nodeId": "<SCREEN_NODE_ID>",
327
- "chapterId": "<CHAPTER_ID>",
328
- "cellName": "<CELL_NAME>",
329
- "title": "<screen.title>",
330
- "imageBase64": "<base64-encoded contents of screen.filepath, no data: URI prefix>",
331
- "mimeType": "image/png",
332
- "description": "<screen.description — 'Shows X. Arrived via: Y. Actions: user can do A, user can do B.'>"
325
+ "screens": [{
326
+ "contentType": "image",
327
+ "nodeId": "<SCREEN_NODE_ID>",
328
+ "chapterId": "<CHAPTER_ID>",
329
+ "cellName": "<CELL_NAME>",
330
+ "title": "<screen.title>",
331
+ "imageBase64": "<base64-encoded contents of screen.filepath, no data: URI prefix>",
332
+ "mimeType": "image/png",
333
+ "description": "<screen.description — 'Shows X. Arrived via: Y. Actions: user can do A, user can do B.'>"
334
+ }]
333
335
  }
334
336
  ```
335
337
 
@@ -22,8 +22,6 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
22
22
  "id": "<uuid>",
23
23
  "eventType": "node:changed",
24
24
  "nodeId": "<CHAPTER_ID>",
25
- "boardId": "<BOARD_ID>",
26
- "timestamp": <NOW_MS>,
27
25
  "changedAttributes": ["meta.title"],
28
26
  "meta": { "type": "CHAPTER", "title": "<flow name>" },
29
27
  "node": { "id": "<CHAPTER_ID>", "data": {} }
@@ -107,8 +107,6 @@ mcp__eventmodelers__submit_node_events {
107
107
  "id": "<uuid>",
108
108
  "eventType": "node:changed",
109
109
  "nodeId": "<chapterId>",
110
- "boardId": "<BOARD_ID>",
111
- "timestamp": 1234567890,
112
110
  "meta": {"type": "CHAPTER", "title": "Reservation & Lending"}
113
111
  }]
114
112
  }
@@ -200,8 +198,6 @@ mcp__eventmodelers__submit_node_events {
200
198
  "id": "<event-uuid>",
201
199
  "eventType": "node:created",
202
200
  "nodeId": "<node-uuid>",
203
- "boardId": "<BOARD_ID>",
204
- "timestamp": 1234567890,
205
201
  "chapterId": "<chapterId>",
206
202
  "cellId": "<swimlaneRowId>-<columnId>",
207
203
  "meta": {
@@ -35,8 +35,6 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
35
35
  "id": "<uuid>",
36
36
  "eventType": "node:changed",
37
37
  "nodeId": "<chapterId>",
38
- "boardId": "<boardId>",
39
- "timestamp": 1234567890,
40
38
  "meta": {"type": "CHAPTER", "title": "Reservation & Lending"}
41
39
  }]'
42
40
  ```
@@ -78,8 +76,6 @@ Same body via `POST .../nodes/events`:
78
76
  "id": "<event-uuid>",
79
77
  "eventType": "node:created",
80
78
  "nodeId": "<node-uuid>",
81
- "boardId": "<boardId>",
82
- "timestamp": 1234567890,
83
79
  "chapterId": "<chapterId>",
84
80
  "cellId": "<swimlaneRowId>-<columnId>",
85
81
  "meta": {
@@ -84,9 +84,11 @@ The todo-list read model goes in the interaction lane, **one column before** its
84
84
  mcp__eventmodelers__place_element {
85
85
  "boardId": "<BOARD_ID>",
86
86
  "timelineId": "<CHAPTER_ID>",
87
- "elementType": "READMODEL",
88
- "title": "NotificationsToSend",
89
- "columnIndex": <automationColumnIndex - 1>
87
+ "elements": [{
88
+ "elementType": "READMODEL",
89
+ "title": "NotificationsToSend",
90
+ "columnIndex": <automationColumnIndex - 1>
91
+ }]
90
92
  }
91
93
  ```
92
94
  Then set `meta.fields` and `meta.listElement: true` on the returned node id:
@@ -95,7 +97,6 @@ mcp__eventmodelers__submit_node_events {
95
97
  "boardId": "<BOARD_ID>",
96
98
  "events": [{
97
99
  "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<returned-node-id>",
98
- "boardId": "<BOARD_ID>", "timestamp": 1234567890,
99
100
  "meta": {"type": "READMODEL", "title": "NotificationsToSend", "fields": [...], "listElement": true}
100
101
  }]
101
102
  }
@@ -103,7 +104,7 @@ mcp__eventmodelers__submit_node_events {
103
104
 
104
105
  **Fallback (no MCP):** see `references/api-fallback.md` — "Placement — todo-list READMODEL, one column before its automation".
105
106
 
106
- **Place every node in a translation chain with `autoConnect: false`** (`place_element` / `create_screen` / `submit_node_events` all take the flag). A chain's columns are inserted into the middle of an existing timeline, so the default nearest-left auto-connect will wire the new todo-list READMODEL or internal EVENT to whatever unrelated event sits in the column before it — the recurring "spurious auto-connect edge near an automation chain" cleanup. Suppress it and build the chain's edges yourself in the `set_connections` batch below, then confirm with `validate_model`.
107
+ **Place every node in a translation chain with `autoConnect: false`** (`place_element` / `create_screen` / `submit_node_events` all take the flag). A chain's columns are inserted into the middle of an existing timeline, so the default nearest-left auto-connect will wire the new todo-list READMODEL or internal EVENT to whatever unrelated event sits in the column before it — the recurring "spurious auto-connect edge near an automation chain" cleanup. Suppress it and build the chain's edges yourself in the `set_connection` batch below, then confirm with `validate_model`.
107
108
 
108
109
  > **Never call `drop` after using `cellId` in `node:created`.** The drop endpoint adds a second cell reference without removing the first.
109
110
 
@@ -113,11 +114,11 @@ For a **translation-chain automation**, place its three columns left to right in
113
114
 
114
115
  1. **READMODEL → AUTOMATION first** — the automation reads its own todo list. The closing connection below is only accepted once this edge exists.
115
116
  ```
116
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<readmodelNodeId>", "target": "<automationNodeId>", "action": "connect" }
117
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<readmodelNodeId>", "target": "<automationNodeId>", "action": "connect" }] }
117
118
  ```
118
119
  2. **Every opening EVENT → READMODEL.**
119
120
  ```
120
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<openingEventNodeId>", "target": "<readmodelNodeId>", "action": "connect" }
121
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<openingEventNodeId>", "target": "<readmodelNodeId>", "action": "connect" }] }
121
122
  ```
122
123
  3. **Every closing EVENT → READMODEL — worker-stage todo lists only.** Including the automation's own resulting event, even though that event is produced by the command this same automation issues. This is not a backward arrow: `EVENT → READMODEL` connections are exempt from column ordering when the read model already has a `READMODEL → AUTOMATION` edge (see `learn-eventmodelers-api` §3) — this todo-list read model qualifies because of the edge from step 1 above. A read model in this shape is a live projection, not a frozen snapshot — a later event closing an earlier-opened item is the normal case, not an exception to reach for only when convenient. **Do not add this edge for a translation automation's todo list** — it has no closing event at all (see the translation-chain rule above); wiring one back is a modeling error, not a convenience.
123
124
 
@@ -125,7 +126,7 @@ For a **translation-chain automation**, place its three columns left to right in
125
126
 
126
127
  Skip a connection silently if the target cell is empty (the element may not exist yet). Log each created arrow, e.g. `→ connected READMODEL→AUTOMATION "NotificationsToSend"→"Send Welcome Notification"`.
127
128
 
128
- If this step is designing more than one automation's chain in the same pass, batch every connection from every automation into one `set_connections` call (with `compact: true`) instead of one `set_connection` per edge — `set_connections` still applies its entries **in order**, so keep each automation's own three-edge sequence intact (READMODEL→AUTOMATION before its closing EVENT→READMODEL) within the combined array; different automations' triples can be interleaved or concatenated freely since they don't depend on each other.
129
+ If this step is designing more than one automation's chain in the same pass, put every connection from every automation into one `set_connection` call (with `compact: true`) instead of one call per edge — `set_connection` still applies its entries **in order**, so keep each automation's own three-edge sequence intact (READMODEL→AUTOMATION before its closing EVENT→READMODEL) within the combined array; different automations' triples can be interleaved or concatenated freely since they don't depend on each other.
129
130
 
130
131
  ## Verification (run before moving to Step 5)
131
132
 
@@ -14,7 +14,6 @@ Only needed when MCP is not connected. Every call below has an MCP equivalent in
14
14
  -H "Content-Type: application/json" \
15
15
  -d '[{
16
16
  "id":"<event-uuid>","eventType":"node:created","nodeId":"<node-uuid>",
17
- "boardId":"<BOARD_ID>","timestamp":1234567890,
18
17
  "chapterId":"<CHAPTER_ID>","cellId":"<interactionRowId>-<columnId>",
19
18
  "meta":{"type":"READMODEL","title":"NotificationsToSend","fields":[...]}
20
19
  }]'
@@ -148,11 +148,13 @@ Commands go in the **interaction lane**, same column as the event they produce.
148
148
  mcp__eventmodelers__place_element {
149
149
  "boardId": "<BOARD_ID>",
150
150
  "timelineId": "<CHAPTER_ID>",
151
- "elementType": "COMMAND",
152
- "title": "ReserveBike"
151
+ "elements": [{
152
+ "elementType": "COMMAND",
153
+ "title": "ReserveBike"
154
+ }]
153
155
  }
154
156
  ```
155
- `place_element` finds/creates the empty cell in the interaction lane in the correct column and places the node but it does not accept `fields`. Immediately follow up with `submit_node_events` (`node:changed`) to set `meta.fields` (with `mapping`/`generated`/`cardinality` per the rules above) on the node it returned:
157
+ `place_element` finds/creates the empty cell in the interaction lane in the correct column and places the node. Pass each entry's `fields` in the same call when the attributes are already known; otherwise follow up with `submit_node_events` (`node:changed`) to set `meta.fields` (with `mapping`/`generated`/`cardinality` per the rules above) on the node it returned:
156
158
  ```
157
159
  mcp__eventmodelers__submit_node_events {
158
160
  "boardId": "<BOARD_ID>",
@@ -160,8 +162,6 @@ mcp__eventmodelers__submit_node_events {
160
162
  "id": "<event-uuid>",
161
163
  "eventType": "node:changed",
162
164
  "nodeId": "<returned-node-id>",
163
- "boardId": "<BOARD_ID>",
164
- "timestamp": 1234567890,
165
165
  "meta": {
166
166
  "type": "COMMAND",
167
167
  "title": "ReserveBike",
@@ -203,9 +203,9 @@ After `place-element` returns the COMMAND node ID, create the arrows that comple
203
203
  ```
204
204
  Read `cells["<actorRowId>-<columnId>"]` for the occupying node id (a cell id absent from that sparse array is empty — no SCREEN placed yet). Then connect with the type-checked edge tool, which auto-corrects direction and skips duplicates:
205
205
  ```
206
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<screenNodeId>", "target": "<commandNodeId>", "action": "connect" }
206
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<screenNodeId>", "target": "<commandNodeId>", "action": "connect" }] }
207
207
  ```
208
- If wiring more than one COMMAND in the same pass, prefer batching every SCREEN→COMMAND and COMMAND→EVENT pair across all of them into one `set_connections` call (see step 2 below) instead of one `set_connection` per pair.
208
+ If wiring more than one COMMAND in the same pass, put every SCREEN→COMMAND and COMMAND→EVENT pair across all of them into one `set_connection` call's `connections` array (see step 2 below) instead of one call per pair.
209
209
 
210
210
  **Fallback (no MCP):** see `references/api-fallback.md` — "Wire connections — Step 1: SCREEN → COMMAND".
211
211
 
@@ -219,7 +219,7 @@ After `place-element` returns the COMMAND node ID, create the arrows that comple
219
219
  ```
220
220
  Read `cells["<swimlaneRowId>-<columnId>"]` for the occupying node id, then:
221
221
  ```
222
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<commandNodeId>", "target": "<eventNodeId>", "action": "connect" }
222
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<commandNodeId>", "target": "<eventNodeId>", "action": "connect" }] }
223
223
  ```
224
224
 
225
225
  **Fallback (no MCP):** see `references/api-fallback.md` — "Wire connections — Step 2: COMMAND → EVENT".
@@ -228,7 +228,7 @@ After `place-element` returns the COMMAND node ID, create the arrows that comple
228
228
 
229
229
  Skip a connection silently if the target cell is empty (the element may be placed in a later step). Log each created arrow: `→ connected SCREEN→COMMAND "PlaceOrder"` or `→ connected COMMAND→EVENT "PlaceOrder"→"OrderPlaced"`.
230
230
 
231
- If wiring more than one COMMAND in this pass, send all the SCREEN→COMMAND and COMMAND→EVENT edges in one `set_connections` call with `compact: true` — you author every edge here deliberately, so the `{connected, existed, removed, notFound, failed, errors}` tally is enough.
231
+ If wiring more than one COMMAND in this pass, send all the SCREEN→COMMAND and COMMAND→EVENT edges in one `set_connection` call with `compact: true` — you author every edge here deliberately, so the `{connected, existed, removed, notFound, failed, errors}` tally is enough.
232
232
 
233
233
  **After wiring, run `validate_model` (`{boardId, chapterId}`).** Its `command-issuers` finding flags any COMMAND that ended up with two issuers — the classic symptom of the platform's auto-connect cross-wiring a previous-column SCREEN into a command that an AUTOMATION already drives (or vice versa). Fix each by removing the wrong edge. If you already know a placement sits next to an unrelated column whose SCREEN/AUTOMATION would be mis-wired, place that COMMAND with `autoConnect: false` and wire its single real issuer yourself.
234
234
 
@@ -34,8 +34,6 @@ curl -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
34
34
  "id": "<event-uuid>",
35
35
  "eventType": "node:created",
36
36
  "nodeId": "<node-uuid>",
37
- "boardId": "<boardId>",
38
- "timestamp": 1234567890,
39
37
  "chapterId": "<chapterId>",
40
38
  "cellId": "<interactionRowId>-<columnId>",
41
39
  "meta": {
@@ -256,9 +256,11 @@ Read models go in the `interaction` lane. For a **SCREEN**, the primary read mod
256
256
  mcp__eventmodelers__place_element {
257
257
  "boardId": "<BOARD_ID>",
258
258
  "timelineId": "<CHAPTER_ID>",
259
- "elementType": "READMODEL",
260
- "title": "ActiveReservationView",
261
- "columnIndex": <consumerScreenColumnIndex> // or <automationColumnIndex - 1> for an AUTOMATION consumer
259
+ "elements": [{
260
+ "elementType": "READMODEL",
261
+ "title": "ActiveReservationView",
262
+ "columnIndex": <consumerScreenColumnIndex> // or <automationColumnIndex - 1> for an AUTOMATION consumer
263
+ }]
262
264
  }
263
265
  ```
264
266
  Then set `meta.fields` (with `mapping`/`generated`/`cardinality`) — and `meta.listElement: true` if this read model is list-shaped — on the returned node id:
@@ -267,7 +269,6 @@ mcp__eventmodelers__submit_node_events {
267
269
  "boardId": "<BOARD_ID>",
268
270
  "events": [{
269
271
  "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<returned-node-id>",
270
- "boardId": "<BOARD_ID>", "timestamp": 1234567890,
271
272
  "meta": {"type": "READMODEL", "title": "ActiveReservationView", "fields": [...]}
272
273
  }]
273
274
  }
@@ -280,7 +281,7 @@ mcp__eventmodelers__get_node { "boardId": "<BOARD_ID>", "nodeId": "<CHAPTER_ID>"
280
281
 
281
282
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5f — Placing a READMODEL node (full manual sequence)".
282
283
 
283
- **Pass `autoConnect: false` on every `place_element` / `node:created` call in this step.** Output read-model columns are inserted right next to automation-chain and command columns whose events are *not* the read model's sources — the default auto-connect would wire the new READMODEL to the nearest event on its left, which is exactly the stray-edge cleanup this step has repeatedly generated. Suppress it here and wire every `EVENT → READMODEL` / `READMODEL → SCREEN` / `READMODEL → AUTOMATION` edge explicitly in Step 5h's `set_connections` batch. After the batch, run `validate_model` (`{boardId, chapterId}`) — its `backward-arrows` and `readmodel-sources` findings confirm the wiring is what you intended and no stray edge slipped in.
284
+ **Pass `autoConnect: false` on every `place_element` / `node:created` call in this step.** Output read-model columns are inserted right next to automation-chain and command columns whose events are *not* the read model's sources — the default auto-connect would wire the new READMODEL to the nearest event on its left, which is exactly the stray-edge cleanup this step has repeatedly generated. Suppress it here and wire every `EVENT → READMODEL` / `READMODEL → SCREEN` / `READMODEL → AUTOMATION` edge explicitly in Step 5h's `set_connection` batch. After the batch, run `validate_model` (`{boardId, chapterId}`) — its `backward-arrows` and `readmodel-sources` findings confirm the wiring is what you intended and no stray edge slipped in.
284
285
 
285
286
  **For an AUTOMATION** (actor lane) — its READMODEL always goes **one column to its left**, never the same column: the automation's own column already holds the COMMAND it issues (interaction row), so the read model can't also live there.
286
287
 
@@ -326,7 +327,7 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
326
327
  ```
327
328
  mcp__eventmodelers__get_node { "boardId": "<BOARD_ID>", "nodeId": "<CHAPTER_ID>", "projection": "cells" }
328
329
  # → read cells["<swimlaneRowId>-<columnId>"] for the occupying node id
329
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<eventNodeId>", "target": "<readmodelNodeId>", "action": "connect" }
330
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<eventNodeId>", "target": "<readmodelNodeId>", "action": "connect" }] }
330
331
  ```
331
332
 
332
333
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5h.1 — Wire EVENT → READMODEL".
@@ -335,7 +336,7 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
335
336
 
336
337
  **Prefer MCP:**
337
338
  ```
338
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<readmodelNodeId>", "target": "<screenNodeId>", "action": "connect" }
339
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<readmodelNodeId>", "target": "<screenNodeId>", "action": "connect" }] }
339
340
  ```
340
341
 
341
342
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5h.2 — Wire READMODEL → SCREEN".
@@ -346,14 +347,14 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
346
347
 
347
348
  **Prefer MCP:**
348
349
  ```
349
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<readmodelNodeId>", "target": "<automationNodeId>", "action": "connect" }
350
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<readmodelNodeId>", "target": "<automationNodeId>", "action": "connect" }] }
350
351
  ```
351
352
 
352
353
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5h.3 — Wire READMODEL → AUTOMATION".
353
354
 
354
355
  Skip a connection silently if the target cell is empty. Log each created arrow: `→ connected EVENT→READMODEL "OrderPlaced"→"OrderStatusView"`, `→ connected READMODEL→SCREEN "OrderStatusView"→"Order Status Screen"`, or `→ connected READMODEL→AUTOMATION "OrderStatusView"→"Fulfillment Processor"`.
355
356
 
356
- If this step is processing more than one read model in the same pass, collect every connection resolved above (across all of them) into one `set_connections` call instead of one `set_connection` per pair — this was the single largest source of individual tool calls in this step. Pass `compact: true` on that call: with `autoConnect: false` set at placement, every edge here is one you're deliberately creating, so a `{connected, existed, removed, notFound, failed, errors}` tally is all you need back — not a row per edge.
357
+ If this step is processing more than one read model in the same pass, collect every connection resolved above (across all of them) into one `set_connection` call instead of one call per pair — this was the single largest source of individual tool calls in this step. Pass `compact: true` on that call: with `autoConnect: false` set at placement, every edge here is one you're deliberately creating, so a `{connected, existed, removed, notFound, failed, errors}` tally is all you need back — not a row per edge.
357
358
 
358
359
  4. **Document the reasoning for each connected event** — for every EVENT → READMODEL edge wired in step 1 (including any added later, e.g. via Step 5g's backward-connection exemption or a Step 5c/copy pattern), record why that event feeds this read model: which field(s) it sets or updates, and why. Use one MARKDOWN note per read model, in that read model's own column (same feedback-lane + MARKDOWN mechanics as `eventmodeling-orchestrating-event-modeling`'s "Documenting decisions inline, at any step" / Step 11 — add the chapter's feedback lane first if it doesn't already exist, then place the note at `cellId = "<feedbackLaneId>-<readModelColumnId>"`). Extend the existing note (don't create a second one) when the read model later gains another connected event.
359
360
 
@@ -26,8 +26,6 @@ Only needed when MCP is not connected. Every call below has an MCP equivalent in
26
26
  "id": "<event-uuid>",
27
27
  "eventType": "node:created",
28
28
  "nodeId": "<node-uuid>",
29
- "boardId": "<BOARD_ID>",
30
- "timestamp": 1234567890,
31
29
  "chapterId": "<CHAPTER_ID>",
32
30
  "cellId": "<interactionRowId>-<columnId>",
33
31
  "meta": {"type": "READMODEL", "title": "ActiveReservationView", "fields": [...]}