# OrganizaOne Draw — Diagram Generation Spec for AI Agents OrganizaOne Draw (https://draw.organizaone.app/) is an offline-first diagram editor. You (an AI agent) can generate a complete diagram and deliver it as a single URL. When the user opens the URL, the editor loads the diagram instantly — no import steps. ## 1. The deep link ``` https://draw.organizaone.app/#odraw= ``` `` is **base64url** of the odraw JSON document, either: - **gzipped** (recommended — URLs get ~5x shorter), or - **plain UTF-8 JSON** (accepted as a fallback; auto-detected). base64url = standard base64 with `+` → `-`, `/` → `_`, and `=` padding stripped. The data lives in the URL fragment (`#`), so it is never sent to any server. If the user already has a diagram on the canvas, the app asks for confirmation before replacing it. **Size guidance:** keep the final URL under ~8,000 characters so it survives pasting into chats and emails. If the diagram is bigger, save the gzipped JSON as a file with the `.odraw` extension instead and tell the user to open it via Menu → Open (same JSON, gzip-compressed — exactly what the payload contains). ### Building the payload Node.js: ```js import { gzipSync } from 'node:zlib'; const payload = gzipSync(Buffer.from(JSON.stringify(diagram))).toString('base64url'); const url = `https://draw.organizaone.app/#odraw=${payload}`; ``` Python: ```python import gzip, base64, json payload = base64.urlsafe_b64encode(gzip.compress(json.dumps(diagram).encode())).decode().rstrip('=') url = f"https://draw.organizaone.app/#odraw={payload}" ``` No gzip available? Encode the raw JSON (works, just longer): ```js const b64 = btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify(diagram)))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); ``` ## 2. The odraw JSON document ```json { "format": "odraw", "formatVersion": 2, "schemaVersion": 1, "elements": [], "arrows": [], "viewport": { "x": 0, "y": 0, "zoom": 1 } } ``` - `format: "odraw"`, `elements` and `arrows` are **required**. - `viewport` is **optional — omit it**: the editor auto-fits the content on screen. - Coordinates are in canvas units (think CSS px at zoom 1). Origin is top-left, x grows right, y grows down. `x`/`y` are always the **top-left corner** of the element. ## 3. Elements Every element needs a unique string `id` (UUIDs recommended) and `type`, `x`, `y`. Common optional fields (all element types): | Field | Type | Notes | |---|---|---| | `color` | hex string | text/border color | | `bgColor` | hex string | fill; omit for transparent | | `bold`, `italic`, `underline` | boolean | text style | | `fontSize` | number | px; 10–48; defaults: card 18, rect 16, diamond 16, circle 16, text 18, label 14, header 22 | | `fontFamily` | string | one of `sans` (default), `serif`, `rounded`, `mono`, `hand`, `display` | | `textAlign` | `left` \| `center` \| `right` | default center | | `verticalAlign` | `top` \| `middle` \| `bottom` | default middle | | `borderStyle` | `solid` \| `dashed` \| `dotted` | default solid | | `legendLabel` | string | shows the element in the bottom legend bar | | `groupId` | string | elements sharing a groupId move/resize together | Per-type fields (defaults in parentheses — good sizes to use): | `type` | Required fields | Size fields (default) | |---|---|---| | `rect` | `title` | `width` (120), `height` (60), `borderWidth?` | | `card` | `title`, `detail`, `rules` (string[]) | `width` (200), `height` (100), `borderWidth?` | | `diamond` | `title` | `width` (140), `height` (100), `borderWidth?` — decision shape | | `circle` | `text` | `diameter` (80) — start/end nodes | | `text` | `content`, `fontSize` | `maxWidth` (300) — free text, no border | | `label` | `content` | `maxWidth` (150) — small caption | | `header` | `title`, `subtitle` | `width` (400), `height` (60), `borderColor?`, `borderWidth?` (2) — section header | | `container` | `title` | `width` (320), `height` (220), `borderWidth?` — visual grouping frame (e.g. VNET / Subnet boundary). Title sits at the top-left; default fill is transparent. List it FIRST in `elements` so it renders behind the shapes it frames. | **Z-order:** elements render in array order — earlier entries are behind later ones. Put containers (and any background frames) at the start of `elements`; put the shapes that should sit on top after them. ### 3.1 Text, sizing and auto-height (READ THIS - the #1 cause of broken diagrams) Boxes have a **fixed width that you choose**. Text never widens a box: it **wraps to the next line and the box grows TALLER**. So an under-wide box with a long label turns into a tall multi-line box that **overlaps whatever you placed below it**. The `height` you set is a **minimum, not a cap** - the real height can be larger than you think. **Width a label needs** (at the default font sizes): - One line holds about `floor((width - 24) / (fontSize * 0.55))` characters. Rule of thumb at the defaults (~16px): a **120px** box ≈ 12 chars on one line, **200px** ≈ 20, **320px** ≈ 34, **420px** ≈ 45. - To keep a label on ONE line, size `width ≈ 10 * characters + 28`. A 30-char label wants `width ≈ 330`. **When unsure, widen the box** - wide-and-short beats narrow-and-tall. **Height a box will actually take** (use this to place the next element): - `charsPerLine = floor((width - 24) / (fontSize * 0.55))` - `lines = ceil(characters / charsPerLine)` - `renderedHeight ≈ max(height, lines * round(fontSize * 1.4) + 24)` - Position the **next** element at `previousY + renderedHeight + gap` using this `renderedHeight` - **never** the nominal `height`, or boxes overlap. **Per-shape caveats:** - `diamond`: the text sits in the narrow middle, so usable width is ~55% of `width`. Give a diamond ~1.8× the width you'd give a rect for the same text, or keep the label to one short word (`"OK?"`, `"Erro?"`). - `circle`: text must fit inside `diameter` (a circle never grows). Keep it to ~8 characters / one short word; raise `diameter` to 100–140 for longer text. - `card`: height = title + optional `detail` + one row per rule + padding. A card with a title and 3 rules is ~150–190px tall. Budget for that and don't pack cards tightly. - `header`: `title` + `subtitle` stack as two lines; keep `width` wide (it spans the section) and expect ~60–90px of height. - `text` / `label`: wrap at `maxWidth` and grow down; set `maxWidth` to fit the text. Keep labels concise. If a label is long, prefer widening the box or splitting the idea across two elements over cramming it into a default-sized box. Suggested palette (the app's own): `#27ae60` green, `#2980b9` blue, `#d4ac0d` yellow, `#e67e22` orange, `#e74c3c` red, `#8e44ad` purple, `#16a085` teal, `#6c757d` gray, `#34495e` graphite, `#2c3e50` charcoal. Light fills: `#e9f7ef`, `#ebf5fb`, `#fef9e7`, `#fdf2e9`, `#fbeae9`, `#f3eafc`. Default canvas-friendly fill is `#ffffff`. ## 4. Arrows ```json { "id": "a1", "start": { "elementId": "n1", "side": "bottom", "point": { "x": 460, "y": 160 } }, "end": { "elementId": "n2", "side": "top", "point": { "x": 460, "y": 260 } }, "color": "#6c757d", "style": "solid", "thickness": 2, "hasArrowhead": true, "label": "yes", "legendLabel": "happy path" } ``` - `start`/`end`: set `elementId` + `side` (`top` | `right` | `bottom` | `left`) to attach to an element, so the arrow follows it when moved. `point` is **required and must be computed by you** as the midpoint of that side: - `top` → `(x + w/2, y)` · `right` → `(x + w, y + h/2)` · `bottom` → `(x + w/2, y + h)` · `left` → `(x, y + h/2)` - For circles, `w = h = diameter`. - Omit `elementId`/`side` for a free-floating endpoint at `point`. - `style`: `solid` or `dashed`. `thickness`: px (2 is standard). `hasArrowhead`: `false` makes a plain line. - `label` renders at the arrow midpoint; `labelBold`/`labelItalic`/`labelUnderline` are optional booleans. `legendLabel` adds the arrow to the legend bar. ## 5. Layout recipes The editor does NOT auto-layout odraw content — you place everything. Rules of thumb: - **Vertical flowchart:** one column, centered. Same center-x for all nodes (e.g. x chosen so `x + width/2 = 400`, i.e. `x = 400 - width/2`). Between elements leave a gap of 60–100 **on top of each box's rendered height** (see 3.1) - compute the next `y` as `prevY + renderedHeight(prev) + gap`, not `prevY + 100`. Connect `bottom` → `top`. - **Decision branches:** from a `diamond`, route `left`/`right` sides to nodes offset ±260–320 horizontally (widen the offset if the branch boxes are wide, so they don't collide with the center column), then continue downward. Put `label: "yes"/"no"` on the branch arrows. - **Horizontal pipeline:** same center-y, gap of 60–100 **plus each box's width**, connect `right` → `left`. - **Start/end:** use `circle` with `text: "START"` / `"END"`. - **Sections:** a `header` (width spanning the content) above each region; leave room below it for its rendered height before the first node. - **Grid math:** pick a fixed column center and consistent row pitch, then derive every `x`/`y` from the box's own width/height and the neighbours' rendered heights. Never hand-pick coordinates that "look about right". **Avoid overlaps:** treat every element as the rectangle `[x, y, x+width, y+renderedHeight]` (circle: `[x, y, x+diameter, y+diameter]`) and keep them disjoint. Two boxes overlap when their rectangles intersect on **both** axes - check this for neighbours before finalizing. ### Self-check before emitting the URL Run through this and fix anything that fails - it eliminates the common failure modes: 1. **Every label fits its box.** For each element, `characters ≤ charsPerLine * expectedLines` (3.1). If a box would need more than ~2 lines, widen it or shorten the text. 2. **No overlaps.** No two element rectangles (using **renderedHeight**, not nominal height) intersect on both axes. Watch decision branches and any header/first-node pair. 3. **Gaps are real.** Each element's `y` was derived from the previous element's rendered height plus a 60–100 gap, so tall boxes did not eat the spacing. 4. **Arrow points are correct.** Every attached endpoint's `point` is the side midpoint of its element's box (recompute if you resized the box), so arrows meet the box edges cleanly. 5. **Containers first.** Any `container`/background frame is early in `elements` and large enough to enclose its members with padding. ## 6. Complete minimal example A three-node flow (start → task → end): ```json { "format": "odraw", "formatVersion": 2, "schemaVersion": 1, "elements": [ { "id": "n1", "type": "circle", "x": 360, "y": 80, "diameter": 80, "text": "START", "color": "#27ae60" }, { "id": "n2", "type": "rect", "x": 340, "y": 260, "width": 120, "height": 60, "title": "Do the thing", "color": "#2980b9", "bgColor": "#ebf5fb" }, { "id": "n3", "type": "circle", "x": 360, "y": 420, "diameter": 80, "text": "END", "color": "#e74c3c" } ], "arrows": [ { "id": "a1", "start": { "elementId": "n1", "side": "bottom", "point": { "x": 400, "y": 160 } }, "end": { "elementId": "n2", "side": "top", "point": { "x": 400, "y": 260 } }, "color": "#6c757d", "style": "solid", "thickness": 2, "hasArrowhead": true }, { "id": "a2", "start": { "elementId": "n2", "side": "bottom", "point": { "x": 400, "y": 320 } }, "end": { "elementId": "n3", "side": "top", "point": { "x": 400, "y": 420 } }, "color": "#6c757d", "style": "solid", "thickness": 2, "hasArrowhead": true } ] } ``` ## 7. Alternatives to the deep link The editor also imports files via Menu → Open / Import: - `.odraw` — the JSON above, gzip-compressed, saved as a file. Use for large diagrams. - `.drawio` — draw.io XML. - `.puml` — PlantUML component diagrams (`A --> B` syntax); the editor auto-layouts these on import. Lowest-effort option, but no control over position, color, or typography.