Automation API
Protocol version 1
The Automation API is intended for external automation, not a modding or plugin platform yet. The surface may grow in future releases.
External scripts and tools can drive Daggermap over a local newline-delimited JSON (NDJSON) Automation API. Enable it in Settings → General → Automation.
Enable Automation API
Off by default. Turning this on makes the app listen on a local TCP port.
Automation port
TCP port for incoming NDJSON connections (default 8765).
Allow remote connections
When unchecked, the API listens on localhost only.
Automation status
Shows the bound address when the API is listening.
Enable Automation API
Off by default. Turning this on makes the app listen on a local TCP port for incoming connections.
Automation port
TCP port for incoming NDJSON connections (default 8765).
Allow remote connections
When unchecked, the API listens on localhost only. Enable this only if you need to connect from another machine on your network and understand the exposure. There is no authentication — anyone who can reach the port can control the running app.
Automation status
Shows the bound address when the API is listening (for example 127.0.0.1:8765).
Protocol
| Property | Value |
|---|---|
| Transport | TCP |
| Framing | One JSON object per line (\n-terminated; \r ignored) |
| Default bind | 127.0.0.1:8765 |
| Remote bind | 0.0.0.0:<port> when Allow remote connections is on |
| Auth / TLS | None |
| Push / events | None — request/response only |
| Protocol version | 1 (advertised by hello) |
Environment overrides (native app):
| Variable | Effect |
|---|---|
DAGGERMAP_GOLEM |
Any non-empty value other than 0 forces the API on |
DAGGERMAP_GOLEM_PORT |
Overrides the listen port |
Semantics of success
A successful action or batch reply means the request was accepted for dispatch, not that the mutation has finished. On desktop, actions are queued and drained a few per frame. To confirm effects, follow up with a query (for example wait until golem.action_queue_count is 0) or wait a short moment.
Limits
| Limit | Value |
|---|---|
| Max request line | 64 KiB |
| Concurrent clients | 8 |
| Action queue | 256 |
| Batch size | 64 sub-requests |
| Drain per frame | 8 actions |
add_tokens per call |
64 |
Discover live limits and registered action ids via hello.
Message envelope
Every request is a JSON object. Every response is a JSON object on its own line.
Request
| Field | Type | Required | Description |
|---|---|---|---|
intent |
string | yes | hello | query | action | batch |
id |
string | no | Correlation id; echoed on the response when present |
| (intent fields) | See each intent below |
Response
Success:
{"id":"1","ok":true,"result":{}}
Failure:
{"id":"1","ok":false,"error":"missing action_id"}
| Field | Type | Notes |
|---|---|---|
ok |
boolean | Always present |
result |
object | array | … | Present on success (may be {}) |
error |
string | Present on failure — human-readable, not a numeric code |
id |
string | Echoed only if the request included id |
Errors are strings such as invalid JSON, unknown intent, path not found, action queue full, or action-specific validation messages (move_token requires id, no selection, …). The last failure is also mirrored under query path golem.last_error.
Quick start
- Enable Settings → General → Automation.
- Open a TCP connection to
127.0.0.1:8765. - Write one JSON object followed by
\n; read one JSON line back.
Wire example (send the left side; receive the right):
→ {"id":"1","intent":"hello"}
← {"id":"1","ok":true,"result":{"protocol":1,"app":"daggermap",…}}
→ {"id":"2","intent":"action","action_id":"set_viewport","args":{"zoom":1}}
← {"id":"2","ok":true,"result":{}}
→ {"id":"3","intent":"query","path":"session.viewport"}
← {"id":"3","ok":true,"result":{"origin_x":0,"origin_y":0,"zoom":1}}
Any language that can open a TCP socket and encode/decode JSON works. Treat the stream as NDJSON — do not assume one recv equals one message.
Intents
hello
Capability advertisement. Optional, but recommended as the first call.
Request
{"id":"1","intent":"hello"}
Result fields
| Field | Type | Description |
|---|---|---|
protocol |
number | Wire protocol version (1) |
app |
string | "daggermap" |
app_version |
string | Optional; may be omitted |
intents |
string[] | ["hello","query","action","batch"] |
features |
string[] | e.g. validate, batch, depth, path, budgeted_drain |
limits |
object | action_queue, batch, drain_per_poll |
actions |
string[] | Every registered action_id |
query
Read a filtered snapshot of app state. Queries run immediately (read lane).
Request
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | no | Dot path into the snapshot; omit or "" for the whole root |
depth |
number | no | Truncate nested depth; omit or -1 = full; 0 strips nested objects/arrays |
{"id":"q1","intent":"query","path":"session.viewport","depth":2}
Root keys (built only as needed for the requested path):
| Path | Contents |
|---|---|
golem |
Automation listener status and queue depth |
session |
Viewport, selection, document meta, grid, scene; at depth ≥ 2 also UI actions catalog and multiplayer/net |
document |
Saved document JSON; at depth ≥ 2 also tokens_by_id |
Unknown top-level paths return "path not found".
golem
| Field | Type | Description |
|---|---|---|
enabled |
boolean | Automation preference (or env override) wants the listener on |
active |
boolean | Listening and bind succeeded |
env_override |
boolean | Forced on via DAGGERMAP_GOLEM |
port |
number | Configured port |
listening |
boolean | Socket is listening |
bind_failed |
boolean | Port could not be bound |
client_connected |
boolean | At least one client connected |
client_count |
number | Connected clients |
request_count |
number | Requests handled since start |
action_queue_count |
number | Accepted actions not yet applied |
action_queue_cap |
number | Queue capacity |
drain_per_poll |
number | Max actions drained per frame |
bulk_insert_pending |
number | Tokens still waiting in the bulk insert queue |
bulk_insert_cap |
number | Bulk insert capacity |
last_error |
string | Last failure message (or "") |
session (depth ≥ 1)
| Path | Shape |
|---|---|
session.viewport |
{origin_x, origin_y, zoom} |
session.selection |
string[] of token UUID7 ids |
session.document_path |
string |
session.document_open |
boolean |
session.window_width / session.window_height |
number (framebuffer pixels) |
session.active_scene_id |
string |
session.measurements_count |
number |
session.background |
{loaded, tiled, width, height, asset_hash} |
session.grid |
{cell_size, visible, style, line_size, origin:{x,y}, detect:{…}} |
session.scene |
{projection_mode} — 0 top-down, 1 isometric |
At depth ≥ 2:
| Path | Shape |
|---|---|
session.actions |
[{id, label, section, description?}] — UI / Controls catalog |
session.net |
Multiplayer and asset-sync snapshot (room_id, broker_url, state, kind, peer/sync counters, downloads, …) |
document
Document save payload (schema_version, scenes, assets, drawings, …). When depth is omitted/-1 or ≥ 2, includes:
"tokens_by_id": {
"<uuid7>": {
"id": "<uuid7>",
"path": "…",
"world_position": {"x": 0, "y": 0},
"scale_x": 1,
"scale_y": 1,
"z_order": 0,
"name": "",
"fields": {}
}
}
Prefer narrow paths (session.selection, document.tokens_by_id.<id>) over dumping the whole root when scripting.
action
Validate and enqueue (or, on web, immediately run) a single mutation.
Request
| Field | Type | Required | Description |
|---|---|---|---|
action_id |
string | yes | Registered action name |
args |
object | no | Action parameters |
{"id":"a1","intent":"action","action_id":"add_token","args":{"path":"/path/to/token.png","x":0,"y":0}}
Success result: {}
Common errors: missing action_id, unknown action_id, action queue full, or a validator string.
batch
Send up to 64 nested requests in one round-trip. Nested intents may be hello, query, action, or nested batch.
Request
{
"id": "b1",
"intent": "batch",
"requests": [
{"id": "a1", "intent": "action", "action_id": "select_all_tokens"},
{"id": "q1", "intent": "query", "path": "session.selection"}
]
}
Success result: { "responses": [ <envelope>, … ] }
The outer envelope is ok: true even when some sub-requests fail — inspect each entry in responses.
Actions
Two families share the same action intent:
- Automation actions — purpose-built for scripting (tokens, viewport, draw, net, …).
- UI actions — the same ids shown under Settings → Controls, so anything bindable in the app can be invoked over the API.
Call hello (or query session.actions at depth ≥ 2) for the live catalog.
Coordinates
| Convention | Used by |
|---|---|
World x/y (default for placement & draw) |
add_token, add_tokens, move_token, draw_*, measure_* |
| Screen pixels | pan_view, zoom_*, select_token_under_cursor; add_token / add_tokens when space is "screen" |
| Point pairs | Arrays [x, y] under named keys (start/end, or aliases a/b) |
Colors are strings resolved like the in-app pen (named colors or #RRGGBB / #RRGGBBAA where supported).
Document & viewport
action_id |
Args | Notes |
|---|---|---|
open_document |
path (string, required) |
|
save_document_as |
path (optional) |
No path → no-op |
set_active_scene |
id (UUID7 string) and/or index (0-based integer) |
At least one required. index follows document.scenes / sort_order order. If both are set they must name the same scene. Does not persist the active scene back to the save file. |
load_background |
path (required) |
|
clear_background |
— | |
detect_grid |
— | Needs a non-tiled background; poll session.grid.detect |
set_viewport |
origin_x?, origin_y?, zoom? (> 0) |
Partial updates allowed |
set_projection_mode |
projection_mode 0|1 and/or isometric bool |
Needs an active scene |
set_gc_policy |
max_state_bytes?, tombstone_retention_days?, run_gc? (default true) |
At least one policy field |
Tokens
action_id |
Args | Notes |
|---|---|---|
add_token |
path or asset_hash; optional x,y; optional space="screen" |
No xy → spiral place; selects the new token |
add_tokens |
one of: dir; positions + shared path/hash; tokens[] |
Max 64; see shapes below |
move_token |
id, x, y |
World coords; id is UUID7 string |
set_token_scale |
id + (scale or scale_x+scale_y) |
|
copy_tokens / cut_tokens |
— | Requires selection |
paste_tokens |
— | Requires clipboard |
add_tokens shapes:
{"dir": "/path/to/folder"}
{"path": "/tok.png", "positions": [[0, 0], [72, 0]]}
{"tokens": [{"path": "/a.png", "x": 0, "y": 0}, {"path": "/b.png", "x": 72, "y": 0}]}
Prefer add_tokens over many tiny add_token calls when dumping assets in.
Draw & measure
World-space strokes. Point-pair shapes accept primary keys or a/b aliases.
action_id |
Args |
|---|---|
draw_freehand |
points (≥ 2 × [x,y]); optional color, thickness (default 6) |
draw_rectangle |
top_left/a + bottom_right/b; optional outline_color, fill_color, thickness |
draw_line |
start/a + end/b; optional colors / thickness |
draw_circle |
center/a + edge/b; optional colors / thickness |
draw_cone |
apex, base_mid; optional outline_color, thickness, angle_deg (default 90) |
draw_freehand_commit |
points or pattern (rdp_zigzag|sine) + point_count; optional zoom |
clear_my_drawings / clear_all_drawings / clear_history |
— |
measure_line |
start/a + end/b |
measure_freehand |
same point sources as freehand commit helpers |
clear_measurements |
— |
Multiplayer
action_id |
Args | Notes |
|---|---|---|
net_configure |
optional broker_url, room_id, display_name, room_key, kind, peer_candidate, stun_host, stun_port |
Empty room_key clears E2EE passphrase |
net_connect |
same as configure; needs room_id (here or previously) |
|
net_disconnect / net_retry |
— | |
net_set_peer_candidate |
peer_candidate |
|
cancel_bg_load / cancel_asset_sync |
— |
UI actions (Controls)
Every id under Settings → Controls is registered as an action_id. Examples:
| Section | Ids |
|---|---|
| Session | undo, redo, open_document, save_document_as |
| UI | back, select, toggle_hint_overlay, toggle_settings, toggle_drawer, open_action_assets, open_action_map, open_action_scenes, open_action_draw, open_action_measure, open_action_grid |
| Navigation | pan_view, zoom_in, zoom_out, pinch_zoom, reset_view, fill_view, fit_view_to_content |
| Tokens | select_token_under_cursor, select_all_tokens, clear_selection, open_token_details, set_token_scale_up, set_token_scale_down, remove_token, copy_tokens, cut_tokens, paste_tokens, move_token_north / _south / _east / _west |
| Draw | set_shape_freehand, set_shape_rectangle, set_shape_cone, set_shape_line, set_shape_circle, clear_all_drawings, clear_my_drawings |
| Menu | toggle_draw_mode, toggle_measure, cycle_draw_shape, toggle_snap_to_grid, toggle_background_tiled, toggle_isometric_view, toggle_grid_adjust_handles, toggle_video_controls |
Args for selected UI actions
| Action | Args |
|---|---|
pan_view |
phase: "down" | "move" | "up" (default "down"); optional screen x,y (default window center). move requires a prior down. |
pinch_zoom |
scale (number); optional screen x,y |
zoom_in / zoom_out |
optional screen x,y (focal point) |
select_token_under_cursor |
optional screen x,y |
move_token_* |
optional steps (default 1); requires selection |
Selection-required actions fail validation with "no selection": move_token_*, remove_token, set_token_scale_up / _down, cut_tokens, copy_tokens.
Examples
Place a token and read it back
→ {"id":"1","intent":"action","action_id":"add_token","args":{"path":"/tmp/goblin.png","x":144,"y":72}}
← {"id":"1","ok":true,"result":{}}
→ {"id":"2","intent":"query","path":"session.selection"}
← {"id":"2","ok":true,"result":["018f…"]}
→ {"id":"3","intent":"action","action_id":"move_token","args":{"id":"018f…","x":216,"y":72}}
← {"id":"3","ok":true,"result":{}}
Draw a measured line
→ {"id":"4","intent":"action","action_id":"draw_line","args":{"start":[0,0],"end":[144,0],"outline_color":"#c44","thickness":4}}
← {"id":"4","ok":true,"result":{}}
Validation failure
→ {"id":"5","intent":"action","action_id":"move_token","args":{"x":1}}
← {"id":"5","ok":false,"error":"move_token requires id"}
Wait for the write queue to drain
→ {"id":"6","intent":"query","path":"golem.action_queue_count"}
← {"id":"6","ok":true,"result":0}