# Nebulit Event Modeling > Nebulit Event Modeling is an agentic, real-time canvas for designing event-driven and traditionally-structured systems. Model a business process by voice or text, let AI generate wireframes/commands/read models/specifications, and connect a coding agent (via CLI, REST API, or MCP) that turns finished slices directly into tested, committed code across multiple backend stacks. ## Features ### Features **TL;DR:** Connect an agent to your board, then either model live or build the code — see **Connect Agent** below for the full walkthrough. ``` npx @eventmodelers/cli init --stack # connect — full backend stack, for building code npx @eventmodelers/cli init --modeling # connect — skills + agent only, no backend, for modeling npx @eventmodelers/cli run --modeling # start modeling — event storming, timelines, wireframes, live on the board npx @eventmodelers/cli run # start building — implements slices into real code as they're marked ready ``` Nebulit Event Modeling is an agentic collaborative canvas for designing event-driven and traditionally-structured systems. It combines visual modeling with AI assistance and multi-user collaboration. **Try a live preview** — Explore a sample board loaded in read-only in-memory mode — no login required. (Live Preview button) The platform provides: - **Event Modeling Canvas** — Drag-and-drop canvas with Events, Commands, Read Models, Automations, Screens, and Model Contexts organized on timelines. - **Real-time Collaboration** — Multiple users can model together in real time using cloud-backed boards. Changes sync instantly across all connected clients. - **AI-Powered Modeling** — Describe a business workflow in plain text and let AI generate a complete event model including events, commands, slices, and specifications. - **Code Generation** — Generate boilerplate code directly from your model. Supports multiple languages and frameworks through configurable code templates. - **Multiple Storage Backends** — Choose between localStorage, IndexedDB, in-memory, or cloud storage. Switch backends without losing your boards. - **Timeline Focus Mode** — Zoom into a single timeline for a focused editing experience, then return to the full board view. - **Given/When/Then Specs** — Attach behavioral specifications to any element. AI can generate scenarios from your model context automatically. - **Context Map** — Automatic context map generation from Model Context elements on the board showing bounded contexts and their relationships. - **Screen Design** — Sketch UI mockups for screen elements directly on the canvas using AI-assisted design generation, or render real HTML/CSS mockups onto HTML Screen nodes when you need a coded-looking prototype. ## Model As You Speak ### Model As You Speak *Dennis opened a fresh chapter before a client call and just left the mic running. As the client walked through their onboarding flow out loud — "so first they sign up, then we send a verification email, then once they click it their account activates" — events landed on the timeline one after another: UserSignedUp, VerificationEmailSent, AccountActivated. He never stopped the conversation to type anything; by the time the call ended the chapter already had a rough timeline to clean up together instead of a blank canvas to start from.* The board has a built-in voice mode: activate a chapter, talk through what you're thinking, and a connected agent turns your words into live changes on the timeline — no typing, no switching windows. It's one of the fastest ways to get a rough model down while a conversation is still happening. **Prerequisite:** Voice mode needs a connected agent — install the **Eventmodelers CLI** in modeling mode and keep the agent process running. See **Connect Agent** for full setup details. **1. Install the modeling kit** Install the Agent Modeling Kit in your project, the same as any other agentic modeling setup: ``` npx @eventmodelers/cli init --modeling ``` **2. Run the agent** Start the long-lived agent process that listens for prompts sent from the board. Until an agent is running, the **Prompts** panel shows **no agent is connected** — you can still type or record prompts, they'll just wait until an agent picks them up. ``` npx @eventmodelers/cli run --modeling ``` *(Screenshot: Prompts panel showing no agent is connected)* **3. Confirm the agent is connected** As soon as the agent process connects to the platform, the panel switches to a green **agent connected** badge next to a **modeling** tag. *(Screenshot: Prompts panel showing agent connected)* **4. Activate a chapter** Voice mode always targets exactly one chapter at a time. Activate one either from the **Chapter** dropdown in the Prompts panel, or by activating it directly in the Chapter Toolbar (the microphone icon). *(Screenshots: Selecting a chapter from the Chapter dropdown; Activating a chapter from the Chapter Toolbar)* **5. Record your instructions** With a chapter active, click the microphone and just talk — describe events, ask for a screen, think out loud about an edge case. A recording timer confirms it's listening. *(Screenshot: Recording indicator with a running timer)* **6. Or type it manually** Prefer typing? Add the same instructions manually in the prompt field instead of recording. *(Screenshot: Typing a prompt manually into the Prompts panel)* **7. Turn a comment into an instruction** Any comment on any node can become an agent instruction — click the robot icon on the comment. This is a quick way to hand off small, targeted asks, like adding examples to elements that already have their fields defined. *(Screenshot: Comment popup with a robot icon to turn it into an agent instruction)* **Note:** Voice mode runs on the browser's Web Speech API, so for now it's only reliable in **Google Chrome**. Full support for other browsers is on the way — until then, typed prompts and comment-to-agent both work everywhere. ## Usage ### Usage *Marc wanted to map out his team's order flow before the sprint kicked off. He opened Nebulit, clicked **New**, typed "Order Management", and hit Enter. He switched storage to **Cloud** so his colleague could join live, then right-clicked the canvas and dropped an Event into the first timeline cell. Within ten minutes he had a rough flow on the board — no setup, no shared doc, just a live canvas.* This guide walks through the core workflows in Nebulit Event Modeling. #### Creating a Board Open the application and click **New** in the left sidebar to create a board. Give it a name and press Enter. You can rename or delete boards at any time using the sidebar controls. #### Storage Backend Click the storage indicator (top-left of the sidebar) to switch between backends: - **LocalStorage** — Persists in browser storage. Data survives page reloads. - **IndexedDB** — Larger capacity local storage, suitable for big boards. - **In-Memory** — Temporary. Data is lost on page reload. Useful for demos. - **Cloud** — Requires login. Enables real-time collaboration and cross-device sync. #### Working with Elements Elements are placed inside **timelines**. A timeline represents a slice of the system workflow. Right-click the canvas to open the element palette and choose from: - **Event** — Something that happened in the system (orange). - **Command** — An intent to change state (blue). - **Read Model** — A query/projection (green). - **Automation** — An automated process triggered by events (purple). - **Screen** — A UI screen or view (pink). - **HTML Screen** — A real, styled HTML/CSS mockup (one or more pages) instead of a wireframe sketch — use it when you want a coded-looking prototype rather than a sketchnote-style Screen. - **Model Context** — A bounded context grouping related slices. #### Timelines Timelines are horizontal containers that group related elements into a flow. Create a timeline from the element palette, then drag elements into its cells. Click the focus icon on a timeline to enter focused editing mode. #### Copy, Paste & Duplicate Select one or more nodes (including timelines) and use `Ctrl+C` / `Ctrl+V` to copy and paste. Pasted timelines bring all their child elements along, with cell references automatically remapped. Use the context menu to duplicate a single element in place. #### Context Map When your board contains Model Context elements, the **Context Map** button appears in the top-right corner of the canvas. Click it to view a generated diagram of your bounded contexts and their relationships. #### Keyboard Shortcuts Press `?` at any time on the canvas to open the shortcuts panel. The most important bindings are listed below. **General** | Key | Action | Description | |---|---|---| | `?` | Show keyboard shortcuts | Opens the shortcuts help panel. | | `/` | Open quick access | Opens the quick access menu. | | `/ → T` | New Chapter | Opens quick access and creates a new timeline chapter. | | `/ → D` | Draw Screen | Opens quick access and opens the screen designer. | | `Tab` | Cycle timelines | Selects the next timeline (left to right) and zooms to it. | | `+` | Zoom in | Zooms into the canvas. | | `-` | Zoom out | Zooms out of the canvas. | | `↑ ↓ ← →` | Pan canvas | When nothing is selected, pans the canvas. | | `Ctrl+P` | Show all edges | Reveals all connections on the canvas. | **Editing** | Key | Action | Description | |---|---|---| | `Ctrl+Enter` | Open inline editor | Select a node to edit its content. | | `Backspace / Delete` | Delete selected nodes | Select one or more nodes first. | | `D` | Clear cell | Select any cell to remove its content. | | `Ctrl+E` | Eject node from cell | Ejects the node out of its cell onto the canvas. | | `Ctrl+-` | Remove column | Select a cell to remove its entire column. | | `Shift+=` | Open formula modal | Opens the formula editor for the selected cell. | | `Z` | Preview screen | On a selected screen node (image or HTML), shows it full-screen. Escape closes it. | **Navigation** | Key | Action | Description | |---|---|---| | `↑ ↓ ← →` | Navigate cells | Move between cells inside a timeline. | | `Enter` | Select node / Open spec | Select a cell to select its node or open a spec node. | | `Escape` | Deselect / Exit / Close | Deselects the selected cell. Also closes modals and exits focus mode. | | `← →` | Page through spec scenarios | Navigate between scenario pages on a spec node. | | `1 – 9` | Pin/open specification by number | First press pins the hover. Press again to open the spec modal. | **Adding Elements** | Key | Action | Description | |---|---|---| | `E` | Add Event | Select an empty cell in a swimlane row. | | `C` | Add Command | Select an empty cell in an interaction row. | | `R` | Add Read Model | Select an empty cell in an interaction row. | | `S` | Add Specification / HTML Screen / Markdown | Spec lane → specification. Actor row → HTML screen. Feedback row → markdown note. | | `A` | Add Automation | Select an empty cell in an actor row. | **Linking** | Key | Action | Description | |---|---|---| | `Space` | Select / Deselect link source | Mark a cell as the link source. Press again on the same cell to deselect. | | `L` | Create / Remove link | After selecting a source with Space, navigate to the target and press L. | | `Ctrl+Space` | Remove incoming edges | Removes all incoming connections from the selected cell. | | `Ctrl+C` | Copy fields from highlighted cell | Mark source with Space, navigate to target, then press Ctrl+C. | **Spec Editor** | Key | Action | Description | |---|---|---| | `G / W / T` | Switch Given / When / Then | Jump to that section inside the scenario editor. | ## REST API ### REST API *Marc's team spun up a new microservice every few weeks. He was tired of creating boards by hand each time, so he grabbed an API token from Settings, wrote a small CI script that called **POST /api/boards** on every new repository, and invited the team via **POST /api/org/:orga_id/invitations**. From then on, a fresh board with the right members was waiting before anyone wrote a single line of code.* The Nebulit backend exposes a REST API for managing tokens, organizations, licenses, boards, and invitations. All endpoints require a Bearer token obtained from the account settings unless noted otherwise. #### Authentication Include your API token in every request as a Bearer token: ``` Authorization: Bearer ``` #### Organizations ##### POST /api/org Create a new organization. The authenticated user becomes the owner. One organization per user. **Request body:** ``` { "name": "Acme Corp" } ``` **Response:** ``` { "orga_id": "uuid", "name": "Acme Corp", "owner_user_id": "user-uuid", "created_at": "2024-01-10T08:00:00Z" } ``` ##### GET /api/org/:user_id List all organizations a user belongs to (owned or member). **Response:** ``` [ { "orga_id": "uuid", "name": "Acme Corp", "owner_user_id": "user-uuid", "created_at": "2024-01-10T08:00:00Z" } ] ``` ##### POST /api/org/:orga_id/members Add the authenticated user to an organization as a member. **Request body:** ``` { "user_id": "user-uuid" } ``` #### Tokens ##### GET /api/org/:orga_id/tokens List all API tokens for the authenticated user in an organization. **Response:** ``` [ { "id": "uuid", "description": "CI/CD pipeline token", "organization_id": "uuid", "issued_at": "2024-01-15T10:30:00Z" } ] ``` ##### POST /api/org/:orga_id/tokens Issue a new API token for the organization. The id field is the token value — store it securely, it will not be shown again. **Request body:** ``` { "description": "CI pipeline" // optional } ``` **Response:** ``` { "id": "uuid", // this is the token value "description": "CI pipeline", "organization_id": "uuid", "issued_at": "2024-01-15T10:30:00Z" } ``` ##### DELETE /api/org/:orga_id/tokens/:id Revoke an API token. #### License ##### POST /api/org/:orga_id/license Activate a commercial license for an organization. **Request body:** ``` { "activation_date": "2024-01-01T00:00:00Z", "expiration_date": "2025-01-01T00:00:00Z", "seats": 10, "company_license": false // optional } ``` **Response:** ``` { "id": "uuid", "orga_id": "uuid", "activation_date": "2024-01-01T00:00:00Z", "expiration_date": "2025-01-01T00:00:00Z", "active": true, "seats": 10, "free_seats": 10, "company_license": false, "trial_enabled": false, "created_at": "2024-01-01T00:00:00Z" } ``` ##### GET /api/org/:orga_id/license/active Check whether an organization has an active, non-expired license. **Response:** ``` { "active": true, "description": "Trial active until January 1, 2025. 3 seats available." // optional } ``` ##### GET /api/org/:orga_id/license/trial Check whether a trial license is available for an organization. **Response:** ``` { "enabled": true } ``` ##### POST /api/org/:orga_id/license/trial Activate a 7-day trial license for an organization. Returns 409 if an active license already exists. **Response:** ``` { "id": "uuid", "orga_id": "uuid", "active": true, "seats": 3, "trial_enabled": true, "expiration_date": "2024-01-08T00:00:00Z" } ``` #### Boards ##### GET /api/boards List all boards accessible to the authenticated user (filtered by organization when using an API token). **Response:** ``` [ { "board_id": "uuid", "board_name": "Order Management", "created_at": "2024-01-10T08:00:00Z", "organization_id": "uuid" } ] ``` ##### POST /api/org/:orgId/boards/:boardId/events Create a board by posting a board:created event. Also used to persist any board-level events (timeline rows, cells). The boardId is a client-generated UUID. **Request body:** ``` [ { "eventType": "board:created", "boardId": "your-new-board-uuid", "board_name": "Payment Processing" } ] ``` **Response:** ``` { "ok": true } ``` ##### DELETE /api/org/:orgId/boards/:boardId Delete a board and all its data permanently. ##### GET /api/org/:orgId/boards/:boardId/events Return all persisted board events in sequence order. Useful for replaying board state or auditing history. **Response:** ``` [ { "id": "event-uuid", "eventType": "board:created", "boardId": "board-uuid", "board_name": "Order Management", "timestamp": 1712920000000 } ] ``` ##### GET /api/org/:orgId/boards/:boardId/events/search Search board events by node name. Pass ?name=OrderPlaced to find events whose title contains the query. **Response:** ``` [ { "id": "node-uuid", "title": "OrderPlaced", "type": "EVENT" } ] ``` ##### PUT /api/org/:orgId/boards/:boardId/organization Assign a board to an organization. The authenticated user must own the board. **Request body:** ``` { "organizationId": "org-uuid" } ``` ##### DELETE /api/org/:orgId/boards/:boardId/organization Unassign a board from its organization. The authenticated user must own the board. #### Nodes Nodes are the individual elements on a board (EVENTs, COMMANDs, READMODELs, SCREENs, etc.). Node data and metadata are patched separately — only send the fields that changed. ##### GET /api/org/:orgId/boards/:boardId/nodes List all nodes on a board. Optionally filter by element type using ?type=EVENT. **Response:** ``` [ { "id": "node-uuid", "type": "EVENT", "node": { "id": "node-uuid", "data": { "title": "OrderPlaced", "type": "EVENT" } }, "meta": { "type": "EVENT", "title": "OrderPlaced", "fields": [] }, "hash": "a3f1c2d4", "version": 3 } ] ``` ##### GET /api/org/:orgId/boards/:boardId/nodes/:nodeId Get a single node by ID. Returns 404 when the node does not exist. ##### POST /api/org/:orgId/boards/:boardId/nodes/events Submit an array of node change events (create, update, or delete). node and meta are shallow-patched — only send changed fields. The server resolves node:created vs node:changed from DB state. Any node:created event carrying a chapterId plus cellId/cellName (i.e. placing a node on a timeline) also triggers a best-effort, fire-and-forget auto-connect to type-compatible neighbors — same rules as POST /connections. **Request body:** ``` [ { "id": "event-uuid", "eventType": "node:changed", "nodeId": "node-uuid", "boardId": "board-uuid", "timestamp": 1712920000000, "changedAttributes": ["meta.title"], "meta": { "type": "COMMAND", "title": "PlaceOrder" }, "edges": [ { "id": "edge-1", "source": "node-uuid", "target": "other-node-uuid" } ] } ] ``` **Response:** ``` { "hashes": { "event-uuid": "a3f1c2d4e5b6" } } ``` ##### POST /api/org/:orgId/boards/:boardId/nodes/:nodeId/auto-connect Auto-connect a node to its timeline neighbors. Looks at the node's own timeline column and the previous column, and creates edge:added events to every type-compatible neighbor found there (same rules as POST /connections). Neighbors that aren't a legal pair, or that already have an edge, are reported in skipped/connected rather than causing an error. A node that isn't placed on a timeline, or isn't a connectable element type, returns an empty result. **Response:** ``` { "connected": [ { "edgeId": "edge-uuid", "source": "node-uuid", "target": "other-node-uuid", "created": true } ], "skipped": [ { "nodeId": "other-node-uuid", "reason": "incompatible-type" } ] } ``` ##### POST /api/org/:orgId/boards/:boardId/connections Create a directed edge between two nodes. Allowed pairs: COMMAND→EVENT, SCREEN→COMMAND, EVENT→READMODEL, READMODEL→SCREEN, READMODEL→AUTOMATION, AUTOMATION→COMMAND. Both nodes must belong to the same timeline. **Request body:** ``` { "source": "node-uuid", "target": "node-uuid" } ``` **Response:** ``` { "id": "edge-uuid", "source": "node-uuid", "target": "node-uuid", "created": true } ``` ##### DELETE /api/org/:orgId/boards/:boardId/connections Remove the edge between two nodes. Direction is checked both ways. Returns removed=false without error if no edge exists. **Request body:** ``` { "source": "node-uuid", "target": "node-uuid" } ``` **Response:** ``` { "removed": true } ``` #### Comments ##### GET /api/org/:orgId/boards/:boardId/nodes/:nodeId/comments Return all comments attached to a node. **Response:** ``` [ { "id": "comment-uuid", "nodeId": "node-uuid", "boardId": "board-uuid", "text": "Should this be split into two slices?", "type": "COMMENT", "resolved": false, "timestamp": 1712920000000, "author": "marc@example.com" } ] ``` ##### POST /api/org/:orgId/boards/:boardId/nodes/:nodeId/comments Add a comment to a node. type defaults to COMMENT; use TASK for actionable items. **Request body:** ``` { "text": "Should this be split into two slices?", "type": "COMMENT", // COMMENT | TASK (default: COMMENT) "author": "marc@example.com" // optional } ``` **Response:** ``` { "id": "comment-uuid" } ``` ##### POST /api/org/:orgId/boards/:boardId/nodes/:nodeId/comments/:commentId/resolve Mark a comment as resolved. Returns 204 on success. ##### DELETE /api/org/:orgId/boards/:boardId/nodes/:nodeId/comments/:commentId Delete a comment permanently. Returns 204 on success. #### Timelines & Chapters Chapters are the top-level timeline containers on a board. Each chapter holds columns (vertical slices) and lanes (horizontal rows). ##### POST /api/org/:orgId/boards/:boardId/chapters Create a new timeline chapter on the board at an optional canvas position. Omitting position auto-stacks it below the lowest existing chapter, sized to each chapter's actual current row-height total — not the height it was created with — so it stays safe even after add_lane/add_column growth. **Request body:** ``` { "position": { "x": 100, "y": 200 } // optional } ``` ##### GET /api/org/:orgId/boards/:boardId/chapters/bounds Absolute canvas bounding box of every chapter on the board. width/height are derived from each chapter's current row/column layout, not a fixed default — use this to pick x/y for a new chapter (e.g. below the chapter with the largest y + height) without risking an overlap once chapters grow. **Response:** ``` { "chapters": [ { "id": "chapter-uuid", "title": "Checkout", "x": 0, "y": 0, "width": 900, "height": 1280 } ] } ``` ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/columns Add a column to a timeline. Pass index to insert at a specific position (0 = prepend, omit = append). **Request body:** ``` { "index": 2 // optional } ``` ##### DELETE /api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId Remove a column from a timeline. Returns 400 if it is the last column. ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/lanes Add a lane (row) to a timeline. **Request body:** ``` { "type": "actor", // actor | interaction | swimlane | spec | feedback "label": "User", // optional "index": 0 // optional insertion position } ``` ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/cells/:cellId/drop Place an existing node into a specific cell. Returns 400 if the cell is occupied or the placement violates lane rules. **Request body:** ``` { "nodeId": "node-uuid", "nodeType": "EVENT" } ``` ##### DELETE /api/org/:orgId/boards/:boardId/timelines/:timelineId/lanes/:rowId Remove a lane (row) from a timeline. Returns 400 if it is the last lane. ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId/move Reorder a column within its timeline. **Request body:** ``` { "toIndex": 2 } ``` ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/lanes/:rowId/move Reorder a lane (row) within its timeline. **Request body:** ``` { "toIndex": 1 } ``` ##### DELETE /api/org/:orgId/boards/:boardId/timelines/:timelineId/cells/:cellId Clear a cell — removes the placed node from the cell and deletes it. ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/nodes/move Move a node from its current cell to another cell in the same timeline. **Request body:** ``` { "movedNodeId": "node-uuid", "toCellId": "cell-uuid" } ``` ##### PUT /api/org/:orgId/boards/:boardId/timelines/:timelineId/position Update the canvas (x, y) position of a timeline node. **Request body:** ``` { "x": 400, "y": 120 } ``` #### Slices ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/slices Create a complete slice (1 column + matching nodes) inside a timeline. state-change = Command+Event+Spec, state-view = ReadModel+Event+Spec, automation = Automation+Event. **Request body:** ``` { "type": "state-change", // state-change | state-view | automation "index": 2 // optional column insertion index } ``` **Response:** ``` { "columnId": "col-uuid", "nodes": { "command": "node-uuid", "event": "node-uuid", "spec": "node-uuid" } } ``` #### Slice Data Read-only endpoints that project board nodes into a structured element graph — useful for code generation, analysis, and CI pipelines. Both endpoints require a Bearer token and board access. ##### GET /api/org/:orgId/boards/:boardId/slicedata/slices List all slices on a board with their id, title, status, and context. Pass contextName to return only slices belonging to that MODEL_CONTEXT. **Response:** ``` { "slices": [ { "id": "node-uuid", "title": "PlaceOrder", "status": "InProgress", "contextId": "ctx-uuid", "contextName": "Order Management" } ] } ``` ##### GET /api/org/:orgId/boards/:boardId/slicedata Build the full element graph for a context — events, commands, read models, screens, automations, specs, and their dependencies. Pass contextId, or contextName matched against MODEL_CONTEXT titles first and timeline titles second (a timeline with no assigned/connected context scopes the result to just that timeline). Optionally filter to a single slice with sliceId. Pass format (default json) to encode the result as yaml, textual (a compact AI-prompt-friendly markdown dump), toon (token-efficient tabular encoding), emlang, or esdm (https://www.esdm.io) instead — same six formats offered in the board's Code view. json is returned as-is; every other format is a transform of it, with the response Content-Type and body changed to match (a raw encoded string, not another JSON envelope). esdm's domain/bounded-context naming is derived entirely from the resolved context/timeline — there's no separate parameter for it. **Response:** ``` { "slices": [ { "id": "node-uuid", "title": "PlaceOrder", "status": "InProgress", "sliceType": "STATE_CHANGE", "chapter": "Checkout", "context": "Order Management", "commands": [ { "id": "...", "title": "PlaceOrder", "type": "COMMAND", "fields": [], "dependencies": [] } ], "events": [ { "id": "...", "title": "OrderPlaced", "type": "EVENT", "fields": [], "dependencies": [] } ], "readmodels": [], "screens": [], "processors": [], "tables": [], "specifications": [], "aggregates": ["order"] } ] } ``` #### Specifications Attach Given/When/Then scenarios to a timeline column. The spec node is created automatically if one does not exist yet. ##### GET /api/org/:orgId/boards/:boardId/timelines/:timelineId/spec-info Return all valid EVENTs, COMMANDs, and READMODELs in a timeline — the elements that may legally appear as given/when/then steps when building specifications. **Response:** ``` { "events": [ { "id": "node-uuid", "title": "OrderPlaced" } ], "commands": [ { "id": "node-uuid", "title": "PlaceOrder" } ], "readmodels": [ { "id": "node-uuid", "title": "OrderSummary" } ] } ``` ##### POST /api/org/:orgId/boards/:boardId/timelines/:timelineId/columns/:columnId/scenarios Append one or more GWT scenarios to a column's spec node. Accepts a single scenario object or an array. The spec node is auto-created if missing. Returns 409 on duplicate title. **Request body:** ``` [ { "id": "scenario-uuid", "title": "Successful order placement", "given": [ { "id": "event-uuid", "title": "CartConfirmed", "type": "EVENT", "fields": [] } ], "when": [ { "id": "cmd-uuid", "title": "PlaceOrder", "type": "COMMAND", "fields": [] } ], "then": [ { "id": "event-uuid2","title": "OrderPlaced", "type": "EVENT", "fields": [] } ] } ] ``` **Response:** ``` { "specNodeId": "node-uuid", "added": 1, "isNewNode": false, "scenarios": [ { "id": "scenario-uuid", "title": "Successful order placement" } ] } ``` #### Images Upload and manage images attached to SCREEN nodes. All image endpoints use multipart/form-data unless noted. ##### POST /api/org/:orgId/boards/:boardId/images/:imageId Upload a file to replace an existing image (e.g. a screen node's image). Fires node:changed. imageId is the node UUID. **Request body:** ``` multipart/form-data file: // required ``` ##### POST /api/org/:orgId/boards/:boardId/imagesnapshots/:imageId Upload a snapshot variant of an image (stored separately from the main image). Fires node:changed. **Request body:** ``` multipart/form-data file: // required ``` ##### POST /api/org/:orgId/boards/:boardId/image-nodes/:nodeId Create a new SCREEN image node from a file upload. Uploads the image and fires node:created. **Request body:** ``` multipart/form-data file: // required chapterId: "uuid" // required — timeline the node belongs to cellName: "col-uuid" // required — cell within the timeline ``` ##### POST /api/org/:orgId/boards/:boardId/images/:imageId/sketch Generate a webp UI sketch from a structured grid description (JSON) and upload it as the node's image. Accepts the same shape as the AI screenDesignInstructions prompt — an elements array on a 50×40 grid. **Request body:** ``` { "elements": [ { "type": "rect", "x": 0, "y": 0, "w": 50, "h": 5, "label": "Header" }, { "type": "button", "x": 2, "y": 7, "w": 10, "h": 3, "label": "Submit" } ], "description": "Checkout screen" // optional — stored as node meta.description } ``` ##### POST /api/org/:orgId/boards/:boardId/image-nodes/:nodeId/sketch Same as the image sketch endpoint, but also creates the node if it does not exist yet. Requires chapterId and cellName in the JSON body alongside elements. **Request body:** ``` { "elements": [ ... ], "chapterId": "uuid", "cellName": "col-uuid", "description": "Order confirmation screen" } ``` #### HTML Screens HTML Screen nodes render one or more real HTML/CSS pages instead of a wireframe sketch — useful for a coded-looking prototype. Each page is a standalone body-only fragment; the server strips `