@eventmodelers/cli 1.0.81 → 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 (17) hide show
  1. package/package.json +1 -1
  2. package/shared/skills/learn-eventmodelers-api/SKILL.md +13 -11
  3. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +19 -15
  4. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-automation-chains/SKILL.md +9 -7
  5. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +9 -7
  6. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +10 -8
  7. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +14 -12
  8. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +2 -2
  9. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/examples.md +2 -2
  10. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +11 -9
  11. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +2 -2
  12. package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +8 -6
  13. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +28 -21
  14. package/stacks/modeling-kit/templates/.claude/skills/place-element/references/api-fallback.md +1 -1
  15. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +18 -14
  16. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +4 -4
  17. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +19 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.81",
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": {
@@ -17,6 +17,8 @@ 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) |
@@ -26,23 +28,23 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
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
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` |
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) |
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) |
@@ -53,12 +55,12 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
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
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) |
@@ -303,13 +303,15 @@ Reconstruct the captured screen as a real HTML/CSS fragment from `screen.capture
303
303
  ```
304
304
  mcp__eventmodelers__create_screen {
305
305
  "boardId": "<BOARD_ID>",
306
- "contentType": "html",
307
- "nodeId": "<SCREEN_NODE_ID>",
308
- "chapterId": "<CHAPTER_ID>",
309
- "cellId": "<CELL_ID>",
310
- "title": "<screen.title>",
311
- "pages": ["<reconstructed HTML fragment for this screen>"],
312
- "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
+ }]
313
315
  }
314
316
  ```
315
317
 
@@ -320,14 +322,16 @@ mcp__eventmodelers__create_screen {
320
322
  ```
321
323
  mcp__eventmodelers__create_screen {
322
324
  "boardId": "<BOARD_ID>",
323
- "contentType": "image",
324
- "nodeId": "<SCREEN_NODE_ID>",
325
- "chapterId": "<CHAPTER_ID>",
326
- "cellName": "<CELL_NAME>",
327
- "title": "<screen.title>",
328
- "imageBase64": "<base64-encoded contents of screen.filepath, no data: URI prefix>",
329
- "mimeType": "image/png",
330
- "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
+ }]
331
335
  }
332
336
  ```
333
337
 
@@ -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:
@@ -102,7 +104,7 @@ mcp__eventmodelers__submit_node_events {
102
104
 
103
105
  **Fallback (no MCP):** see `references/api-fallback.md` — "Placement — todo-list READMODEL, one column before its automation".
104
106
 
105
- **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`.
106
108
 
107
109
  > **Never call `drop` after using `cellId` in `node:created`.** The drop endpoint adds a second cell reference without removing the first.
108
110
 
@@ -112,11 +114,11 @@ For a **translation-chain automation**, place its three columns left to right in
112
114
 
113
115
  1. **READMODEL → AUTOMATION first** — the automation reads its own todo list. The closing connection below is only accepted once this edge exists.
114
116
  ```
115
- 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" }] }
116
118
  ```
117
119
  2. **Every opening EVENT → READMODEL.**
118
120
  ```
119
- 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" }] }
120
122
  ```
121
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.
122
124
 
@@ -124,7 +126,7 @@ For a **translation-chain automation**, place its three columns left to right in
124
126
 
125
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"`.
126
128
 
127
- 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.
128
130
 
129
131
  ## Verification (run before moving to Step 5)
130
132
 
@@ -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>",
@@ -201,9 +203,9 @@ After `place-element` returns the COMMAND node ID, create the arrows that comple
201
203
  ```
202
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:
203
205
  ```
204
- 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" }] }
205
207
  ```
206
- 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.
207
209
 
208
210
  **Fallback (no MCP):** see `references/api-fallback.md` — "Wire connections — Step 1: SCREEN → COMMAND".
209
211
 
@@ -217,7 +219,7 @@ After `place-element` returns the COMMAND node ID, create the arrows that comple
217
219
  ```
218
220
  Read `cells["<swimlaneRowId>-<columnId>"]` for the occupying node id, then:
219
221
  ```
220
- 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" }] }
221
223
  ```
222
224
 
223
225
  **Fallback (no MCP):** see `references/api-fallback.md` — "Wire connections — Step 2: COMMAND → EVENT".
@@ -226,7 +228,7 @@ After `place-element` returns the COMMAND node ID, create the arrows that comple
226
228
 
227
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"`.
228
230
 
229
- 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.
230
232
 
231
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.
232
234
 
@@ -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:
@@ -279,7 +281,7 @@ mcp__eventmodelers__get_node { "boardId": "<BOARD_ID>", "nodeId": "<CHAPTER_ID>"
279
281
 
280
282
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5f — Placing a READMODEL node (full manual sequence)".
281
283
 
282
- **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.
283
285
 
284
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.
285
287
 
@@ -325,7 +327,7 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
325
327
  ```
326
328
  mcp__eventmodelers__get_node { "boardId": "<BOARD_ID>", "nodeId": "<CHAPTER_ID>", "projection": "cells" }
327
329
  # → read cells["<swimlaneRowId>-<columnId>"] for the occupying node id
328
- 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" }] }
329
331
  ```
330
332
 
331
333
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5h.1 — Wire EVENT → READMODEL".
@@ -334,7 +336,7 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
334
336
 
335
337
  **Prefer MCP:**
336
338
  ```
337
- 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" }] }
338
340
  ```
339
341
 
340
342
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5h.2 — Wire READMODEL → SCREEN".
@@ -345,14 +347,14 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
345
347
 
346
348
  **Prefer MCP:**
347
349
  ```
348
- 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" }] }
349
351
  ```
350
352
 
351
353
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 5h.3 — Wire READMODEL → AUTOMATION".
352
354
 
353
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"`.
354
356
 
355
- 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.
356
358
 
357
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.
358
360
 
@@ -110,7 +110,7 @@ For each returned node, check whether it has a valid cell assignment. A node wit
110
110
 
111
111
  **Prefer MCP:**
112
112
  ```
113
- mcp__eventmodelers__delete_node { "boardId": "$BOARD_ID", "nodeId": "<nodeId>" }
113
+ mcp__eventmodelers__delete_node { "boardId": "$BOARD_ID", "nodeIds": ["<nodeId>"] }
114
114
  ```
115
115
 
116
116
  **Fallback (no MCP):** see `references/api-fallback.md` — "No unplaced elements (0,0 nodes) — Delete an orphaned node".
@@ -135,18 +135,20 @@ Screens placed during Step 3 (Storyboarding) are provisional positions. Steps 4
135
135
  ### Column insertion
136
136
  Use `add_column` with `{"index": N}` to insert a column at a specific position (shifts existing columns right) — or, when the insertion point is "immediately before/after a node already on the board" rather than a numeric position you'd otherwise have to compute, pass `beforeNodeId`/`afterNodeId` instead and let the tool resolve the index itself. Do not use no position at all (append) when placing read models or view screens — always target the correct position.
137
137
 
138
- **Suppress auto-connect when inserting into an existing chain.** When you insert columns next to nodes that are *not* meant to connect to what you're about to place — e.g. slotting an output read model's column in beside an automation-chain column — the node placement's default auto-connect will wire the new node to whatever type-compatible node happens to sit in its own or the previous column (the "nearest event to the left"). That is the source of the recurring stray-edge cleanup. When the placement you're about to make should be wired only by your own explicit `set_connections` batch, pass `autoConnect: false` on the placing call (`submit_node_events`, `place_element`, `create_screen`/`create_screens`) and then wire every edge yourself. Keep the default (auto-connect on) for Steps 1/3/4 where same-column neighbors are exactly the intended wiring.
138
+ **Suppress auto-connect when inserting into an existing chain.** When you insert columns next to nodes that are *not* meant to connect to what you're about to place — e.g. slotting an output read model's column in beside an automation-chain column — the node placement's default auto-connect will wire the new node to whatever type-compatible node happens to sit in its own or the previous column (the "nearest event to the left"). That is the source of the recurring stray-edge cleanup. When the placement you're about to make should be wired only by your own explicit `set_connection` batch, pass `autoConnect: false` on the placing call (`submit_node_events`, `place_element`, `create_screen`) and then wire every edge yourself. Keep the default (auto-connect on) for Steps 1/3/4 where same-column neighbors are exactly the intended wiring.
139
139
 
140
- ### Prefer batch MCP tools over one-call-per-item loops
140
+ ### Every write tool takes an array — fill it
141
141
 
142
- Several MCP tools have a batch form that does the exact same thing as calling their singular form once per item, with the same validation rules and (where order matters, e.g. wiring a READMODEL→AUTOMATION edge before the backward EVENT→READMODEL edge that depends on it) the same in-order guarantee just fewer round trips. Whenever a step's own instructions below show a single-item call and more than one item is being processed in the same pass, use the batch form instead:
142
+ Each write tool takes its items as an array, so handling several in one pass is one call rather than a loop. Entries are applied in order — which matters where a later one depends on an earlier one, e.g. wiring a READMODEL→AUTOMATION edge before the backward EVENT→READMODEL edge that needs it every entry reports its own outcome or error in `results`, and one failure does not stop the rest. Whenever a step's instructions below show a one-entry call and more than one item is being processed in the same pass, extend the array instead of repeating the call:
143
143
 
144
- - `set_connections` (not `set_connection` repeated) — wiring multiple edges
145
- - `auto_connect_nodes` (not `auto_connect_node` repeated) — auto-connecting multiple freshly-placed nodes
146
- - `create_slice_definitions` (not `create_slice_definition` repeated) — defining multiple slices
147
- - `create_screens` (not `create_screen` repeated) — creating multiple HTML screens whose content is already authored
148
- - `move_nodes` (not `move_node_in_timeline` repeated) moving multiple already-placed nodes within one timeline
149
- - `delete_nodes` / `delete_columns` (not `delete_node`/`delete_column` repeated) removing multiple nodes or columns, e.g. a corrective cleanup after a modeling mistake
144
+ - `set_connection`'s `connections` — wiring multiple edges
145
+ - `auto_connect_node`'s `nodeIds` — auto-connecting multiple freshly-placed nodes
146
+ - `create_slice_definition`'s `slices` — defining multiple slices
147
+ - `create_screen`'s `screens` — creating multiple screens
148
+ - `place_element`'s `elements` — laying out a whole slice or column run, each entry seeing the columns the previous one added
149
+ - `move_node_in_timeline`'s `moves` — moving multiple already-placed nodes within one timeline
150
+ - `delete_node`'s `nodeIds` / `delete_column`'s `columnIds` — removing multiple nodes or columns, e.g. a corrective cleanup after a modeling mistake
151
+ - `add_comment`'s `comments`, `create_drawing`'s `drawings`, `add_lane`'s `lanes` — same shape
150
152
  - `add_column`'s `count` param (not `add_column` repeated) — appending or inserting several columns at once; `beforeNodeId`/`afterNodeId` resolve the insertion point from an already-placed node instead of a computed index
151
153
  - `create_chapter`'s `columns` param — when the chapter's initial column count is already known, instead of creating the default 3 and appending more after
152
154
 
@@ -156,7 +158,7 @@ For a "what is on the board and how is it wired right now" check between steps
156
158
 
157
159
  **Structural validation is one call, not a scan.** `validate_model` (`{boardId, chapterId}`) runs the whole structural checklist server-side — unplaced nodes, backward arrows (todo-list exception applied), zero/multi-issuer commands, sourceless read models, two-screens-in-a-column, missing scenarios — and returns only `findings`. Use it for the mandatory post-step unplaced check and as the first move in Step 9, instead of per-type `get_nodes` loops and `get_node` `projection: "edges"` spot-checks.
158
160
 
159
- **Ask echo-heavy write tools for less.** `add_scenario`, `add_storyline`, `set_connections` and `submit_node_events` each accept `compact: true`, which drops the full-object echo from the response (returning `{specNodeId, added, count, isNewNode}`, a `{connected, existed, removed, notFound, failed, errors}` tally, or `{persisted: <count>}` respectively). Pass it whenever you're not going to read individual fields back off the response — which is almost always for a large `set_connections` batch or a bulk scenario post.
161
+ **Ask echo-heavy write tools for less.** `add_scenario`, `add_storyline`, `set_connection` and `submit_node_events` each accept `compact: true`, which drops the full-object echo from the response (returning `{specNodeId, added, count, isNewNode}`, a `{connected, existed, removed, notFound, failed, errors}` tally, or `{persisted: <count>}` respectively). Pass it whenever you're not going to read individual fields back off the response — which is almost always for a large `set_connection` batch or a bulk scenario post.
160
162
 
161
163
  ### Documenting decisions inline, at any step
162
164
 
@@ -416,7 +418,7 @@ Not delegated to a separate skill — performed directly by this orchestrating s
416
418
 
417
419
  **Prefer MCP:**
418
420
  ```
419
- mcp__eventmodelers__add_lane { "boardId": "$BOARD_ID", "timelineId": "$CHAPTER_ID", "type": "feedback", "label": "Notes" }
421
+ mcp__eventmodelers__add_lane { "boardId": "$BOARD_ID", "timelineId": "$CHAPTER_ID", "lanes": [{ "type": "feedback", "label": "Notes" }] }
420
422
  ```
421
423
 
422
424
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 11 — Document Reasoning — Add a feedback lane".
@@ -99,7 +99,7 @@ For every column from Step 2 that doesn't already have a matching slice, mark ea
99
99
 
100
100
  Prefer MCP:
101
101
  ```
102
- mcp__eventmodelers__create_slice_definitions { "boardId": "<BOARD_ID>", "timelineId": "<TL>", "slices": [
102
+ mcp__eventmodelers__create_slice_definition { "boardId": "<BOARD_ID>", "timelineId": "<TL>", "slices": [
103
103
  { "columnId": "<colId1>", "title": "PlaceOrder" },
104
104
  { "columnId": "<colId2>", "title": "OrderStatusView" }
105
105
  ] }
@@ -112,7 +112,7 @@ mcp__eventmodelers__create_slice_definitions { "boardId": "<BOARD_ID>", "timelin
112
112
  - READMODEL column → title = read model name (state-view slice)
113
113
  - AUTOMATION column → title = automation name, or the command it issues (automation slice)
114
114
 
115
- Use **`create_slice_definitions`/`slice-definitions`**, never `create_slice`/the plain **`slices`** endpoint here — `create_slice`/`slices` creates a brand-new column with its own swimlane/content nodes, which would duplicate the element already placed on the timeline. `create_slice_definitions`/`slice-definitions` only adds a `SLICE_BORDER` node to each column you already resolved in Step 2. `title` always comes from the request body — it is never derived automatically from the command/read model/automation node.
115
+ Use **`create_slice_definition`/`slice-definitions`**, never `create_slice`/the plain **`slices`** endpoint here — `create_slice`/`slices` creates a brand-new column with its own swimlane/content nodes, which would duplicate the element already placed on the timeline. `create_slice_definition`/`slice-definitions` only adds a `SLICE_BORDER` node to each column you already resolved in Step 2. `title` always comes from the request body — it is never derived automatically from the command/read model/automation node.
116
116
 
117
117
  **If Step 2 finds nothing to slice** (every COMMAND/READMODEL/AUTOMATION on the timeline already has a matching `SLICE_BORDER`), this skill's job is done — there is no existing element left to make explicit. Do not invent new model content here; that is out of scope for a skill whose whole design assumes the model is already complete. Invoke the `add-next-slice` skill instead — it owns deciding on and creating a genuinely new slice from scratch.
118
118
 
@@ -50,10 +50,10 @@ No slice depends on another slice directly — only on the events it produces.
50
50
 
51
51
  ## Creating These Slices via the API
52
52
 
53
- These elements already exist on the timeline (from `spec-info`) — use `create_slice_definitions`/`slice-definitions`, which only adds a `SLICE_BORDER` to each column's existing element. Never use `create_slice`/the plain `/slices` endpoint here: that endpoint creates a brand-new column with its own nodes, which would duplicate the element already on the board.
53
+ These elements already exist on the timeline (from `spec-info`) — use `create_slice_definition`/`slice-definitions`, which only adds a `SLICE_BORDER` to each column's existing element. Never use `create_slice`/the plain `/slices` endpoint here: that endpoint creates a brand-new column with its own nodes, which would duplicate the element already on the board.
54
54
 
55
55
  ```
56
- mcp__eventmodelers__create_slice_definitions { "boardId": "<BOARD_ID>", "timelineId": "<TL>", "slices": [
56
+ mcp__eventmodelers__create_slice_definition { "boardId": "<BOARD_ID>", "timelineId": "<TL>", "slices": [
57
57
  { "columnId": "<placeOrderColumnId>", "title": "PlaceOrder" },
58
58
  { "columnId": "<orderDetailViewColumnId>", "title": "OrderDetailView" },
59
59
  { "columnId": "<reserveInventoryOnPaymentColumnId>", "title": "ReserveInventoryOnPayment" }
@@ -122,7 +122,7 @@ After completing the screen analysis, use the `handle-comment` skill to post a c
122
122
 
123
123
  **Prefer MCP:**
124
124
  ```
125
- mcp__eventmodelers__add_lane { "boardId": "$BOARD_ID", "timelineId": "$CHAPTER_ID", "type": "actor", "label": "<Role Name>" }
125
+ mcp__eventmodelers__add_lane { "boardId": "$BOARD_ID", "timelineId": "$CHAPTER_ID", "lanes": [{ "type": "actor", "label": "<Role Name>" }] }
126
126
  ```
127
127
 
128
128
  **Fallback (no MCP):** see `references/api-fallback.md` — "Resolve One Actor Lane Per Human Role — Step 3: Create a new actor lane".
@@ -226,13 +226,15 @@ Every screen node requires rendered content. **HTML_SCREEN (via the `html-screen
226
226
  ```
227
227
  mcp__eventmodelers__create_screen {
228
228
  "boardId": "<BOARD_ID>",
229
- "contentType": "html",
230
- "nodeId": "<node-uuid>",
231
- "chapterId": "<CHAPTER_ID>",
232
- "cellId": "<actorRowId>-<columnId>",
233
- "pages": ["<div>...</div>"],
234
- "description": "<concise description of what this screen shows>",
235
- "fields": [ /* per "Mandatory Field Definitions" below — set in this same call */ ]
229
+ "screens": [{
230
+ "contentType": "html",
231
+ "nodeId": "<node-uuid>",
232
+ "chapterId": "<CHAPTER_ID>",
233
+ "cellId": "<actorRowId>-<columnId>",
234
+ "pages": ["<div>...</div>"],
235
+ "description": "<concise description of what this screen shows>",
236
+ "fields": [ /* per "Mandatory Field Definitions" below — set in this same call */ ]
237
+ }]
236
238
  }
237
239
  ```
238
240
 
@@ -240,7 +242,7 @@ mcp__eventmodelers__create_screen {
240
242
 
241
243
  The MCP `create_screen` call above already sets `meta.fields` (per "Mandatory Field Definitions" below) in the same call — no separate `node:changed` follow-up needed when using MCP.
242
244
 
243
- A storyboard screen is placed at a *provisional* position — Steps 4 and 5 wire it to its COMMAND / READMODEL once those exist, and may move it first. Pass `autoConnect: false` on `create_screen` / `create_screens` here so the placement doesn't pre-wire the screen to whatever happens to sit in the adjacent column; the real `SCREEN → COMMAND` and `READMODEL → SCREEN` edges are created deliberately in Steps 4 and 5. When creating several screens whose HTML is already authored, use `create_screens` (batch) with `autoConnect: false`.
245
+ A storyboard screen is placed at a *provisional* position — Steps 4 and 5 wire it to its COMMAND / READMODEL once those exist, and may move it first. Pass `autoConnect: false` on `create_screen` here so the placement doesn't pre-wire the screen to whatever happens to sit in the adjacent column; the real `SCREEN → COMMAND` and `READMODEL → SCREEN` edges are created deliberately in Steps 4 and 5. When creating several screens whose HTML is already authored, put them all in one `create_screen` call's `screens` array with `autoConnect: false`.
244
246
 
245
247
  Design the page(s) as real HTML/CSS, following the `html-screen` skill's guidance: write full-size markup (16px body text, generous padding — the canvas scales it down, don't shrink it yourself), one complete self-contained fragment per page (no `<html>`/`<head>`/`<body>` wrapper — the canvas adds those), no `<script>`/inline handlers (stripped server-side), and Bulma CSS classes (`title`, `button`, `is-primary`, `field`/`control`/`input`, etc. — remember heading size modifiers like `class="title is-1"`) since Bulma 0.9.4 is loaded by default. Every page MUST include real field labels matching the actual event/command fields this screen captures or displays, and at least one primary action (submit/confirm button) for command screens.
246
248
 
@@ -43,7 +43,7 @@ Route to the matching section below based on `action`.
43
43
  **Prefer MCP** — one call, `type` (`COMMENT`/`TASK`) passed straight through:
44
44
 
45
45
  ```
46
- mcp__eventmodelers__add_comment { "boardId": "$BOARD_ID", "nodeId": "$NODE_ID", "text": "<text>", "type": "<COMMENT|TASK>", "author": "<author>" }
46
+ mcp__eventmodelers__add_comment { "boardId": "$BOARD_ID", "comments": [{ "nodeId": "$NODE_ID", "text": "<text>", "type": "<COMMENT|TASK>", "author": "<author>" }] }
47
47
  ```
48
48
 
49
49
  **Fallback (no MCP):** see `references/api-fallback.md` — "Action: place".
@@ -58,7 +58,7 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/comments" \
58
58
  {"nodeId":"<id2>","text":"<text2>","type":"QUESTION","author":"wdyt"}]'
59
59
  ```
60
60
 
61
- Response: `201 {"results":[{"nodeId":"<id>","id":"<commentId>"}, …]}` in request order — an entry whose node doesn't exist reports `error` there without dropping the rest. Over MCP this is the `add_comments` tool.
61
+ Response: `201 {"results":[{"nodeId":"<id>","id":"<commentId>"}, …]}` in request order — an entry whose node doesn't exist reports `error` there without dropping the rest. Over MCP this is the `add_comment` tool's `comments` array.
62
62
 
63
63
  **Report:**
64
64
  ```
@@ -135,12 +135,14 @@ mcp__eventmodelers__render_screen {
135
135
  ```
136
136
  mcp__eventmodelers__create_screen {
137
137
  "boardId": "<BOARD_ID>",
138
- "contentType": "html",
139
- "nodeId": "<generated-uuid>",
140
- "chapterId": "<CHAPTER_ID>",
141
- "cellName": "<CELL_NAME>",
142
- "pages": ["<div>...</div>"],
143
- "fields": [ /* optional — set meta.fields here too (see Step 5) instead of a separate call */ ]
138
+ "screens": [{
139
+ "contentType": "html",
140
+ "nodeId": "<generated-uuid>",
141
+ "chapterId": "<CHAPTER_ID>",
142
+ "cellName": "<CELL_NAME>",
143
+ "pages": ["<div>...</div>"],
144
+ "fields": [ /* optional — set meta.fields here too (see Step 5) instead of a separate call */ ]
145
+ }]
144
146
  }
145
147
  ```
146
148
 
@@ -44,14 +44,17 @@ Use `BOARD_ID` and `BASE_URL` from the `connect` skill. If a `boardId` argument
44
44
  mcp__eventmodelers__place_element {
45
45
  "boardId": "<BOARD_ID>",
46
46
  "timelineId": "<TIMELINE_ID>",
47
- "elementType": "<COMMAND|READMODEL|EVENT>",
48
- "title": "<title>",
49
- "columnIndex": <position, if given>,
50
- "fields": [{ "name": "orderId", "type": "String", "example": "ord-1" }]
47
+ "elements": [{
48
+ "elementType": "<COMMAND|READMODEL|EVENT>",
49
+ "title": "<title>",
50
+ "columnIndex": <position,
51
+ if given>,
52
+ "fields": [{ "name": "orderId", "type": "String", "example": "ord-1" }]
53
+ }]
51
54
  }
52
55
  ```
53
56
 
54
- Pass `fields` whenever the element's attributes are already known — they are written by this same call, so don't follow a placement with a `submit_node_events` just to set them. Placing **more than one** element is `place_elements` with an `elements` array (same per-entry options), applied in order so each entry sees the columns the previous one added — one call for a whole slice's or column run's worth instead of one per element.
57
+ Pass `fields` whenever the element's attributes are already known — they are written by this same call, so don't follow a placement with a `submit_node_events` just to set them. Placing **more than one** element is more entries in the same `elements` array, applied in order so each entry sees the columns the previous one added — one call for a whole slice's or column run's worth instead of one per element.
55
58
 
56
59
  This tool finds or creates an empty cell in the correct lane and places the node in one call — it collapses the "resolve timeline → fetch columns → determine lane → check occupancy → create node" sequence (Steps 2–3, 4, 6, 7b below) into a single round trip. A `columnIndex` past the timeline's current column count is handled automatically (columns are added to reach it) — no need to pre-check the column count or catch an out-of-range error yourself. If `timelineId` is unknown, resolve it first via Step 2's MCP call. Pass `compact: true` for a smaller `{nodeId, cellName, columnIndex}` response (plus `connectedCount` if auto-connect wired an edge) when you don't need the full `lane`/`elementType`/`title`/`autoConnected` detail back. Go straight to Step 8 once it returns.
57
60
 
@@ -274,13 +277,15 @@ mcp__eventmodelers__link_element { "boardId": "<BOARD_ID>", "nodeId": "<origin-n
274
277
  ```
275
278
  mcp__eventmodelers__create_screen {
276
279
  "boardId": "<BOARD_ID>",
277
- "contentType": "html",
278
- "nodeId": "<node-uuid>",
279
- "chapterId": "<TIMELINE_ID>",
280
- "cellId": "<CELL_ID>",
281
- "title": "<title>",
282
- "pages": ["<div>...</div>"],
283
- "description": "<title — what this screen shows>"
280
+ "screens": [{
281
+ "contentType": "html",
282
+ "nodeId": "<node-uuid>",
283
+ "chapterId": "<TIMELINE_ID>",
284
+ "cellId": "<CELL_ID>",
285
+ "title": "<title>",
286
+ "pages": ["<div>...</div>"],
287
+ "description": "<title — what this screen shows>"
288
+ }]
284
289
  }
285
290
  ```
286
291
 
@@ -293,13 +298,15 @@ mcp__eventmodelers__create_screen {
293
298
  ```
294
299
  mcp__eventmodelers__create_screen {
295
300
  "boardId": "<BOARD_ID>",
296
- "contentType": "sketch",
297
- "nodeId": "<node-uuid>",
298
- "chapterId": "<TIMELINE_ID>",
299
- "cellId": "<CELL_ID>",
300
- "title": "<title>",
301
- "elements": [...],
302
- "description": "<title — what this screen shows>"
301
+ "screens": [{
302
+ "contentType": "sketch",
303
+ "nodeId": "<node-uuid>",
304
+ "chapterId": "<TIMELINE_ID>",
305
+ "cellId": "<CELL_ID>",
306
+ "title": "<title>",
307
+ "elements": [...],
308
+ "description": "<title — what this screen shows>"
309
+ }]
303
310
  }
304
311
  ```
305
312
 
@@ -378,7 +385,7 @@ Count inbound edges where `target === COMMAND_NODE_ID` and the source node is ty
378
385
  - **2 or more** → keep the edge whose source sits in the COMMAND's own column (the deliberate, same-slice issuer) and remove every other one:
379
386
 
380
387
  ```
381
- mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<extra-issuer-node-id>", "target": "<COMMAND_NODE_ID>", "action": "remove" }
388
+ mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "connections": [{ "source": "<extra-issuer-node-id>", "target": "<COMMAND_NODE_ID>", "action": "remove" }] }
382
389
  ```
383
390
 
384
391
  If it's not clear which edge is the deliberate one (e.g. neither source sits in the COMMAND's own column), do not guess — leave both edges and post a comment on the COMMAND node via `handle-comment` instead, describing the ambiguity.
@@ -409,4 +416,4 @@ Timeline: <timelineId>
409
416
 
410
417
  ## Example — place an EVENT via curl
411
418
 
412
- A full worked example (add column → fetch chapter → create node) placing an EVENT called "Order Placed" at the end of a timeline lives in `references/api-fallback.md`'s "Full worked example" section. With MCP connected, the same result is one call: `mcp__eventmodelers__place_element { "boardId": "<BOARD_ID>", "timelineId": "<TIMELINE_ID>", "elementType": "EVENT", "title": "Order Placed" }` (see "Prefer MCP" above).
419
+ A full worked example (add column → fetch chapter → create node) placing an EVENT called "Order Placed" at the end of a timeline lives in `references/api-fallback.md`'s "Full worked example" section. With MCP connected, the same result is one call: `mcp__eventmodelers__place_element { "boardId": "<BOARD_ID>", "timelineId": "<TIMELINE_ID>", "elements": [{ "elementType": "EVENT", "title": "Order Placed" }] }` (see "Prefer MCP" above).
@@ -164,7 +164,7 @@ Response: `{ "hashes": { "<event-id>": "<hash>" } }` — keyed by the `id` you s
164
164
 
165
165
  ## Full worked example — place an EVENT via curl, start to finish
166
166
 
167
- With MCP connected, the same result is one call: `mcp__eventmodelers__place_element { "boardId": "<BOARD_ID>", "timelineId": "<TIMELINE_ID>", "elementType": "EVENT", "title": "Order Placed" }`.
167
+ With MCP connected, the same result is one call: `mcp__eventmodelers__place_element { "boardId": "<BOARD_ID>", "timelineId": "<TIMELINE_ID>", "elements": [{ "elementType": "EVENT", "title": "Order Placed" }] }`.
168
168
 
169
169
  Placing an EVENT called "Order Placed" at the end of a timeline, over curl:
170
170
 
@@ -179,13 +179,15 @@ Build the payload, then send a single call that creates the screen node, places
179
179
  ```
180
180
  mcp__eventmodelers__create_screen {
181
181
  "boardId": "<BOARD_ID>",
182
- "contentType": "html",
183
- "nodeId": "<SCREEN_NODE_ID>",
184
- "chapterId": "<CHAPTER_ID>",
185
- "cellId": "<actorCellId>",
186
- "title": "<screenTitle>",
187
- "pages": ["<div>...</div>"],
188
- "description": "<screenTitle — what this screen shows>"
182
+ "screens": [{
183
+ "contentType": "html",
184
+ "nodeId": "<SCREEN_NODE_ID>",
185
+ "chapterId": "<CHAPTER_ID>",
186
+ "cellId": "<actorCellId>",
187
+ "title": "<screenTitle>",
188
+ "pages": ["<div>...</div>"],
189
+ "description": "<screenTitle — what this screen shows>"
190
+ }]
189
191
  }
190
192
  ```
191
193
 
@@ -198,13 +200,15 @@ mcp__eventmodelers__create_screen {
198
200
  ```
199
201
  mcp__eventmodelers__create_screen {
200
202
  "boardId": "<BOARD_ID>",
201
- "contentType": "sketch",
202
- "nodeId": "<SCREEN_NODE_ID>",
203
- "chapterId": "<CHAPTER_ID>",
204
- "cellId": "<actorCellId>",
205
- "title": "<screenTitle>",
206
- "elements": [...],
207
- "description": "<screenTitle — what this screen shows>"
203
+ "screens": [{
204
+ "contentType": "sketch",
205
+ "nodeId": "<SCREEN_NODE_ID>",
206
+ "chapterId": "<CHAPTER_ID>",
207
+ "cellId": "<actorCellId>",
208
+ "title": "<screenTitle>",
209
+ "elements": [...],
210
+ "description": "<screenTitle — what this screen shows>"
211
+ }]
208
212
  }
209
213
  ```
210
214
 
@@ -191,7 +191,7 @@ emptyColumns = [columnId, ...] // in column order, ready to reuse
191
191
 
192
192
  **Prefer MCP:**
193
193
  ```
194
- mcp__eventmodelers__delete_column { "boardId": "<BOARD_ID>", "timelineId": "<CHAPTER_ID>", "columnId": "<columnId>" }
194
+ mcp__eventmodelers__delete_column { "boardId": "<BOARD_ID>", "timelineId": "<CHAPTER_ID>", "columnIds": ["<columnId>"] }
195
195
  ```
196
196
 
197
197
  **Fallback (no MCP):** see `references/api-fallback.md` — "Delete a column".
@@ -260,7 +260,7 @@ mcp__eventmodelers__submit_node_events {
260
260
 
261
261
  **Prefer MCP:** this whole "find/create a column, compute the cell, place the node" sequence collapses into one call:
262
262
  ```
263
- mcp__eventmodelers__place_element { "boardId": "<BOARD_ID>", "timelineId": "<CHAPTER_ID>", "elementType": "EVENT", "title": "<EventName>", "columnIndex": <index> }
263
+ mcp__eventmodelers__place_element { "boardId": "<BOARD_ID>", "timelineId": "<CHAPTER_ID>", "elements": [{ "elementType": "EVENT", "title": "<EventName>", "columnIndex": <index> }] }
264
264
  ```
265
265
  Extract `nodeId` and `columnId` directly from the tool result.
266
266
 
@@ -319,7 +319,7 @@ Two steps — delete the node, then delete the column:
319
319
 
320
320
  **Prefer MCP:**
321
321
  ```
322
- mcp__eventmodelers__delete_node { "boardId": "<BOARD_ID>", "nodeId": "<eventNodeId>" }
322
+ mcp__eventmodelers__delete_node { "boardId": "<BOARD_ID>", "nodeIds": ["<eventNodeId>"] }
323
323
  ```
324
324
 
325
325
  **Fallback (no MCP):** see `references/api-fallback.md` — "Delete an event node".
@@ -328,7 +328,7 @@ mcp__eventmodelers__delete_node { "boardId": "<BOARD_ID>", "nodeId": "<eventNode
328
328
 
329
329
  **Prefer MCP:**
330
330
  ```
331
- mcp__eventmodelers__delete_column { "boardId": "<BOARD_ID>", "timelineId": "<CHAPTER_ID>", "columnId": "<columnId>" }
331
+ mcp__eventmodelers__delete_column { "boardId": "<BOARD_ID>", "timelineId": "<CHAPTER_ID>", "columnIds": ["<columnId>"] }
332
332
  ```
333
333
 
334
334
  **Fallback (no MCP):** see `references/api-fallback.md` — "Delete a column".
@@ -138,7 +138,7 @@ Use the `handle-comment` skill with `action=place` to post each comment. Pass:
138
138
  - `type` — `COMMENT` (there is no separate question type — the text itself carries the question)
139
139
  - `author` — `wdyt`
140
140
 
141
- Post them together, not one at a time: `handle-comment` sends every comment of a run in a single batch request (`add_comments` over MCP, `POST .../boards/:boardId/comments` over REST).
141
+ Post them together, not one at a time: `handle-comment` sends every comment of a run in a single batch request (one `add_comment` call over MCP, `POST .../boards/:boardId/comments` over REST).
142
142
 
143
143
  Only post questions that are **genuinely unclear or missing** — don't post observations that are clearly intentional design decisions.
144
144
 
@@ -153,18 +153,30 @@ There are two kinds — no text-callout kind; a drawing never carries the questi
153
153
  - **Arrow** (`kind: "path"`, `arrowEnd: true`) — the concern is about a missing or unclear relationship *between two elements* (e.g. "does this event actually reach this automation?"). Draw a straight line from one element's position to the other's. `path` is `M 0 0 L <dx> <dy>` in the box's own local coordinates; `x`/`y`/`width`/`height` describe that box in canvas space (so `width`/`height` = the delta between the two elements' positions).
154
154
  ```
155
155
  mcp__eventmodelers__create_drawing {
156
- "boardId": "$BOARD_ID", "kind": "path",
157
- "x": <sourceX>, "y": <sourceY>, "width": <dx>, "height": <dy>,
158
- "path": "M 0 0 L <dx> <dy>", "arrowEnd": true
156
+ "boardId": "$BOARD_ID",
157
+ "drawings": [{
158
+ "kind": "path",
159
+ "x": <sourceX>,
160
+ "y": <sourceY>,
161
+ "width": <dx>,
162
+ "height": <dy>,
163
+ "path": "M 0 0 L <dx> <dy>",
164
+ "arrowEnd": true
165
+ }]
159
166
  }
160
167
  ```
161
168
  Get element positions from the slice data already loaded in Step 2. If a position is missing, fetch the nodes you need in **one** call — `mcp__eventmodelers__get_nodes { "boardId": "$BOARD_ID", "nodeIds": [<the ids>] }` — not `get_node` per element.
162
169
  - **Group loop** (`kind: "rect"`, drawn around a computed bounding box) — the concern spans a *cluster* of elements together (e.g. "this whole flow assumes nothing ever fails"). There's no dedicated group endpoint — union the elements' own `x`/`y`/`width`/`height` (plus some padding) yourself and draw one `rect` around that box:
163
170
  ```
164
171
  mcp__eventmodelers__create_drawing {
165
- "boardId": "$BOARD_ID", "kind": "rect",
166
- "x": <minX - pad>, "y": <minY - pad>,
167
- "width": <maxX - minX + 2*pad>, "height": <maxY - minY + 2*pad>
172
+ "boardId": "$BOARD_ID",
173
+ "drawings": [{
174
+ "kind": "rect",
175
+ "x": <minX - pad>,
176
+ "y": <minY - pad>,
177
+ "width": <maxX - minX + 2*pad>,
178
+ "height": <maxY - minY + 2*pad>
179
+ }]
168
180
  }
169
181
  ```
170
182
  This is a visual grouping only — unrelated to the `MODEL_CONTEXT` node type; never touch a `modelContext` field to satisfy this.