The agent protocol

An AI agent can draw here. Not generate a picture of a drawing — draw: place a circle with a centre and a radius, group three shapes, align the group against a wordmark. What it produces is a real vector document, and every shape in it is one you can afterwards select, move, recolour and delete.

That works because there is only one set of verbs. When you press Ctrl+G in the editor, the keyboard sends group.create. When you click the group button, the toolbar sends group.create. When you run jd group create, the CLI sends group.create. When a language model decides two shapes belong together, it sends group.create. Four seats, one steering wheel — so an agent’s edit lands in your undo history, appears on your screen as it happens, and autosaves like any other edit.

Start here

Paste a project URL at an agent and it will work the rest out, because the URL teaches. Every project publishes a briefing generated from its own live contents:

GET https://app.justdraw.fyi/p/<uuid>.md

That briefing lists what is currently on the canvas with real ids, the whole command surface, and curl examples with that project’s id already substituted in — so they run as written. An agent that has never heard of justdraw.fyi can be handed a link and get to work. This page is the same material, minus the project.

Three steps

1. Make a project. No account, no key.

curl -sX POST https://app.justdraw.fyi/api/v1/projects \
  -H 'Origin: https://app.justdraw.fyi'

{"uuid":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40","url":"/p/0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40"}

2. Open it in a browser. https://app.justdraw.fyi/p/<uuid> — see the constraint below.

3. Send commands.

curl -sX POST https://app.justdraw.fyi/api/v1/agent \
  -H 'Content-Type: application/json' \
  -H 'Origin: https://app.justdraw.fyi' \
  -d '{"cmd":"element.draw","project":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40",
       "args":{"shape":"rect","x1":0,"y1":0,"x2":40,"y2":30,"fill":"#5b7cfa"}}'

{"ok":true,"result":{"id":"<new-uuid>","type":"rect","name":"Rectangle 1"}}

Every command that creates something returns its id. Ids are opaque: pass them straight back into the next command, and never parse them.

The two things that will cost you a retry

The Origin header is required. Leave it out and you get 403 Origin or Referer header required. It is a cross-site guard: a browser will not attach that header when another website tries to POST here on a user’s behalf, which is the attack it exists to stop. curl doesn’t send one unless you say so. Set it to this server and forget about it.

A drawing command needs the project open in a browser. With no tab, you get 409. The editor in the browser is the engine — it owns the geometry, the layers, the undo stack — and the server only relays commands down the WebSocket that the open editor holds. That is a real constraint, and it is the reason there is exactly one implementation of what every command means, shared by the toolbar, the keyboard, the CLI and any agent. Nothing can drift.

What does work with no tab open, because it is answered from the database: proj.create|destroy|list|show|rename, all of snap.*, and doc.export with {"format":"yaml"}. Everything else — including element.list, doc.get, and doc.export to SVG — is executed by the editor and returns 409 without one.

Commands

Each is the cmd and args of the POST body above, with "project":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40" alongside them.

cmdargswhat it does
element.draw{"shape":"rect","x1":0,"y1":0,"x2":40,"y2":30,"fill":"#e5614c"}rect / square / ellipse: two opposite corners
element.draw{"shape":"circle","cx":80,"cy":15,"r":15}circle: centre and radius
element.draw{"shape":"triangle","x1":0,"y1":0,"x2":40,"y2":30}triangle: base flush with the bottom, apex centred
element.draw{"shape":"line","x1":0,"y1":0,"x2":50,"y2":50}line: two endpoints
element.draw{"shape":"polygon","x1":0,"y1":0,"x2":40,"y2":40,"sides":6}polygon: a REGULAR n-gon inscribed in the box (sides defaults to 5)
element.draw{"shape":"star","x1":0,"y1":0,"x2":40,"y2":40,"points":5}star: points defaults to 5
element.draw{"shape":"text","x":0,"y":0,"text":"hello","size":16}text: size defaults to 16
element.style{"ids":["<id>"],"fill":"#5b7cfa","stroke":"none"}colour: fill, stroke, strokeWidth, opacity, rx
element.style{"ids":["<id>"],"fontSize":48,"weight":700,"fontFamily":"Georgia"}type: fontSize, weight (100–900), fontFamily, align, text
element.list{}every element: id, type, name, bounding box, fill, stroke
element.move{"ids":["<id>"],"dx":10,"dy":-5}translate by a delta
element.name{"id":"<id>","name":"hero"}rename an element, group, layer — or the project, with no id
element.clone{"ids":["<id>"]}duplicate; the copy is named _1
element.delete{"ids":["<id>","<id2>"]}remove elements
group.create{"ids":["<id>","<id2>"]}group them; returns the group id
group.ungroup{"ids":["<id>"]}dissolve the group that element is in
arrange.align{"mode":"left","ids":["<id>","<id2>"]}up, down, left, right, hcenter, vcenter
arrange.distribute{"axis":"h","ids":["<a>","<b>","<c>"]}even spacing: h or v
arrange.order{"mode":"front","ids":["<id>"]}front, back, forward, backward
doc.export{"format":"svg"}the whole drawing as SVG (or yaml)
doc.get{}the raw document
snap.create{"name":"before my changes"}freeze the document; returns the snapshot’s id
snap.list{}the timeline: id, name, element count, when
snap.show{"id":"<snapshot>"}that snapshot’s document, as YAML
snap.restore{"id":"<snapshot>"}put it back (what it replaces is frozen first)
snap.delete{"id":"<snapshot>"}forget a snapshot

Notes that will save you a retry

The canvas is dark — #141518. You cannot see it, so take this on trust: a near-black shape is invisible on it, and so is dark text. If you want something to read as black (a wordmark, a door), it needs to sit on a light shape you drew, or be light itself.

A palette that works on it: #5b7cfa blue, #e5614c red, #8bd450 green, #e0b64a amber, #4ad0c0 teal, #c86bd6 violet, #ffffff white, #e8e8ec off-white (the default for text). #111319 near-black is for use on top of a light shape — not against the canvas.

Going back

A project holds one document, and every save replaces it — so without something else, “what did this look like before?” is a question only an open tab’s undo stack can answer, and only until it closes. Snapshots are that something else: a frozen copy of the whole document, taken at a moment worth coming back to.

curl -sX POST https://app.justdraw.fyi/api/v1/agent -H 'Origin: https://app.justdraw.fyi' \
  -H 'Content-Type: application/json' \
  -d '{"cmd":"snap.create","project":"0192a4e1-7c33-7f21-9b0e-2f1a6c9d8e40","args":{"name":"before the rewrite"}}'

snap.list prints the timeline, snap.restore puts one back, and a restore freezes the document it is about to replace first — so restoring the wrong one costs you nothing but another restore. The server also takes one on its own every few minutes while a drawing is being changed, and the ones nobody named are the only ones it ever evicts.

None of this needs a browser: a snapshot is a copy of the document as a blob, and copying a blob does not require knowing what is drawn in it. So snap.* works on a project nobody has open — unlike everything that touches geometry. If a human is watching, a restore is handed to their editor as an ordinary edit, so Ctrl+Z takes it back.

Every one of those notes is a scar

They are not style advice. Each was written after watching a real agent, handed nothing but a URL, get that exact thing wrong:

The last two were bugs in the product, not in the model. The machine did the obvious thing and revealed that the obvious thing was broken.

MCP

If your agent speaks the Model Context Protocol — Claude, Cursor, Windsurf and most of the rest do — jd-mcp gives it these commands as tools, so it never has to write a curl at all. Download it from the releases and point your client at it:

{
  "mcpServers": {
    "justdraw": {
      "command": "/path/to/jd-mcp",
      "args": ["-server", "https://app.justdraw.fyi"]
    }
  }
}

Tools: create_project, list_projects, draw, list_elements, style, move, delete, group, ungroup, align, distribute, order, export. It is a thin adapter over the same HTTP calls above — it does not know what “align” means, and it keeps no second copy of the document. Ask it to create a project, open the URL it gives you, and tell it what to draw.

The command line

jd speaks this exact protocol from a shell. Every command prints the id of what it made, so ids pipe from one command into the next:

jd proj create logo          # prints the project id
jd snap take <project> "before I touch it"
jd draw <project> rect 0 0 120 80   # prints the new element's id
jd style <id> fill=#5b7cfa
jd group create <id> <id2>
jd align vcenter <group> <id3>
jd export <project> logo.svg
jd snap list <project>       # …and jd snap restore <project> <snapshot>

An element id works anywhere a project id is expected — the server finds the project it belongs to. Run jd help for the full surface.

justdraw.fyi is a vector editor that opens in a tab. No account, nothing to install, and every drawing is a link you can send to anyone.

← All posts