Open the interactive page in DDR · Syntax reference · For agents (MCP)

DDR syntax reference

The DDR diagram language: common syntax, chains, comments, positions and bends, then every diagram type with a rendered example you can open in the editor.

Generated from the DDR engine 0.0.5-alpha.5 (docs/spec, core/ddr/diag.go, internal/server, docs/examples) by go run ./cmd/ddr-docs. Do not edit by hand.

Overview

A .ddr file is plain text: a header of Key: value lines, a blank line, then sections. The editor draws it live, and dragging on the canvas writes back to the text.

Types: Flowchart, Graph, Architecture, Use case, Activity, Conceptual data model (CDM), Physical data model (PDM), Entity relationship (ERD, Chen), Documents (.tdr) and use case specifications.

Command line: ddr check, ddr fmt [-w], ddr render [-view v] [-theme light|dark] [-o out.svg], ddr views, ddr spec, ddr mcp, ddr version.

Header keys

The header is Key: value lines at the top of the file, ending at the first blank line. Keys are case-insensitive; unknown keys give W102 and are kept.

key required values default meaning
Type yes see the type list - Selects the diagram type. Missing: W106; unknown: W107.
Title no text - Diagram title. Missing: W106 (warning only).
Author no text - Metadata, not drawn.
Date no text (ISO date recommended) - Metadata, not drawn.
Version no text - Metadata.
Status no text - Metadata.
Tags no comma list - Metadata.
Theme no light dark light Preferred theme for exports.
Format no text - Metadata.
Direction no TB LR TB (LR for CDM/PDM) Main layout direction. Also accepted inside a Layout: section. Ignored in Activity (W110).
Lang no en id en Language of generated use case specification text.
Line Jumps no on off on Draw a small hop where two straight lines cross.

Lines, comments and strings

# A comment line
Type: Flowchart
Title: Lexical example

Flow {
  Start
  Read order #123
  note: "Contains; a semicolon"
  End
}

Sections

A section is Name: followed by an indented - list, or Name { … } (a block). Section names are case-insensitive.

name used in meaning
Actors UseCase Actor list.
Use Cases UseCase Use case list; alias UseCases.
Flow Flowchart, Activity, use cases The flow block.
Activity Activity Alias of Flow in activities.
Graph Graph Nodes and edges; alias Nodes.
Nodes Graph Alias of Graph.
Edges Graph Extra edges for the graph.
Entities CDM, PDM, ERD Entity list; alias Entity.
Entity CDM, PDM, ERD Alias of Entities.
Tables PDM Table list; alias Table.
Table PDM Alias of Tables.
Relations ERD Relation list; alias Relation.
Relation ERD Alias of Relations.
References CDM, PDM Reference list; also Ref { } or Ref: lines; Refs for per-segment via.
Layout all Section form of layout keys: Layout: then Direction: LR.
Numbering UseCase Header: use case codes.
Show Codes UseCase Header: print codes.
Min Use Cases UseCase Header: W120 threshold (default 6).
Lanes Activity Header: swimlanes.
Line Jumps all Header: off disables line hops.

Ids, labels and attributes

Every node is id: Label [key: value, flag]. Without an id the id is the slug of the label (Enter payment details → enter-payment-details). Ids match [A-Za-z_][A-Za-z0-9_.-]*. Several attribute groups are allowed: Login [shape: round] [x: 10, y: 20]. Unknown attributes give W105.

Node attributes:

attribute meaning
x y Pinned centre position (numbers). Without them the node gets auto layout. Ignored in Activity (W110).
w h Size override (numbers).
shape rect round pill ellipse circle diamond parallelogram actor cylinder box.
color accent purple amber orange teal pink blue green red gray, or #hex.
style Node style hint.
label Display label when the id is all you wrote.
id Alias for the node, same as an id: prefix. One word (E107 otherwise); a duplicate is E204.
system external primary secondary deprecated Flags: actor and use case kinds.
side Actors: left or right of the system boundary.
code Use case code set by hand, e.g. [code: UC07].
spec Use case specification table: own or none.
identifier pi CDM identifier (<pi> and <M>); also a key in PDM.
pk fk PDM primary / foreign key column.
unique PDM unique column (kept, not drawn).
null nullable not null nn required PDM nullability (kept, not drawn); required / [mandatory] / [M] mark CDM <M>.

Positions and bends

Type: Graph
Title: Pinned

Graph {
  a: Client [x: 100, y: 80]
  b: Server [x: 400, y: 80]
  a -> b: request [via: (250,20)]
}

Edges and chains

where syntax
Graph, Flowchart, Activity a -> b / a -> b: label [attrs]
Flow step step -> [attrs] styles the implicit edge to the next step; step -> target adds an edge
if if ok? -> [yes attrs] -> [no attrs] { … }
goto goto id [attrs] (no -> after it: W127)
Use case - Customer: Register -> Customer [attrs]
CDM / PDM Ref: a.id < b.a_id: name [attrs] with < > - <>

Chains. a -> b -> c adds one edge per consecutive pair. A label (: text) and attribute groups belong to the segment they follow; nothing is copied to other segments. Spaces around -> are optional (a->b->c). Every unknown node is E203; nodes are never created by an edge. A targetless -> [..] inside a chain or a trailing -> is ignored with W126. References chain with their own operators: Ref: a.id < b.a_id: owns < c.b_id: keeps.

Edge attributes:

attribute meaning
via Bend points in order: [via: (120,80) (240,80)].
style straight (default, polyline) or curve.
label Edge label (same as : text).
dash dashed Flag: dashed line.
arrow end (default) none both.
color As for nodes.
head Arrowhead: triangle (generalization), open, filled.
from to Explicit endpoints on a data-model reference.
Type: Graph
Title: Chain

Graph {
  a: Browser
  b: API
  c: Database [shape: cylinder]
  a -> b: HTTPS -> c: SQL [dashed]
}

Flow statements

Used in Flowchart, Activity and use case Flow { } blocks. Keywords count only at the start of a statement and are case-insensitive: if, elif, else, loop, goto, Start, End; in activities also parallel … and, stop.

stmt = step | "Start" [label] | "End" [label]
     | "if" cond block { "elif" cond block } [ "else" block ]
     | "loop" [cond] block | "goto" id | id ":" label | edge
     | "parallel" block { "and" block } | "stop" ;   (* activity only *)

Diagnostics

ddr check and the editor report these codes. Errors (E) stop parts of the diagram from drawing; warnings (W) point at something to fix.

code severity meaning fix
E101 error unclosed { Add the missing } (or remove the stray {).
E102 error bad indentation Indent with spaces to match the item above; fields go two spaces under their - item.
E103 error unknown or unexpected statement (also an empty actor group, - Clerk {}) Check the statement spelling, or quote the label ("if this is text"). An actor group needs at least one case.
E104 error unexpected } Remove the extra }.
E105 error unclosed " Close the string with ", or escape a literal quote as \".
E106 error unclosed [ Close the attribute group with ].
E107 error bad attribute syntax Write attributes as [key: value, flag]; an id is one word.
E108 error a .tdr document given to check (prose with embedded .ddr diagrams): check the .ddr files it embeds instead Run check on the .ddr files the document embeds (the paths in ::diagram[...] / ::usecases[...]), not on the .tdr itself.
E201 error unknown actor Declare the actor under Actors: or fix its spelling.
E202 error unknown use case in Includes / Extends Name an existing use case in Includes / Extends.
E203 error unknown edge or goto target Declare the target node (with id: Label) before using it in an edge or goto; edges never create nodes.
E204 error duplicate id Give each node a unique id.
E205 error activity: unknown lane (not in Lanes:) Add the lane to Lanes: or fix the prefix.
E206 error activity: more than one start (start written twice or after a step) Write start at most once, as the first statement (it is implicit anyway).
E207 error data model: a relationship names an entity or table that is not declared Declare the entity or table, or fix the name in the Ref: / relation.
W101 warning tab indentation Replace tabs with spaces.
W102 warning unknown key (kept) Remove or rename the key; unknown keys are kept but unused.
W104 warning actor with no use cases Link the actor to a use case or remove it.
W105 warning unknown attribute (ignored) Remove the attribute or use one from the attribute tables.
W106 warning missing Type / Title header Add Type: and Title: header lines at the top.
W107 warning unknown Type Use one of the documented types (Architecture is Type: Graph).
W108 warning a use case includes or extends itself (the line is not drawn) Remove the self include / extend.
W109 warning include cycle (drawn; the cases keep their actor lines) Break the include cycle.
W110 warning layout adjustment (activities: [x, y] ignored, Direction ignored): a [via: ...] bend pulled inside the system boundary so an actor stays outside, a line that cannot fit without reaching an actor, or an actor pinned inside the boundary Remove the [x, y] / [via: …] the layout cannot honor (activities ignore them).
W111 warning activity: the branches of a decision meet again without a merge Let the branches meet again (the structured if always does).
W112 warning activity: a parallel branch reaches the flow after the join without passing it Let every parallel branch reach the join.
W113 warning activity: a step no flow from the start reaches Connect the step to the flow or delete it.
W114 warning activity: a lane in Lanes: with no steps Remove the empty lane from Lanes: or put a step in it.
W115 warning activity: a parallel block with an empty branch (left out) Add a step to the empty parallel branch or remove the branch.
W116 warning activity: an action with more than one incoming or outgoing flow Use a decision or merge instead of several a -> b flows on one action.
W117 warning activity: the layout did not converge; the result may overlap Simplify the activity (fewer crossing gotos); the result may overlap.
W118 warning data model: a reference that is not a.attr < b.attr, or an attribute the reference names but the entity does not have Write references as a.attr < b.attr with attributes the entities have; per-segment via goes in Refs.
W119 warning data model: a relationship from an entity to itself (not drawn) Self relationships are not drawn in CDM/PDM; model them with a link entity.
W120 warning Numbering: on and fewer main use cases than Min Use Cases: (default 6) Add main use cases or lower Min Use Cases:.
W121 warning two use cases have the same code Give each use case a unique [code: …].
W122 warning Alternative N / Alur Alternatif N.M, or a goto N inside it, names a main flow step that does not exist Number the alternative after an existing main flow step.
W123 warning a [code: ...] that does not follow the Numbering: format (UC-01 under Numbering: UC) Follow the Numbering: format, e.g. UC-01 under Numbering: UC-.
W124 warning ERD: a relation that joins fewer than two entities Make the relation name at least two entities (the same one twice for a self relation); no chains inside relations.
W125 warning ERD: an entity without a key attribute (*) Mark a key attribute with *.
W126 warning a targetless -> [..] clause or a trailing -> inside an edge chain (ignored; fmt drops a bare trailing ->) Remove the targetless -> [..] or trailing -> from the chain (ddr fmt drops a bare trailing ->).
W127 warning goto x -> y: a goto jumps to one target, the -> part is ignored Write goto x alone; add a separate edge if you need one.
W128 warning flowchart/graph: a later End with a different label joins the one End; its label is not drawn Write one End (or give every End the same label); all paths join the diagram's single End.
W129 warning unknown Lang: value (not en or id, any case); falls back to English (en) Use Lang: en or Lang: id (or drop the line for English).

HTTP API

The cloud server's REST API. Paths are under /api/v1 except /healthz, /ws/… and the browser redirects of Google sign-in. JSON in and out; errors are application/problem+json ({"type","title","status","detail"}). Auth is the ddr_session cookie set by login/register; optional means signed-out callers see public projects only. State-changing requests must come from the same origin. Every route below is read from internal/server/server.go; a test fails if one is missing.

Diagrams are rendered in the browser (WebAssembly), by ddr render and by the MCP render tool; the server stores text and has no render endpoint yet.

Health

GET /healthz · auth: none
Liveness probe (outside /api/v1).

Response:

200 {"status":"ok","db":true,"realtime":{…}} (503 "degraded" when the database is down)

GET /api/v1/health · auth: none
Liveness probe.

Response:

200 {"status":"ok","db":true,"realtime":{…}} (503 "degraded" when the database is down)

Admin

GET /api/v1/admin/stats · auth: session
Site statistics (admins only).

Response:

200 {"users":12,"projects":40,"files":180,"storageBytes":123456}

GET /api/v1/admin/users · auth: session
Users with role, project count and last seen (?q= filters).

Response:

200 {"items":[]}

PATCH /api/v1/admin/users/{id} · auth: session
Set a user's site role (user or admin).

Request:

{"role":"admin"}

Response:

200 {"id":"…","role":"admin"}

GET /api/v1/admin/audit · auth: session
Audit log: sign-ins, project/file create/delete, uploads, role changes, agent use (?action=, ?before=, ?limit=).

Response:

200 {"items":[]}

Agent

GET /api/v1/agent/status · auth: session
Which AI providers budi can use and whether you saved your own key.

Response:

200 {"providers":["gemini:gemini-2.5-flash"],"byokEnabled":true,"available":true}

POST /api/v1/agent/chat · auth: session
Chat with budi. context is graph-retrieved project context; edits come back as fenced file blocks for review. Rate limited per user.

Request:

{"messages":[{"role":"user","content":"add a cancel step"}],"context":"…","projectId":"…"}

Response:

200 {"reply":"…","provider":"gemini","model":"gemini-2.5-flash"}

PUT /api/v1/me/agent-key · auth: session
Save your own AI key (encrypted at rest); budi uses it first, then the free providers.

Request:

{"provider":"openrouter","model":"","key":"sk-…"}

Response:

204 No Content

DELETE /api/v1/me/agent-key · auth: session
Remove your own AI key or disconnect OpenRouter.

Response:

204 No Content

POST /api/v1/me/agent-key/openrouter · auth: session
Finish OpenRouter OAuth PKCE: exchanges the code for a key on your OpenRouter account (your credits).

Request:

{"code":"…","codeVerifier":"…","model":"openrouter/auto"}

Response:

204 No Content

Auth

POST /api/v1/auth/register · auth: none
Create an account and sign in (sets the session cookie). Rate limited.

Request:

{"email":"ana@example.com","password":"correct horse battery","name":"Ana"}

Response:

201 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

POST /api/v1/auth/login · auth: none
Sign in with email and password (sets the session cookie). Rate limited.

Request:

{"email":"ana@example.com","password":"correct horse battery"}

Response:

200 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

POST /api/v1/auth/logout · auth: optional
End the current session and clear the cookie.

Response:

204 No Content

POST /api/v1/auth/verify · auth: none
Confirm an email address with the token from the mail.

Request:

{"token":"v_abc…"}

Response:

200 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

POST /api/v1/auth/verify/resend · auth: session
Send a new verification link (valid 24h, single use). At most one per minute and 10 per day per user; otherwise 429 with Retry-After. Mail goes through Resend (RESEND_API_KEY) or SMTP (SMTP_HOST…), from MAIL_FROM; with neither, the link is written to the server log.

Response:

200 {"retryAfter":60,"remainingToday":9,"cooldown":60,"dailyLimit":10}

GET /api/v1/auth/verify/status · auth: session
Whether your email is verified, whether verification is required (EMAIL_VERIFICATION_REQUIRED; when false new emails are verified at once and resend answers 409 verification_disabled), and when Resend is allowed again.

Response:

200 {"verified":false,"required":true,"retryAfter":42,"remainingToday":8,"cooldown":60,"dailyLimit":10}

POST /api/v1/auth/forgot · auth: none
Request a password reset mail. Always 204, so it cannot probe for accounts.

Request:

{"email":"ana@example.com"}

Response:

204 No Content

POST /api/v1/auth/reset · auth: none
Set a new password with the reset token.

Request:

{"token":"r_abc…","password":"new long password"}

Response:

204 No Content

GET /api/v1/auth/oauth/{provider} · auth: none
Generic OAuth route for other providers: always 501. Google sign-in has its own routes (GET /auth/google) once they are on this server.

Response:

501 {"type":"…/not_implemented","status":501,"detail":"…"}

GET /api/v1/auth/oauth/{provider}/callback · auth: none
Callback of the generic OAuth route: always 501.

Response:

501 {"type":"…/not_implemented","status":501,"detail":"…"}

GET /api/v1/auth/providers · auth: none
Which sign-in methods this server offers.

Response:

200 {"password":true,"google":true}

GET /auth/google · auth: none
Start Google sign-in (outside /api/v1). ?next=/path is where to land afterwards (same-site paths only).

Response:

302 Location: https://accounts.google.com/o/oauth2/v2/auth?…

GET /auth/google/callback · auth: none
Google redirects here (outside /api/v1); signs in or creates the account, sets the session cookie.

Request:

`?code=…&state=…`

Response:

302 Location: /<next> (or /login?error=<code>)

Account

GET /api/v1/me · auth: session
The signed-in user.

Response:

200 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

PATCH /api/v1/me · auth: session
Change name, email, language or password. A new password, or a new email on an account with a password, needs currentPassword. A new email starts unverified and gets a verification link; email changes share the resend limits (1 per minute, 10 per day) and are capped at 5 per day (429 with Retry-After).

Request:

{"name":"Ana B.","email":"ana@new.example","currentPassword":"…"}

Response:

200 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

PUT /api/v1/me/avatar · auth: session
Upload an avatar: the raw PNG, JPEG or WebP as the body (max 2 MB). Stored as a 256x256 WebP in S3-compatible storage (S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE); the previous one is deleted. 422 for other types, 413 when too large.

Request:

<image bytes>

Response:

200 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

DELETE /api/v1/me/avatar · auth: session
Remove the uploaded avatar (a Google profile picture shows again if there is one).

Response:

200 {"id":"7b6c…","email":"ana@example.com","name":"Ana","avatarUrl":null,"emailVerified":true,"createdAt":"2026-10-22T10:00:00Z"}

GET /api/v1/users/{id}/avatar · auth: none
An uploaded avatar (image/webp). With ?v=<etag> it is cached for a year; sends an ETag and answers If-None-Match with 304.

Response:

200 image/webp

GET /api/v1/me/sessions · auth: session
List your sessions.

Response:

200 {"items":[{"id":"…","createdAt":"…","lastSeenAt":"…","expiresAt":"…","userAgent":"Firefox","ip":"203.0.113.5","current":true}]}

DELETE /api/v1/me/sessions/{id} · auth: session
Sign out one session.

Response:

204 No Content

GET /api/v1/me/invites · auth: session
Pending invites for your email.

Response:

200 {"items":[{"id":"c1e2…","projectId":"3f2a…","email":"bo@example.com","role":"editor","status":"pending","createdAt":"…","expiresAt":"…"}],"emailVerified":true}

POST /api/v1/me/invites/{id}/accept · auth: session
Accept an invite addressed to you.

Response:

200 {"projectId":"3f2a…","role":"editor"}

POST /api/v1/me/invites/{id}/decline · auth: session
Decline an invite addressed to you.

Response:

204 No Content

GET /api/v1/me/limits · auth: session
Your site role, usage and quotas.

Response:

200 {"role":"user","usage":{"projects":3,"storageBytes":10240},"limits":{"projects":30,"filesPerProject":30,"fileBytes":2097152,"storageBytes":262144000}}

DELETE /api/v1/me · auth: session
Delete your account.

Response:

204 No Content

GET /api/v1/me/identities · auth: session
Linked sign-in identities (e.g. Google).

Response:

200 {"items":[{"provider":"google","email":"ana@example.com"}]}

DELETE /api/v1/me/identities/{provider} · auth: session
Unlink an identity.

Response:

204 No Content

Projects

GET /api/v1/projects · auth: optional
List projects. ?filter= (all default; public lists public projects signed out), ?limit=, ?offset=.

Response:

200 {"items":[{"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"}],"limit":50,"offset":0}

POST /api/v1/projects · auth: session
Create a project.

Request:

{"name":"Online Store","description":"","visibility":"private"}

Response:

201 {"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"}

POST /api/v1/projects/import · auth: session
Import a .ddrproj.zip (raw application/zip body or multipart field file). ?name= overrides the name.

Request:

<zip bytes>

Response:

201 {"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"}

GET /api/v1/projects/{pid} · auth: optional
One project (members, link holders, or public).

Response:

200 {"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"}

PATCH /api/v1/projects/{pid} · auth: session
Rename, describe, change visibility or template flag.

Request:

{"visibility":"public"}

Response:

200 {"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"}

DELETE /api/v1/projects/{pid} · auth: session
Delete a project (owner).

Response:

204 No Content

POST /api/v1/projects/{pid}/duplicate · auth: session
Copy a project you can view into your account.

Request:

{"name":"Online Store copy"}

Response:

201 {"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"}

GET /api/v1/projects/{pid}/export.zip · auth: optional
Download the project as .ddrproj.zip.

Response:

200 application/zip

GET /api/v1/p/{slug} · auth: none
A public project and its files by slug.

Response:

200 {"project":{"id":"3f2a…","name":"Online Store","slug":"online-store","description":"","visibility":"private","template":false,"ownerId":"7b6c…","ownerName":"Ana","role":"owner","createdAt":"…","updatedAt":"…"},"files":[{"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}]}

Files

POST /api/v1/projects/{pid}/assets · auth: session
Upload a file (raw body, ?path=img/logo.png): png, jpg, gif, webp, svg, pdf, txt, csv, md. Free accounts: 2 MB per file, 30 files per project, 250 MB in total (403 quota_exceeded).

Response:

201 {"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}

GET /api/v1/projects/{pid}/files/{fid}/raw · auth: optional
The bytes of an uploaded file (sandboxed CSP), for embedding in .tdr documents.

Response:

200 image/png

GET /api/v1/projects/{pid}/files · auth: optional
List files.

Response:

200 {"items":[{"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}]}

POST /api/v1/projects/{pid}/files · auth: session
Create a file (kind ddr or tdr, inferred from the path).

Request:

{"path":"diagrams/flow.ddr","content":"Type: Flowchart\nTitle: Flow\n"}

Response:

201 {"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}

GET /api/v1/projects/{pid}/files/{fid} · auth: optional
File metadata. Sends an ETag.

Response:

200 {"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}

PATCH /api/v1/projects/{pid}/files/{fid} · auth: session
Rename or replace content. Send If-Match or version; a stale version is 412.

Request:

{"path":"diagrams/checkout.ddr","version":1}

Response:

200 {"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}

DELETE /api/v1/projects/{pid}/files/{fid} · auth: session
Delete a file.

Response:

204 No Content

GET /api/v1/projects/{pid}/files/{fid}/text · auth: optional
The file text (text/plain, ETag; If-None-Match gives 304).

Response:

200 text/plain
Type: Flowchart
Title: Flow

PUT /api/v1/projects/{pid}/files/{fid}/text · auth: session
Replace the text (text/plain body). If-Match: "v1"; ?reset=true drops the live Yjs state.

Request:

Type: Flowchart
Title: Flow

Response:

200 {"id":"91d0…","seq":4,"path":"diagrams/flow.ddr","kind":"ddr","version":1,"etag":"\"v1\"","size":64,"createdAt":"…","updatedAt":"…"}

Sharing

GET /api/v1/projects/{pid}/members · auth: session
List members.

Response:

200 {"items":[{"userId":"…","email":"bo@example.com","name":"Bo","avatarUrl":null,"role":"editor","addedAt":"…"}]}

POST /api/v1/projects/{pid}/members · auth: session
Invite by email (same as POST invites).

Request:

{"email":"bo@example.com","role":"editor"}

Response:

201 {"id":"c1e2…","projectId":"3f2a…","email":"bo@example.com","role":"editor","status":"pending","createdAt":"…","expiresAt":"…"}

PATCH /api/v1/projects/{pid}/members/{uid} · auth: session
Change a role; owner transfers ownership.

Request:

{"role":"viewer"}

Response:

200 {"userId":"…","role":"viewer"}

DELETE /api/v1/projects/{pid}/members/{uid} · auth: session
Remove a member (or leave).

Response:

204 No Content

GET /api/v1/projects/{pid}/invites · auth: session
List invites.

Response:

200 {"items":[{"id":"c1e2…","projectId":"3f2a…","email":"bo@example.com","role":"editor","status":"pending","createdAt":"…","expiresAt":"…"}]}

POST /api/v1/projects/{pid}/invites · auth: session
Invite by email.

Request:

{"email":"bo@example.com","role":"editor"}

Response:

201 {"id":"c1e2…","projectId":"3f2a…","email":"bo@example.com","role":"editor","status":"pending","createdAt":"…","expiresAt":"…"}

DELETE /api/v1/invites/{id} · auth: session
Revoke an invite.

Response:

204 No Content

POST /api/v1/invites/{id}/resend · auth: session
Resend an invite mail.

Response:

200 {"id":"c1e2…","projectId":"3f2a…","email":"bo@example.com","role":"editor","status":"pending","createdAt":"…","expiresAt":"…"}

GET /api/v1/invites/lookup · auth: none
Describe an invite by ?token=.

Response:

200 {"projectName":"Online Store","inviterName":"Ana","email":"bo@example.com","role":"editor","status":"pending"}

GET /api/v1/invites/{token} · auth: none
Same as lookup, token in the path.

Response:

200 {"projectName":"Online Store","inviterName":"Ana","email":"bo@example.com","role":"editor","status":"pending"}

POST /api/v1/invites/accept · auth: session
Accept an invite by token.

Request:

{"token":"i_abc…"}

Response:

200 {"projectId":"3f2a…","role":"editor"}

POST /api/v1/invites/decline · auth: none
Decline an invite by token.

Request:

{"token":"i_abc…"}

Response:

204 No Content

GET /api/v1/projects/{pid}/links · auth: session
List share links.

Response:

200 {"items":[{"id":"5aa1…","projectId":"3f2a…","role":"viewer","token":"s_9x…","url":"https://example.com/s/s_9x…","createdAt":"…","expiresAt":null}]}

POST /api/v1/projects/{pid}/links · auth: session
Create a share link. Editor links need confirm: true.

Request:

{"role":"viewer","expiresInHours":72}

Response:

201 {"id":"5aa1…","projectId":"3f2a…","role":"viewer","token":"s_9x…","url":"https://example.com/s/s_9x…","createdAt":"…","expiresAt":null}

DELETE /api/v1/projects/{pid}/links/{lid} · auth: session
Revoke a link.

Response:

204 No Content

DELETE /api/v1/projects/{pid}/links · auth: session
Revoke a link given as ?id=.

Response:

204 No Content

GET /api/v1/links/lookup · auth: none
Describe a share link by ?token=.

Response:

200 {"projectId":"3f2a…","projectName":"Online Store","role":"viewer","expiresAt":null}

POST /api/v1/links/accept · auth: session
Join a project through a share link.

Request:

{"token":"s_9x…"}

Response:

200 {"projectId":"3f2a…","role":"viewer"}

GET /api/v1/projects/{pid}/access-requests · auth: session
List access requests (managers).

Response:

200 {"items":[{"id":"e4f5…","projectId":"3f2a…","userId":"8c9d…","message":"May I view this?","status":"pending","createdAt":"…"}]}

POST /api/v1/projects/{pid}/access-requests · auth: session
Ask the owner for access.

Request:

{"message":"May I view this?"}

Response:

201 {"id":"e4f5…","projectId":"3f2a…","userId":"8c9d…","message":"May I view this?","status":"pending","createdAt":"…"}

PATCH /api/v1/access-requests/{id} · auth: session
Approve or deny a request.

Request:

{"status":"approved","role":"viewer"}

Response:

200 {"id":"e4f5…","projectId":"3f2a…","userId":"8c9d…","message":"May I view this?","status":"pending","createdAt":"…"}

Planned

GET /api/v1/assets/{aid} · auth: optional
Read an asset (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

GET /api/v1/projects/{pid}/versions · auth: session
Version history (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

POST /api/v1/projects/{pid}/versions · auth: session
Save a version (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

POST /api/v1/projects/{pid}/versions/{vid}/restore · auth: session
Restore a version (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

GET /api/v1/files/{fid}/threads · auth: session
Comment threads (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

POST /api/v1/files/{fid}/threads · auth: session
Start a thread (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

POST /api/v1/threads/{tid}/comments · auth: session
Reply (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

PATCH /api/v1/threads/{tid} · auth: session
Resolve a thread (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

POST /api/v1/projects/{pid}/export · auth: session
Server-side render/export job (planned). Today rendering runs in the browser (WebAssembly), the ddr render CLI and the render MCP tool.

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

GET /api/v1/exports/{id} · auth: session
Export job status (planned).

Response:

501 {"type":"…/not_implemented","title":"Not Implemented","status":501,"detail":"not available in v0.0.1"}

Realtime

GET /ws/projects/{pid} · auth: session
WebSocket for live editing (outside /api/v1). Binary frames kind u8 | docRef uvarint | payload: 0x01 subscribe to file (docRef = file seq), 0x02 unsubscribe, 0x10 y-protocols sync, 0x11 awareness, 0x12 checkpoint, 0x13 seq ack, 0x20 file tree (JSON), 0x30 presence (JSON), 0x40 events (JSON), 0x7E error, 0x7F ping.

Request:

GET with `Upgrade: websocket` and the session cookie

Response:

101 Switching Protocols

Flowchart

Classic flowchart (ISO 5807): terminators, process boxes, decision diamonds, input/output parallelograms.

key required values default meaning
Flow yes block - The one flow of the chart.
Grammar
file      = header blank "Flow" "{" { stmt } "}" ;
stmt      = step | start | end | if | loop | goto | edge ;
start     = "Start" [ label ] ;
end       = "End" [ label ] ;
step      = [ id ":" ] label { attrs } [ "->" ( attrs | target [ ":" label ] { attrs } ) ] ;
if        = "if" cond [ "->" attrs [ "->" attrs ] ] block { "elif" cond block } [ "else" block ] ;
loop      = "loop" [ cond ] block ;
goto      = "goto" id [ attrs ] ;
edge      = id "->" id [ ":" label ] { attrs } { "->" id [ ":" label ] { attrs } } ;
block     = "{" { stmt } "}" ;

Example (docs/examples/flowchart.ddr):

Type: Flowchart
Title: Password reset
Direction: TB

# One Flow block. Steps run top to bottom; `if`, `loop` and `goto` branch.
Flow {
  Start;
  Enter email address;
  if Account exists? {
    Send reset link;
    open: Open link from email;
    if Link still valid? {
      Choose new password;
    } else {
      Show expired message;
      goto open;
    }
  } else {
    Show generic confirmation;
  }
  End;
}

Flowchart: Start/End labels and the one End

start = "Start" [ ":" ] [ label ] ;
end   = "End" [ ":" ] [ label ] ;

Example (docs/examples/pending/flowchart-start-end-labels.ddr):

Type: Flowchart
Title: Order approval

Flow {
  Start: Order received;
  if In stock? {
    Ship order;
    End: Order closed;
  } else {
    Notify customer;
    End: Order closed;
  }
}

Graph

Free boxes-and-arrows graph: node statements and explicit a -> b edges.

key required values default meaning
Graph yes block (alias Nodes) - Nodes and edges.
Edges no block - Extra edges added to the same graph.
Grammar
file  = header blank ( "Graph" | "Nodes" ) "{" { node | edge } "}" [ "Edges" "{" { edge } "}" ] ;
node  = [ id ":" ] label { attrs } ;
edge  = id "->" id [ ":" label ] { attrs } { "->" id [ ":" label ] { attrs } } ;

Example (docs/examples/graph.ddr):

Type: Graph
Title: Order service
Direction: LR

Graph {
  user: Customer [shape: actor];
  web: Web app [shape: round];
  api: Order API;
  queue: Event queue [shape: pill, color: amber];
  db: Orders database [shape: cylinder];
  mail: Email service [shape: round, color: teal];
  user -> web: browses;
  web -> api: JSON over HTTPS;
  api -> db: SQL;
  api -> queue -> mail: order.created;
}

Architecture

Architecture diagrams are written as Type: Graph with shapes (cylinder for stores, actor for people, round for services) and colors per tier.

Grammar
Same as Graph.

Example (docs/examples/architecture.ddr):

Type: Graph
Title: Web platform architecture
Direction: LR

# Architecture diagrams are Graphs with shapes and colors for each tier.
Graph {
  browser: Browser [shape: round];
  cdn: CDN [shape: pill, color: teal];
  app: App server;
  worker: Background worker [color: purple];
  cache: Cache [shape: cylinder, color: red];
  db: Database [shape: cylinder];
  store: File storage [shape: cylinder, color: amber];
  browser -> cdn: static files;
  browser -> app: API calls;
  app -> cache;
  app -> db: reads and writes;
  app -> worker: jobs [dashed];
  worker -> store: uploads;
}

Use case

UML use case diagram: actors outside a system boundary, use cases inside, include / extend / generalization.

key required values default meaning
Actors yes list - One actor per - line. Flags: [system], [external], [primary], [secondary]; [side: left|right].
Use Cases yes list - - Actor, Actor: Use case with indented fields.
Numbering no prefix, e.g. UC or UC- off Gives each main case a code UC01, UC02, … (sub-cases UC03.1).
Show Codes no yes no no Print the code above the name.
Min Use Cases no number 6 With Numbering:, fewer main cases warns W120.
Grammar
file      = header blank "Actors:" { actor } "Use Cases:" { case | group } ;
actor     = "-" name { attrs } [ "->" name "[head: triangle]" ] ;
case      = "-" actors ":" name { attrs } [ "->" actor { attrs } ] { field } ;
group     = "-" actors [ ":" ] "{" { name { attrs } [ "{" fields "}" ] ( ";" | NL ) } "}" ;
actors    = name { "," name } ;
field     = key ":" text | ( "Flow" | "Main Flow" | "Alternative" [ N ] | "Exceptions" ) "{" { stmt } "}" ;
key       = "Description" | "Includes" | "Extends" | "Precondition" | "Postcondition" | "Actors" | ... ;

Example (docs/examples/usecase.ddr):

Type: UseCase
Title: Online Store
Author: Product Team

Actors:
- Customer
- Store Manager
- Payment Provider [system]

Use Cases:
- Customer: Browse catalog
  Description: The customer looks through products by category.
- Customer: Place order
  Description: The customer buys the items in the cart.
  Includes: Pay for order
- Customer, Payment Provider: Pay for order
  Description: The order total is charged to the customer.
- Customer: Track order
  Extends: Place order
- Store Manager: Manage products
  Description: The manager adds, edits and removes products.

Activity

UML activity diagram with swimlanes, decisions, merges, fork/join bars and one implicit start and end.

key required values default meaning
Lanes no comma list lanes in order of first use Swimlane columns, left to right. A lane named in a step must be listed (E205) when Lanes: is given.
Flow yes block (alias Activity) - The activity.
Grammar
file      = header blank ( "Flow" | "Activity" ) "{" { stmt } "}" ;
stmt      = [ lane ":" ] ( step | if | loop | parallel | goto | "start" | "end" | "stop" | edge ) ;
step      = [ id ":" ] label { attrs } ;
if        = "if" cond [ "[yes:" text "," "no:" text "]" ] block { "elif" cond block } [ "else" block ] ;
loop      = "loop" [ cond ] block ;
parallel  = "parallel" block "and" block { "and" block } ;

Example (docs/examples/activity.ddr):

Type: Activity
Title: Support ticket
Lanes: Customer, Support, System

Flow {
  Customer: Submit ticket
  System: Assign ticket number
  Support: Review ticket
  if Needs more information? {
    Support: Ask customer for details
    Customer: Reply with details
  } else {
    Support: Accept ticket
  }
  parallel {
    Support: Resolve issue
  } and {
    System: Notify customer of progress
  }
  Customer: Confirm resolution
}

Language of generated labels (Lang)

lang = ( "Lang" | "Language" ) ":" ( "en" | "id" ) ;

Example (docs/examples/pending/activity-lang.ddr):

Type: Activity
Title: Leave request
Lang: en
Lanes: Employee, Manager

Flow {
  Employee: Submit leave request
  Manager: Review request
  if Approved? {
    Manager: Record approval
  } else {
    Employee: Revise request
  }
}

Conceptual data model (CDM)

PowerDesigner-style conceptual model: entities, attributes with identifiers <pi> and mandatory <M>, named relationships with crow's feet.

key required values default meaning
Entities yes list - One entity per - item.
Ref no line (repeatable) - A relationship. Also as a References: list or a Ref { } block.
Grammar
file      = header blank "Entities:" { entity } { ref } ;
entity    = "-" name ( "{" attr { ( ";" | NL ) attr } "}" | NL { INDENT attr NL } ) ;
attr      = name [ ":" type ] { attrs } ;           (* [identifier] [pi] [mandatory] [M] *)
ref       = "Ref:" end op end [ ":" name ] { attrs } { op end [ ":" name ] } ;
end       = entity_name [ "." attr_name ] ;
op        = "<" | ">" | "-" | "<>" ;

Example (docs/examples/cdm.ddr):

Type: CDM
Title: Online Store

Entities:
- Customer {
    Customer ID: varchar(12) [identifier]
    Full Name: varchar(80) [mandatory]
    Email: varchar(120)
  }
- Order {
    Order ID: varchar(12) [identifier]
    Order Date: date [mandatory]
    Total: double
  }
- Product {
    Product ID: varchar(12) [identifier]
    Name: varchar(100) [mandatory]
    Price: double
  }

Ref: Customer < Order: places
Ref: Order <> Product: contains

Physical data model (PDM)

Physical tables with SQL types and PK/FK columns; references are written between columns.

key required values default meaning
Tables yes list (alias Entities) - One table per - item.
Ref no line (repeatable) - Ref: a.col < b.col. The many side (or the right side of -) is the FK.
Grammar
file      = header blank ( "Tables:" | "Entities:" ) { table } { ref } ;
table     = "-" name ( "{" column { ( ";" | NL ) column } "}" | NL { INDENT column NL } ) ;
column    = name ":" sqltype { attrs } ;             (* [pk] [fk] [unique] [not null] [null] *)
ref       = "Ref:" table "." column op table "." column [ ":" name ] { attrs } ;

Example (docs/examples/pdm.ddr):

Type: PDM
Title: Online Store

Tables:
- customers {
    customer_id: bigint [pk]
    full_name: varchar(120) [not null]
    email: varchar(180) [not null]
  }
- orders {
    order_id: bigint [pk]
    customer_id: bigint [not null]
    ordered_at: timestamp [not null]
  }
- order_items {
    order_id: bigint [pk]
    product_id: bigint [pk]
    quantity: int [not null]
  }
- products {
    product_id: bigint [pk]
    name: varchar(100) [not null]
    price: numeric(10,2) [not null]
  }

Ref: customers.customer_id < orders.customer_id
Ref: orders.order_id < order_items.order_id
Ref: products.product_id < order_items.product_id

Entity relationship (ERD, Chen)

Chen notation: entities are rectangles, relations diamonds, attributes ovals; key * (bold, underlined), foreign key **, derived # (dashed), composite name { ... }.

key required values default meaning
Entities yes list - - Name { Attr *; Attr; Ref ** }.
Relations yes list - - Verb { EntityA: 1; EntityB: N; attribute }.
Grammar
file      = header blank "Entities:" { entity } "Relations:" { relation } ;
entity    = "-" name "{" attr { ";" attr } "}" ;
attr      = mark name [ "[x:" n "," "y:" n "]" ] [ "{" attr { ";" attr } "}" ]
          | name [ "*" | "**" ] [ "#" ] [ "[x:" n "," "y:" n "]" ] [ "{" attr { ";" attr } "}" ] ;
mark      = "*" | "**" ;
relation  = "-" name "{" member { ";" member } "}" ;
member    = entity_name [ ":" card ] | attr ;          (* card: 1, M, N, 0..1, ... *)

Example (docs/examples/erd.ddr):

Type: ERD
Title: Online Store

Entities:
- Customer { Customer ID *; Name; Email }
- Order { Order ID *; Order Date; Customer ID ** }
- Product { Product ID *; Name; Price }

Relations:
- Places { Customer: 1; Order: N }
- Contains { Order: M; Product: N; Quantity }

ERD: ternary relationships

A relation joins every entity it names, so a ternary (or wider) relationship is one diamond with three member lines:

relation = "-" name "{" member ";" member ";" member { ";" ( member | attr ) } "}" ;

Example (docs/examples/erd-ternary.ddr):

Type: ERD
Title: Course enrollment

Entities:
- Student { Student ID *; Name }
- Course { Course ID *; Title }
- Teacher { Teacher ID *; Name }

Relations:
- Enrolls { Student: N; Course: M; Teacher: 1; Term }

ERD: key, derived and composite attributes

Attribute marks and nesting:

attr = name [ "*" | "**" ] [ "#" ] [ "{" attr { ";" attr } "}" ] ;

Example (docs/examples/erd-attributes.ddr):

Type: ERD
Title: Users and Orders

# key `*`, foreign key `**`, derived `#`, composite `{ }`
Entities:
- User {
    User ID *
    Name
    Age #
    Address {
      Country
      City
      District
    }
  }
- Order { *Order ID; Order Date; User ID **; Total # }

Relations:
- Places { User: 1; Order: N }

ERD: optional cardinality

The cardinality after Entity: is optional:

member = entity_name [ ":" card ] ;   (* card: 1 | M | N | 0..1 | 1..N | ... ; free text *)

Example (docs/examples/erd-optional-card.ddr):

Type: ERD
Title: Employees and parking spots

Entities:
- Employee { Employee ID *; Name }
- Parking Spot { Spot ID *; Level }
- Department { Department ID *; Name }

Relations:
- Uses { Employee: 0..1; Parking Spot: 0..1 }
- Works In { Employee; Department }

Documents (.tdr) and use case specifications

Markdown documents with a header and directives that embed live diagrams and use case specification tables.

key required values default meaning
Title no text - Title block.
Subtitle no text - Title block.
Author no text - Title block.
Date no text - Title block.
Grammar
file      = [ { key ":" value NL } "---" NL ] markdown ;
directive = "::diagram[" path "]" [ "{" "view=" q [ "caption=" q ] "}" ]
          | "::usecases[" path "]" [ "{" "title=" q "}" ] ;

Example (docs/examples/document.tdr):

Title: Online Store requirements
Author: Product Team
---

# Introduction

This document describes the Online Store. Each figure is a live view of a
`.ddr` file, so it stays in sync with the diagram source.

::diagram[diagrams/usecase.ddr]{view="overview" caption="Use cases"}

## Use case specifications

::usecases[diagrams/usecase.ddr]{title=""}