# Weld program and `.weldplan` container

> The seam model that weld nodes use (curves, arc length, the seam frame, work and travel angles, bands and tolerances), the .weldplan plan request container and its checks, and the seam worker's stdin protocol.

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

A weld is described in three layers:

1. **The seam model**: how one weld's line and torch angles are written. Weld nodes in a [`robot.v4.program.v2`](/docs/reference/program-format) document use it for their `geometry` when `kind` is `seam`.
2. **The `.weldplan` container**: the program plus everything a planner needs to plan it, packed into one file with digests. This is what the [weld planner](https://advancedmetalresearch.com/docs/apis/weld-planner-http) accepts.
3. **The seam worker protocol**: the JSON operations that detect seams in a STEP file, search torch angles and build the container.

A weld program says **what weld is required**, never how a robot gets there. It holds no joint values, no poses the robot must reach and no robot state. Rotation of the torch about its own electrode axis is left free by default, for the planner to use.

## The weld-program.v1 schema

`weld_planner/v1/schema/amr-weld-planner-v1.weld-program.v1.schema.json` is the original standalone weld program schema. It is now the **source of the seam vocabulary**, not a format the planner reads:

- program.v2 copies its definitions of `identifier`, `vec3`, `unit_vec3`, `scalar_function`, `analytic`, `curve_segment`, `curve`, `orientation_limit_axis`, `seam`, `weld_joint`, `groove_face`, `weave`, `source` and `workpiece`. Tests keep the copies identical.
- Two differences, both deliberate: in program.v2 a seam has no `process` block (travel speed, standoff and weave are fields of the weld node), and `weld_joints` sits inside `workpiece` rather than at the top level.
- A `.weldplan` whose `program.json` is a `weld-program.v1` document is refused. It has no weld nodes, so it would otherwise open and plan nothing.

## Conventions

- **Units are in field names**: `_m`, `_mm`, `_deg`, `_rad`, `_mm_s`, `_l_min`. Geometry is in metres, process quantities in millimetres, angles authored by people in degrees.
- **Workpiece frame only.** All seam geometry is in the frame named by `workpiece.frame_id`, which must be `workpiece` in a program with welds. The program's `placement` says where that frame sits on the cell.
- **No baked sampling.** Anything that varies along a seam is a function of normalised arc length, never a per-sample array. The planner chooses the sampling.
- **Identifiers** match `^[A-Za-z_][A-Za-z0-9_:.-]*$`.

## Curves

Every curve is a list of clamped, optionally rational B-spline segments joined end to end.

| Field | Type | Required | Description |
|---|---|---|---|
| `segments[]` | array | yes | The segments, in order |
| `closed` | boolean | no | The last segment's end meets the first segment's start |
| `length_m` | number, m | no | Cached total length. Derived and not authoritative. |

Each segment:

| Field | Type | Required | Description |
|---|---|---|---|
| `degree` | integer, 1–7 | yes | 1 with two control points is a line; 2 rational is an exact arc or conic |
| `control_points_xyz_m` | array of 3-vectors, m | yes | At least two |
| `knots_u` | array of numbers | yes | Non-decreasing and clamped. Length is control points + degree + 1. |
| `weights` | array of numbers > 0 | no | Omit for a non-rational spline. One per control point. |
| `continuity_to_next` | string | no | `C0`, `G1`, `C1` or `C2`. Advisory: `C0` marks a corner. |
| `analytic` | object | no | The exact line, circle or arc, for readability. The B-spline wins if they disagree. |
| `length_m` | number, m | no | Cached, derived |
| `id`, `source_edge_id` | string | no | Identity and the CAD edge it came from |

A line is degree 1, two control points, `knots_u` `[0, 0, 1, 1]`. A quarter arc is degree 2, three control points, `weights` `[1, 0.7071, 1]`, `knots_u` `[0, 0, 0, 1, 1, 1]`.

### Arc length

The B-spline parameter `u` is not a distance. Everything along a seam is defined on **normalised arc length** `s` in [0, 1] over the whole curve: `s = 0.5` is halfway along the weld, whatever the segments look like. Travel speed is mm/s of arc length.

### Scalar functions

A quantity that varies along the seam, such as an angle or a speed, is one of:

| `kind` | Fields |
|---|---|
| `constant` | `value` |
| `piecewise_linear` | `knots`: `[s, value]` pairs, `s` strictly increasing from 0.0 to 1.0 |
| `bspline` | `degree` (≥ 1), `knots_u`, `control_values` |
| `samples` | `s` and `values` (at least two each), optional `interpolation`: `linear` (default), `cubic` or `step` |

## Seams

A seam is one pass of one weld.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | identifier | yes | Unique |
| `reference` | object | yes | Where the 0° work-angle direction comes from; see [The seam frame](https://advancedmetalresearch.com/docs/reference/weld-program-format#seam-frame) |
| `orientation` | object | yes | `work_angle_deg`, `travel_angle_deg`, optional `spin` and `limits` |
| `curve` | curve or reference | no | The line the electrode tip follows: inline, or the `weld_line` of the joint in `weld_joint_ref` |
| `weld_joint_ref` | identifier or null | no | The weld joint this seam is on. Null means hand-authored. |
| `span` | object | no | `start_s`, `end_s`: weld only part of the curve. `start_s > end_s` wraps through 0, on a closed curve only. |
| `direction` | string | no | `forward` or `reverse` along the curve |
| `pass` | object | no | `index`, `role` (`single`, `root`, `fill`, `cap`, `tack`), `offset_in_frame_rb_m` |
| `weld_geometry` | object | no | ISO 2553 sizing: `leg_length_mm`, `throat_mm`, `root_gap_mm`, `bevel_angle_deg`, `intermittent` |
| `lead` | object | no | `lead_in_mm`, `lead_out_mm` along the tangent |
| `tolerance` | object | no | `position_mm`, `work_angle_deg`, `travel_angle_deg`, each > 0 |
| `sampling_hint` | object | no | Advisory only: `max_chord_deviation_mm`, `max_segment_mm`, `min_segment_mm` |
| `origin` | object | no | Why this seam is a separate piece: `group_id`, `index_in_group`, `group_size`, `split_reason` (`none`, `reachability`, `continuity`, `manual`), `boundary_continuity` |
| `label` | string | no | Display name |

`tolerance.position_mm` is the tolerance of the verifier's tracking certificate for this seam. It defaults to 0.5 mm. See [The tracking certificate](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification#the-tracking-certificate-welds-only).

### The seam frame

At each `s` the seam has a right-handed frame:

```text
P(s)  point on the seam curve
t(s)  unit tangent, in the travel direction
r(s)  reference direction, orthogonalised against t
b(s)  = t × r
```

`reference.mode` says where `r` comes from:

| `mode` | Needs | Reference direction |
|---|---|---|
| `rail_curve` | `rail` (a curve) | From the seam point toward the matching point on the rail. Points match by normalised arc length, per segment when the two curves have the same number of segments. |
| `fixed_vector` | `fixed_direction_xyz` | A fixed direction, orthogonalised against the tangent |
| `rotation_minimizing` | `seed_direction_xyz` | A seed direction carried along the curve without twisting |

`reference.semantics` records how the reference was made: `bisector`, `member_face` (with `member_ref`), `gravity_projected` or `custom`.

### Work and travel angles

```text
r' = R(t,  work_angle)  · r       roll in the plane across the seam
b' = t × r'
u  = R(b', −travel_angle) · r'    tilt along travel
```

`u` points from the weld point toward the torch body. The electrode axis is `−u`. `R` is a right-handed rotation.

- **Work angle** > 0 rotates `r` toward `b`. 0° puts the torch on the reference direction.
- **Travel angle** > 0 is drag (backhand): the torch leans back over the finished weld.
- **Spin**, the rotation about the electrode axis, is `free` by default. `preferred` and `locked` take an `angle_deg` function and a `reference` direction.

### Orientation bands

`orientation.limits.work_angle_deg` and `orientation.limits.travel_angle_deg` bound how far a planner may move each angle. A profile outside its band is a different weld, so a consumer must refuse it rather than clamp it.

| Field | Type | Description |
|---|---|---|
| `min`, `max` | scalar function, deg | Where the angle may be at all, as a function of `s` |
| `max_deviation_deg` | number ≥ 0, deg | How far the angle may move from the authored profile, either way |
| `max_deviation_plus_deg`, `max_deviation_minus_deg` | number ≥ 0, deg | The same, split by direction |
| `max_rate_deg_per_mm` | number ≥ 0, deg/mm | The largest rate of change per millimetre of seam arc |

The shipped weld preset `gmaw_steel_fillet` ("GMAW · mild steel · 6 mm fillet") authors 0° work and 12° travel, with bands of −15° to 15° work and 0° to 20° travel, each at most 0.5°/mm.

## Weld joints

A weld joint is an objective fact from the CAD: where two parts meet. Seams refer to joints by `weld_joint_ref`. In program.v2 the list is `workpiece.weld_joints`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | identifier | yes | Unique |
| `type` | string | yes | `butt`, `tee`, `lap`, `corner`, `edge`, `cruciform`, `plug` or `unknown` |
| `members[]` | array | yes | At least two: `solid_id`, optional contact `face_ids` and `role` (`base`, `branch`, `unspecified`) |
| `weld_line` | curve | yes | The line where the parts meet |
| `weld_symbol` | string | no | ISO 2553, for example `fillet` or `v_groove` |
| `contact` | object | no | `kind` (`coincident`, `gap`, `overlap`, `interference`), `gap_mm`, `overlap_area_mm2`, `max_face_deviation_mm` |
| `groove_faces[]` | array | no | Per weld-line segment, the two exposed faces: `solid_id`, `face_id`, `surface_type`, `outward_normal_xyz` |
| `dihedral_angle_deg` | scalar function, deg | no | The angle between the groove faces, through the open side |
| `bisector_rail` | object | no | A rail along the dihedral bisector: `curve`, `offset_m`, `continuous`, `approximate`, `max_deviation_mm`, `within_tolerance` |
| `accessibility` | object | no | Which stretches no torch direction can reach, found by casting rays: `blocked_spans`, `reachable_runs`, `open_fraction`, `suggested_start_s` and how they were measured |
| `accessible_sides`, `confidence`, `evidence`, `label` | | no | Advisory data from detection |

## The `.weldplan` container

A `.weldplan` is a zip file that carries one plan request. It is built by one implementation, `weldplan/plan_request.py`, through the seam worker's `build_plan_request` operation, so the bytes are reproducible: the same inputs give the same bytes and the same digests.

| Member | Required | Contents |
|---|---|---|
| `mimetype` | yes | First, stored uncompressed: a fixed media type string, so the format can be identified without parsing JSON |
| `manifest.json` | yes | What is inside, with digests; see below |
| `program.json` | yes | The `robot.v4.program.v2` document, including its `placement` |
| `cell.json` | yes | The cell descriptor: joints, axes, limits, frames. Mesh references are removed and `meshes_omitted` is `true`; the planner uses its own copy of the robot's meshes. |
| `tooling.json` | when the program names a `tool` | The fitted torch |
| `source/<name>.step` | when the program has welds | The workpiece geometry, as bytes |
| `fixtures.json` | no | Workcell bodies in the cell's world frame. Absent means the cell's own placeholder stands; an empty list means there are none. |
| `context/…` | no | The planning context OLP compiled: the corrected URDF, the merged planning numbers, the sphere model and the meshes. The planner reads the robot from here rather than from its own disk. |

JSON members are written with sorted keys and 2-space indentation, and every zip entry carries the fixed timestamp 1980-01-01 00:00:00.

### Manifest

| Field | Description |
|---|---|
| `schema` | `amr-weld-planner-v1.plan-request.v1` |
| `request_id`, `label`, `created_utc` | Request identity |
| `app` | `{module: "amr-weld-planner/v1", ui_version}` |
| `program` | `{path, program_id, schema, node_count, weld_count}` |
| `cell` | `{path: "cell.json", id}` |
| `planning_context` | `{root: "context/", files: [...]}`, when a context is packed |
| `source` | `{kind: "none"}`, or `{kind: "step", path, filename, sha256, bytes}` |
| `fixtures` | `{path: "fixtures.json", count}` or `null` |
| `planner_owns[]` | Sentences naming what the request leaves to the planner: positioner joint values, the cell's collision meshes, and the torch orientation within each seam's bands |
| `contents[]` | `{path, sha256, bytes}` for every member except `mimetype` and `manifest.json` |

### Checks when packing

Packing refuses the request, with a sentence per problem, when:

- the program fails its own program.v2 validation
- the program has welds and `workpiece.frame_id` is not `workpiece`
- a weld is still a hand-placed `manual_pose_wp`. OLP lowers those to seams before packing.
- there is no `placement`, or its `anchor_xyz_m`, `offset_xyz_m` or `rpy_rad` is not three finite numbers, or it names no `cell_id`
- the placement's `cell_id`, or the program's `robot.model_id`, differs from the packed cell's id
- the cell has no `work` frame
- the program has welds and no STEP was given
- the program names a `tool` and no tooling was given, or pins one whose digest differs

Packing also stamps two pins into `program.json`. A tool reference gets the SHA-256 of the packed `tooling.json`. A robot pin of all zeros takes the packed robot description's identity. A robot pin that names a **different** description is refused: the description changed since the program was written, so review and accept the new robot settings in OLP, then plan again.

### Checks when opening

The planner refuses a `.weldplan` unless:

- it is a zip file whose `mimetype` entry is the expected one
- `manifest.json` exists with the expected `schema`
- every member in `contents` exists and matches its SHA-256
- `program.json` is `robot.v4.program.v2`
- the program's `robot.robot_description_sha256` equals the packed cell's
- a program `tool` has a `tooling.json` whose SHA-256 matches its pin
- every declared planning-context file exists

The planner's answer names the request by the SHA-256 of the whole file. That digest becomes the plan's `program_digest` and the root of its `plan_id`.

## Seam worker protocol

The seam worker is `python -m seam_worker.workers --stdin`, run in `weld_planner/v1`. It reads **one** JSON object from stdin and writes one JSON object to stdout, then exits. OLP starts one per request and forwards the operations through [`POST /api/offline-programming/v1/seam`](/docs/apis/olp-http#authoring-and-planning).

```bash
cd weld_planner/v1
echo '{"operation": "sample_seam_frames", "curve": {"segments": [{"degree": 1,
  "control_points_xyz_m": [[0,0,0],[0.1,0,0]], "knots_u": [0,0,1,1]}]},
  "reference": {"mode": "fixed_vector", "fixed_direction_xyz": [0,0,1]}, "samples": 3}' \
  | pixi run -e default python -m seam_worker.workers --stdin
```

| `operation` | Request fields | Response |
|---|---|---|
| `detect_joints` | `step_base64`; optional `topology`, `rail_offset_m`, `intersector`, `seam_options` (`min_length_m`, `max_corner_angle_deg`) | `{topology, joints}`: the B-rep topology, and the weld joints annotated with accessibility, plus one default seam per reachable run |
| `check_torch_fits` | `step_base64`, `joint`, `seam`, `torch`; optional `options` | `{clearance}`: how much of the seam the torch barrel can reach |
| `plan_torch_path` | `step_base64`, `joint`, `seam`, `torch`, `default_standoff_mm`, `search` (`work_deviation_deg`, `travel_deviation_deg`; optional bin sizes, `knot_spacing_mm`, `sample_step_mm`, `ray_count`, `standoff_cost_deg_per_mm`); optional `direction` (`forward` or `reverse`), `intersector` | `{direction, plan, rays_cast, samples, warnings}`: the work and travel profile that welds the most arc, closest to what was authored |
| `sample_seam_frames` | `curve`, `reference`; optional `samples`, `max_chord_m`, `max_angle_rad` | `{frames}`: the `{P, t, r, b}` frame at each sample |
| `build_plan_request` | `program`, `planning_context` (`{files: {path: base64}}`), and `step_base64` or `workpiece_absent: true`; optional `step_filename`, `tooling`, `fixtures`, `request_id`, `label`, `created_utc` | `{weldplan_base64, bytes, sha256}` |

`plan_torch_path` searches one direction per call. A `reverse` search answers with the reversed weld in the welder's own terms, and sets `direction_changed` in the plan.

A failure is written as `{"error": {"code": "…", "detail": "…"}}`. The worker's own codes are `invalid_request` (exit status 2) and `worker_failed` (exit status 1). OLP adds `payload_too_large`, `busy`, `timeout`, `canceled`, `unavailable` and `invalid_response`; see the [OLP seam route](https://advancedmetalresearch.com/docs/apis/olp-http#authoring-and-planning).

## Related pages

- [Program format (`robot.v4.program.v2`)](/docs/reference/program-format)
- [Weld planning and verification](https://advancedmetalresearch.com/docs/concepts/weld-planning-and-verification)
- [Program a weld from CAD](https://advancedmetalresearch.com/docs/guides/offline-programming)
- [Weld planner HTTP API](https://advancedmetalresearch.com/docs/apis/weld-planner-http)

## Sources

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

- `weld_planner/v1/schema/amr-weld-planner-v1.weld-program.v1.schema.json ($defs identifier, curve_segment, curve, scalar_function, seam, orientation_limit_axis, weld_joint, groove_face)`
- `weld_planner/v1/tests/shared/test_program_v2.py:31-46,126-140`
- `weld_planner/v1/python/weldplan/plan_request.py:88-525`
- `weld_planner/v1/python/seam_worker/seam/frames.py:55-84`
- `weld_planner/v1/python/seam_worker/workers.py:249-701`
- `weld_planner/v1/python/weld_motion_planner/planner/trajectory_optimization/engine/weld_trajopt.py:494-498`
- `weld_planner/v1/data/presets/weld/gmaw-steel-fillet.json`
- `steamdeck/real/v4/contracts/programs/robot.v4.program.v2.schema.json ($defs seam)`
- `offline-programming/v1/internal/seam/types.go:33-56`
- `offline-programming/v1/internal/seam/worker.go:483-495`
