Program format (robot.v4.program.v2)
On this page
A robot.v4.program.v2 document is an authored robot program: an ordered list of nodes that say what the robot should do, not how. Welds carry their seam geometry, freespace moves carry constraints, taught moves carry a destination, and events (I/O and dwell) do not move. It never contains a solved trajectory. The weld planner turns it into joint trajectories and a .rdt file.
OLP, the Steam Deck v5 pendant and the weld planner all read and write this format. The TypeScript validator is offline-programming/v1/ui/src/contracts/robot-v4-program-v2.ts. Its twins are the Go test steamdeck/real/v4/contracts/programs/program_v2_test.go, the Python module weld_planner/v1/python/weldplan/program_v2.py and the JSON Schema steamdeck/real/v4/contracts/programs/robot.v4.program.v2.schema.json.
Example#
A valid program with one weld: Home, approach, weld, retract, a dwell, Home.
{
"schema": "robot.v4.program.v2",
"program_id": "bracket_fillet",
"program_name": "Bracket fillet, one pass",
"program_revision": 1,
"base_program_revision": 0,
"program_state": "saved_unplanned",
"execution_mode": "simulation",
"created_utc": "2026-09-29T09:00:00Z",
"units": {"length": "m", "angle": "rad", "time": "s",
"joint_order": ["J1", "J2", "J3", "J4", "J5", "J6", "J7", "J8", "J9"]},
"producer": {
"component": "offline-programming-v1",
"source_revision": "local",
"inputs": [{"kind": "project", "ref": "bracket",
"sha256": "4f6c2d7a0b3e9f18c5a6d2e7b1f0c3a9d8e4b7f2a1c6d5e0f9b8a7c6d5e4f3a2"}]
},
"workpiece": {"frame_id": "bracket_origin"},
"placement": {"cell_id": "rosie_1400_v3", "anchor_xyz_m": [0, 0, 0],
"offset_xyz_m": [0.1, 0, 0.1], "rpy_rad": [0, 0, 0]},
"robot": {"model_id": "rosie_1400_v3",
"robot_description_sha256": "sha256:b2103adffd53cb9a66db1edf842ae1cb34173f1c8ff83c3b30ff7c8dacee9b0a"},
"defaults": {"clearance_min_mm": 20, "speed_scale": 0.5, "torch_policy": "free", "positioner_policy": "hold"},
"nodes": [
{"id": "home-start", "op": "home", "enabled": true, "label": "Home", "comment": "", "row_hash": "sha256:…"},
{"id": "approach-1", "op": "approach", "enabled": true, "label": "Approach", "comment": "", "row_hash": "sha256:…",
"arrive_standoff": {"direction": "torch_axis", "distance_mm": 30}},
{"id": "weld-1", "op": "weld", "enabled": true, "label": "Fillet A", "comment": "", "row_hash": "sha256:…",
"geometry": {"kind": "manual_pose_wp", "frame_id": "bracket_origin", "points": [
{"xyz_m": [0.0, 0.0, 0.01], "rpy_rad": [3.14159, 0.0, 0.0], "continuity": "C0"},
{"xyz_m": [0.1, 0.0, 0.01], "rpy_rad": [3.14159, 0.0, 0.0], "continuity": "C0"}]},
"travel_speed_mm_s": {"kind": "constant", "value": 8.0},
"standoff_mm": {"kind": "constant", "value": 15.0},
"weave": {"shape": "none"},
"weld_parameters": {}},
{"id": "retract-1", "op": "retract", "enabled": true, "label": "Retract", "comment": "", "row_hash": "sha256:…",
"depart_standoff": {"direction": "torch_axis", "distance_mm": 30}},
{"id": "settle", "op": "dwell", "enabled": true, "label": "Settle", "comment": "", "row_hash": "sha256:…",
"duration_s": 0.5},
{"id": "home-end", "op": "home", "enabled": true, "label": "Home", "comment": "", "row_hash": "sha256:…"}
],
"planner_owns": [
"the joint-space solution of every freespace node -- this document states constraints, never a path",
"time parametrization and dynamics"
]
}row_hash is shown elided. A real one is the node's digest; see Digests.
Validate a document#
The TypeScript module exports the validator. With Node 22 or later, from offline-programming/v1/ui:
import { readFileSync } from "node:fs";
import { programV2Problems } from "./src/contracts/robot-v4-program-v2.ts";
const doc = JSON.parse(readFileSync(process.argv[2], "utf8"));
console.log(programV2Problems(doc)); // [] when the document is acceptednode --experimental-strip-types check-program.mjs bracket_fillet.program.jsonThe validator stops at the first problem and states it as a sentence, for example weld "weld-1" is not preceded by an approach or transit (an approach is missing). programV2LineProblems returns every ordering problem at once.
Units#
units is fixed: lengths in m, angles in rad, time in s, and joint_order is J1…J9. Fields that use another unit say so in their name: _mm, _mm_s, _deg and _s. The UI shows poses in mm and degrees, but the document stores m and rad.
Top-level fields#
| Field | Type | Required | Description |
|---|---|---|---|
schema | string | yes | robot.v4.program.v2 |
program_id | string | yes | Stable program identity |
program_name | string | yes | Display name |
program_revision | integer ≥ 1 | yes | This revision |
base_program_revision | integer ≥ 0 | yes | The revision this one was edited from |
program_digest | string | no | sha256:<64 hex>; see Digests |
program_state | string | yes | draft, saved_unplanned, needs_plan, planning, planned, accepted_simulation, accepted_physical, execution_ready, running, completed, stopped or faulted |
execution_mode | string | yes | simulation, dry_run or production |
created_utc | string | yes | RFC 3339 timestamp |
units | object | yes | See Units |
producer.component | string | yes | offline-programming-v1 or weld-planner-v1 |
producer.source_revision | string | yes | Revision of the producing code |
producer.inputs[] | array | yes | At least one {kind, ref, sha256}. kind is project, weld_program, cad, cell or tool; sha256 is 64 hex characters without a prefix. |
source | object | no | Where the geometry came from, for example a STEP file and its hash |
workpiece.frame_id | string | yes | The workpiece frame. Seams and hand-placed weld poses are in this frame. |
workpiece.solids[] | array | no | Solids from the CAD import |
workpiece.weld_joints[] | array | no | Weld joints that a seam's weld_joint_ref resolves against. IDs must be unique. |
placement.cell_id | string | yes | Must equal robot.model_id |
placement.anchor_xyz_m, offset_xyz_m | 3 numbers, m | yes | Where the workpiece sits in the description's work frame |
placement.rpy_rad | 3 numbers, rad | yes | Workpiece orientation |
robot.model_id | string | yes | Robot model, for example rosie_1400_v3 |
robot.robot_description_sha256 | string | yes | sha256:<64 hex> identity of the robot description. The all-zero value means the producer had no description to pin to. |
tool | object | no | {id, sha256} of the torch asset |
speed_scale | number | no | Whole-program speed multiplier applied after planning, 0.01–1. Default 1. |
defaults | object | yes | The freespace policy; see below. All four keys are required. |
nodes[] | array | yes | 1–4,096 nodes |
planner_owns[] | array of strings | yes | At least one sentence naming something this document deliberately leaves to the planner |
extensions | object | no | Producer-specific data |
Freespace policy#
defaults sets the policy for every freespace node. A node's overrides replaces any subset of it.
| Key | Type | Description |
|---|---|---|
clearance_min_mm | number ≥ 0, mm | Minimum clearance for freespace motion |
speed_scale | number in (0, 1] | Freespace speed fraction |
torch_policy | string | free, hold_last or torch_down |
positioner_policy | string | free (the planner may move the positioner) or hold |
Nodes#
Every node has these fields, and only the fields its op allows. An unknown key is refused.
| Field | Type | Description |
|---|---|---|
id | string | Unique within the program |
op | string | weld, approach, transit, retract, home, move, dwell or io |
enabled | boolean | |
label, comment | string | May be empty |
row_hash | string | Non-empty; the node digest |
extensions | object | Optional |
op | Kind | Extra fields |
|---|---|---|
weld | Motion | geometry, travel_speed_mm_s, standoff_mm, weave, weld_parameters, optional weld_preset_id |
approach, transit, retract, home | Freespace motion | Optional depart_standoff, arrive_standoff, overrides |
move | Taught motion | motion, target, speed_scale, speed_mm_s, capture; optional target_space, via, acceleration_scale, constant_tcp_speed |
dwell | Event | duration_s (≥ 0, s) |
io | Event | channel (non-empty string), state (boolean) |
The single line#
The motion nodes, ignoring dwell and io, must form one line:
home? approach weld (transit weld)* retract home?- A
weldis preceded by anapproachortransitand followed by aretractortransit. - A
transitsits between two welds. - An
approachis followed by a weld, and may follow nothing, ahome, amoveor aretract. - A
retractfollows a weld, and may be followed by nothing, ahome, amoveor anapproach. - A
homemay only start or end the program. movenodes may appear before, after or between complete weld blocks.
Weld nodes#
| Field | Type | Description |
|---|---|---|
geometry | object | seam, posed_polyline or manual_pose_wp; see below |
travel_speed_mm_s | scalar function, mm/s | Travel speed along the weld |
standoff_mm | scalar function, mm | Contact-tip standoff. Required, never defaulted, and positive everywhere. |
weave | object | {shape, …}; shape is required, for example none |
weld_preset_id | string | Optional label of the preset the values started from. The values are the authority. |
weld_parameters | object | Reserved. Must be present and empty in v2. |
A scalar function is a value over normalised arc length s in [0, 1]:
kind | Fields |
|---|---|
constant | value |
piecewise_linear | knots: [s, value] pairs from s = 0 to s = 1, strictly increasing |
bspline | degree, knots_u, control_values; knots_u has control_values + degree + 1 entries |
samples | s and values, the same length, at least two |
Geometry kinds#
seam is the weld planner's seam. It needs id, reference and orientation (work_angle_deg and travel_angle_deg as scalar functions, in degrees). Its line is either an inline curve or the weld_line of the joint named by weld_joint_ref. An inline curve is a list of B-spline segments, each with degree, control_points_xyz_m and clamped, non-decreasing knots_u (poles + degree + 1 entries), and optional weights. Optional fields include span (start_s, end_s; wrapping through 0 only on a closed curve), direction (forward or reverse), lead (lead_in_mm, lead_out_mm) and search_provenance, the record of the angle search. See the weld program format for the seam model.
posed_polyline is the classic flow's weld: frame_id and at least two points, each {xyz_m, rpy_rad}. The RPY on each point is the authority.
manual_pose_wp is a weld placed by hand as torch poses. frame_id must be the workpiece frame. Each point has xyz_m (the work point, m), rpy_rad (torch orientation, URDF fixed-axis; tool +Z points from the gun into the work) and continuity (C0, C1 or C2; ignored on the first and last point). Consecutive points must be more than 1 µm apart. shape is spline (the default: chord-length cubic Hermite segments) or arc (exactly three non-collinear points on one circle).
Freespace nodes#
approach, transit, retract and home state constraints only. A standoff is {direction, distance_mm}, where direction is torch_axis, workpiece_z or a 3-vector, and distance_mm ≥ 0.
depart_standoffis not allowed onapproachorhome, since nothing is welded before them.arrive_standoffis not allowed onretractorhome, since nothing is welded after them.- A freespace node may not carry path vocabulary:
knots,knots_u,times_s,control_points_xyz_m,segments,samples,points,poses,rpy_radorpath.
Move nodes#
A taught destination, with the observation it was taught from.
| Field | Type | Description |
|---|---|---|
motion | string | joint (MoveJ), linear (MoveL) or circular (MoveC) |
target | TeachPose | The destination |
via | TeachPose | Optional; the through point of a circular move |
target_space | string | Optional. cartesian re-solves IK from the pose at planning; joint or absent keeps the recorded joint values. |
speed_scale | number in (0, 1] | Joint speed fraction |
speed_mm_s | number > 0, mm/s | TCP speed for linear and circular moves |
acceleration_scale | number in (0, 1] | Optional joint acceleration fraction. Absent, it follows speed_scale. |
constant_tcp_speed | boolean | Optional, default true: linear and circular moves cruise at TCP speed, with ramps |
capture.source | string | machine (taught on a connected cell) or preview (taught in the viewer) |
capture.observed_at | string | RFC 3339 timestamp |
capture.model_id, capture.robot_description_sha256 | string | The robot it was taught on |
capture.machine_planning_calibration_sha256 | string | Optional, sha256:<64 hex> |
capture.cell_id | string | Required when source is machine |
capture.pose | TeachPose | The pose as observed |
A TeachPose is {frame_id: "world", xyz_m, rpy_rad, joint_names, joint_values_rad}. xyz_m and rpy_rad are the TCP pose in m and rad. joint_names lists 1–9 unique names and joint_values_rad the matching values in rad. No other keys are allowed.
See Teach waypoints and moves for how the UI records them.
Forbidden keys#
The document states intent, never a solution. These keys are refused anywhere in it, at any depth: joints, joint_positions_rad, target_joints_rad, q_rad, qd_rad_s, target_tcp_m, tcp_pose_m, trajectory, planned_path, accepted_plan, preloaded_plan, waypoints and rows. Freespace nodes and defaults also refuse the path vocabulary listed under Freespace nodes.
Digests#
Both digests are sha256:<64 hex> over canonical JSON: keys sorted at every level, no whitespace, non-finite numbers refused.
row_hash: the node withrow_hashremoved.program_digest: the whole document withprogram_digestremoved. The validator runs first.
The TypeScript module computes them with computeProgramV2NodeDigest(node) and computeProgramV2Digest(program).
OLP sends the program digest with a plan request and echoes it in the plan response. It is not the program_digest in the resulting .rdt header: there the planner writes sha256: + the SHA-256 of the .weldplan request it was given, and plan_id is <program_id>:<first 12 hex of that digest>. So replanning the same program gives a new plan identity, and resending the same request gives the same one.