clips
Record gameplay clips and query them frame by frame.
The clips tool is the backbone of the dashcam workflow. It writes every physics frame of spatial data to disk, so you can scrub through a timeline of exactly what happened — positions, velocities, and properties — at any frame in the recording.
When to use it
- Capturing a bug: record while playing, mark the bug moment, analyze the clip
- Post-mortem analysis: something went wrong in a playtest — query what happened
- Regression testing: record expected behavior, compare against future runs
- Long sessions: in-memory ring buffer holds ~10 seconds; recordings hold hours
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action | any"add_marker" | "save" | "status" | "list" | "delete" | "markers" | "snapshot_at" | "trajectory" | "query_range" | "diff_frames" | "find_event" | "screenshot_at" | "screenshots" | required | Action to perform. |
at_frame | number | optional | Frame number for snapshot_at. |
at_time_ms | number | optional | Timestamp (ms) for snapshot_at. Finds nearest frame. |
clip_id | string | optional | Clip to operate on (from list response). Defaults to most recent clip if omitted. |
condition | any | optional | Condition for query_range. Object with "type" key. Types: "moved" (threshold), "proximity" (target, threshold), "velocity_spike" (threshold), "property_change" (property), "state_transition" (property), "signal_emitted" (signal), "entered_area", "collision". Example: {"type": "proximity", "target": "walls/*", "threshold": 0.5} |
detail | string | optional | Detail level for snapshot_at: "summary", "standard", "full". |
event_filter | string | optional | Event filter for find_event (substring match). |
event_type | string | optional | Event type for find_event. |
frame_a | number | optional | Frame A for diff_frames. |
frame_b | number | optional | Frame B for diff_frames. |
from_frame | number | optional | Start of frame range for query_range / find_event. |
marker_frame | number | optional | Frame to attach marker to (add_marker). Defaults to current. |
marker_label | string | optional | Marker label (add_marker, save). |
node | string | optional | Node path for query_range. |
properties | string[] | optional | Properties to sample in trajectory. Default: ["position"]. Options: position, rotation_deg, velocity, speed, or any state property name. |
sample_interval | number | optional | Sample every Nth frame for trajectory. Default: 1. |
to_frame | number | optional | End of frame range for query_range / find_event. |
token_budget | number | optional | Soft token budget. |
Actions
start
Begin recording. A new clip is created and data is written to disk on every physics tick.
{
"action": "start",
"clip_id": "chase_bug_01"
}If clip_id is omitted, a unique ID is generated automatically (e.g., clip_1741987200).
Response:
{
"action": "start",
"clip_id": "chase_bug_01",
"result": "ok",
"record_path": "/tmp/theatre-clips/chase_bug_01.clip"
}stop
Stop the current recording.
{
"action": "stop"
}Response:
{
"action": "stop",
"clip_id": "chase_bug_01",
"result": "ok",
"frame_count": 512,
"duration_ms": 8533,
"file_size_bytes": 204800
}mark
Mark the current frame as a point of interest (e.g., "bug happened here"). This is what the F9 key triggers from the editor dock.
{
"action": "mark",
"label": "player_clips_wall"
}Response:
{
"action": "mark",
"clip_id": "chase_bug_01",
"frame": 337,
"label": "player_clips_wall",
"result": "ok"
}list
List all available clips.
{
"action": "list"
}Response:
{
"clips": [
{
"clip_id": "chase_bug_01",
"frame_count": 512,
"duration_ms": 8533,
"created_at": "2026-03-12T14:30:00Z",
"markers": [
{ "frame": 337, "label": "player_clips_wall" }
]
}
]
}query_frame
Get the complete spatial state at a specific frame.
{
"action": "query_frame",
"clip_id": "chase_bug_01",
"frame": 337,
"nodes": ["Player", "Wall_East"],
"detail": "full"
}| Parameter | Type | Description |
|---|---|---|
clip_id | string | Which clip to query |
frame | integer | Frame number (0-based) |
nodes | string[] | Limit to these nodes (optional) |
detail | string | "summary" or "full" |
Response:
{
"clip_id": "chase_bug_01",
"frame": 337,
"timestamp_ms": 5617,
"nodes": {
"Player": {
"class": "CharacterBody3D",
"global_position": [8.92, 0.0, -3.14],
"velocity": [45.3, 0.0, 0.0]
},
"Wall_East": {
"class": "StaticBody3D",
"global_position": [9.0, 0.0, -3.0]
}
}
}query_range
Query multiple consecutive frames at once. The primary tool for analyzing a bug across time.
{
"action": "query_range",
"clip_id": "chase_bug_01",
"start_frame": 325,
"end_frame": 350,
"nodes": ["Player"],
"detail": "summary",
"stride": 1
}| Parameter | Type | Default | Description |
|---|---|---|---|
clip_id | string | required | Which clip to query |
start_frame | integer | required | First frame (inclusive) |
end_frame | integer | required | Last frame (inclusive) |
nodes | string[] | all | Nodes to include |
detail | string | "summary" | Data level per node |
stride | integer | 1 | Sample every N frames |
condition | object | null | Filter frames by condition |
Condition filtering:
Use condition to include only frames where something specific is true:
{
"condition": {
"type": "proximity",
"nodes": ["Player", "Wall_East"],
"max_distance": 1.0
}
}Other condition types:
{ "type": "velocity_above", "node": "Player", "threshold": 20.0 }— frames where the node's speed exceeds threshold{ "type": "property_equals", "node": "Player", "property": "on_floor", "value": false }— frames where a property matches a value
delete
Delete a clip file from disk.
{
"action": "delete",
"clip_id": "chase_bug_01"
}Response:
{
"action": "delete",
"clip_id": "chase_bug_01",
"result": "ok"
}Example conversation
Tips
Always specify nodes in query_range. Even small clips with all nodes and detail: full can be enormous. Filter to the 2-3 nodes relevant to the bug.
Use stride: 5 for long recordings. Instead of every frame, sample every 5th frame for a quick scan. Then use query_frame to drill into specific moments.
Use conditions to filter. The proximity condition is especially powerful — it finds frames where two nodes are closer than a threshold, which is exactly when collision bugs occur.
Markers are set automatically with F9. In the editor dock, pressing F9 calls clips { "action": "mark" } with a default label. You can also add more labels by calling the tool directly during a session.