# Program format (robot.v4.program.v2)

> The robot.v4.program.v2 document that OLP, the pendant and the weld planner share, with its top-level fields, node types, the single-line ordering rule, weld geometry, taught moves, units and digests.

URL: https://advancedmetalresearch.com/docs/reference/program-format
Section: RosieOS docs / Reference
Last updated: 2026-10-10

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`](/docs/reference/rdt-format) 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.

bracket_fillet.program.json:

```json
{
  "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](https://advancedmetalresearch.com/docs/reference/program-format#digests).

## Validate a document

The TypeScript module exports the validator. With Node 22 or later, from `offline-programming/v1/ui`:

check-program.mjs:

```js
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 accepted
```

```bash
node --experimental-strip-types check-program.mjs bracket_fillet.program.json
```

The 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](https://advancedmetalresearch.com/docs/reference/program-format#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](https://advancedmetalresearch.com/docs/reference/program-format#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:

```text
home? approach weld (transit weld)* retract home?
```

- A `weld` is preceded by an `approach` or `transit` and followed by a `retract` or `transit`.
- A `transit` sits between two welds.
- An `approach` is followed by a weld, and may follow nothing, a `home`, a `move` or a `retract`.
- A `retract` follows a weld, and may be followed by nothing, a `home`, a `move` or an `approach`.
- A `home` may only start or end the program.
- `move` nodes 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](https://advancedmetalresearch.com/docs/reference/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_standoff` is not allowed on `approach` or `home`, since nothing is welded before them.
- `arrive_standoff` is not allowed on `retract` or `home`, 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_rad` or `path`.

### 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](https://advancedmetalresearch.com/docs/guides/teach-waypoints) 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](https://advancedmetalresearch.com/docs/reference/program-format#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 with `row_hash` removed.
- **`program_digest`**: the whole document with `program_digest` removed. 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.

## Related pages

- [Motion paths and planning](https://advancedmetalresearch.com/docs/concepts/motion-and-planning)
- [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification)
- [Offline programming HTTP API](https://advancedmetalresearch.com/docs/apis/olp-http#weld-plan)
- [Dense trajectory (.rdt) format](https://advancedmetalresearch.com/docs/reference/rdt-format)

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `offline-programming/v1/ui/src/contracts/robot-v4-program-v2.ts:26-58,63-332,352-525,527-563,565-605,607-691,714-873,882-902`
- `offline-programming/v1/ui/src/contracts/robot-v4-program.ts:15-46,112-124,653-664`
- `steamdeck/real/v4/contracts/programs/robot.v4.program.v2.schema.json`
- `steamdeck/real/v4/contracts/programs/fixtures/complete-program-v2.json`
- `weld_planner/v1/python/weldplan/program_v2.py:1-62`
- `offline-programming/v1/weld_plan.go:47-77`
