holmd records what is done to each box in a trace. You can read the trace to see what happened, and fork a box to make a new box that does the same steps again.
Traces and forks need a server. The Rust library with no server has no trace. See The server.
What the trace records
Each entry has a sequence number (seq), a time (at_ms), an actor, an event, and sometimes a frame hash.
| Actor | Meaning |
|---|---|
agent |
A client of the API: the CLI, an MCP tool, or a program. |
person |
A person who has the screen during a takeover. |
system |
The server, for example when it removes an expired box. |
Event (kind) |
Recorded when |
|---|---|
box_created |
The box is created. Contains the full spec and placement. |
forked_from |
The box is a fork. Names the source box and up_to. |
adopted |
A server took the box back after a restart. |
acted |
An action ran, with the full action, ok, and the error if it failed. |
frame |
The screen changed. The entry has the frame hash. |
executed |
A command ran, with its arguments, exit code, and timed_out. |
app_launched |
An application was opened, with its window. |
apps_installed |
Applications were installed into the running box. |
file_written, file_read |
A file was written or read, with the path and size. Not the content. |
clipboard_set, clipboard_read |
The clipboard was set or read. Not the text. |
page_captured |
A page screenshot or PDF was made. |
takeover_started, takeover_ended |
A person got the screen, and gave it back. |
box_paused, box_resumed, box_stopped, box_started |
Lifecycle changes. |
gone |
The box was removed by the server, with the reason. |
box_deleted |
A client removed the box. |
What is not recorded
- A person's input. It goes to the box over VNC, so the server does not see it. The trace records only the period when the person had the screen. The frames in that period show what changed.
- Page actions outside a batch.
holm browsercommands and the MCP page tools (click_element,fill_field,dropdown, and others) are not in the trace. Page actions inside an action batch are. See Make a run that you can fork. - Reads.
read,snapshot,find, andevaluatethrough their own endpoints are not recorded.
The actor of a frame entry is the one that had the screen when the frame was captured, not the one that changed it. A frame is recorded only when the screen changed, so a still screen adds no entries. No frame entry between takeover_started and takeover_ended means that nothing visible changed during the takeover.
Read the trace
holm trace "$BOX"
holm trace "$BOX" --after 40 0 agent created 1280x800
1 agent screen 0 open_url
2 agent screen 0 the screen changed
3 agent screen 0 handed to a person
4 person screen 0 the screen changed
5 agent screen 0 taken back
6 agent ran ls /tmp → 0REST:
curl -s "$BASE/v1/boxes/$BOX/trace?after=12&limit=100"
curl -s "$BASE/v1/boxes/$BOX/trace/frames/$HASH" -o frame.pngThe response has entries and next. To read more, send next as after. A page has at most 500 entries.
A trace stays after its box is removed.
Make a run that you can fork
A fork does again only some of what the trace has:
| Interface | Replayed by a fork | Recorded, but not replayed |
|---|---|---|
| CLI | open, mouse, keyboard, wait, app, exec, batch |
clip (set), file put |
| MCP | open_url, click, type_text, press_key, drag, scroll, wait_until_still, open_app, run_command, batch |
clipboard (set), write_file |
| REST | Each action in POST …/actions, and POST …/exec |
PUT …/clipboard, PUT …/files, POST …/apps |
To make page actions part of a run that you can fork, put them in a batch:
cat > steps.json <<'JSON'
[
{ "type": "open_url", "url": "https://example.com/order" },
{ "type": "on_page", "what": { "op": "fill", "query": "Name", "text": "Ada" } },
{ "type": "on_page", "what": { "op": "click", "query": "Continue" } },
{ "type": "on_page", "what": { "op": "wait_for", "query": "Details" } }
]
JSON
holm batch "$BOX" steps.jsonMCP: the batch tool, with steps such as fill_field and click_element.
Each step of a batch becomes its own acted entry.
Fork a box
NEW=$(holm fork "$BOX")
NEW=$(holm fork "$BOX" --up-to 40)REST:
curl -s -X POST "$BASE/v1/boxes/$BOX/fork" \
-H 'content-type: application/json' \
-H 'idempotency-key: fork-1' \
-d '{"up_to": 40}'| Field | Effect |
|---|---|
up_to |
Do the steps up to this sequence number. Default: all. |
placement |
A placement for the new box. It replaces the source placement completely. |
mode |
replay (default). snapshot is refused. |
MCP: fork_box. It forks the full trace. It has no up_to or placement.
The server:
- Reads the spec and placement from the
box_createdentry. - Creates a new box on the same runtime, unless your
placementnames another. The new box does not get the source's browser profile. - Records
box_createdandforked_fromin the new trace. - Does the recorded steps again, in order, at about the original speed. A wait between two steps is at most 2 seconds.
- Captures a frame at the end.
You can fork a box that was removed, because its trace stays.
The result
{
"box": { "id": "box_…", "state": "ready" },
"replay": {
"attempted": 3,
"ok": 3,
"truncated": false,
"skipped": [
{ "seq": 2, "kind": "file_written", "why": "the trace records that /tmp/marker.txt was written, not what went into it" }
]
}
}| Field | Meaning |
|---|---|
attempted, ok |
How many steps ran, and how many succeeded. |
stopped_at |
The source seq of the step that failed. The fork stops at the first failure. |
truncated |
true if the fork stopped after 3 minutes. |
skipped |
Steps that the fork could not do again, with the reason. |
The CLI prints the new box ID. The new box stays, even when the replay stopped early.
Why a fork can be different
A fork does the steps again. It does not copy memory or disk. The result can differ from the source because:
- The page can be different now.
- The network and page timing can be different.
- A dialog or a popup can appear this time and not the first time.
- Animations make two screens of the same state look different.
Compare the last frame of each trace if you must know how close the fork is.
What the fork does not do again:
| Entry | Why |
|---|---|
acted with ok: false |
The source refused it, so it did not happen. |
file_written |
The trace does not keep the content. Listed in skipped. |
clipboard_set |
The trace does not keep the text. Listed in skipped. |
apps_installed |
Not done again, and not listed in skipped. Put applications in the spec so that the fork's image has them. |
| A person's input | Not in the trace. |
Keep traces across restarts
The default memory store loses all traces when the server stops. It also keeps at most 10,000 entries and 256 frames for each box, and traces for at most 256 boxes.
After a restart with the memory store, the server takes its boxes back, but each trace starts again with adopted. A fork of such a box does only the steps after the restart.
Use a durable store to keep traces:
HOLM_STORAGE_BACKEND=local HOLM_STATE_DIR=/var/lib/holm holmd| Variable | Default | Effect |
|---|---|---|
HOLM_KEEP_FRAMES_SECS |
2 hours | How long to keep frames. |
HOLM_KEEP_ENTRIES_SECS |
7 days | How long to keep trace entries. |
HOLM_PRUNE_SECS |
1 hour | How often to remove old data. |
A fork needs the frames only for comparison, not to replay, so old frames can go before the entries. See Storage.