---
title: "Print setting rollout"
description: "Move a queue of jobs from one print setting to another, with a preview of the layer counts and print times before anything is saved."
url: https://developer.formlabs.com/examples/print-setting-rollout/
---

# Print setting rollout

When a new version of a print setting is approved, every job already prepared on the old version
has to move to the new one. In PreForm, you would open each job, change the print setting in **Job
Setup**, and save it again. This script does that for a whole folder of `.form` files. It changes
only the jobs on the setting you name, saves the changed jobs to a new folder, and reports the layer
count and print time of each job before and after the change.

[View on GitHub](https://github.com/Formlabs/formlabs-api-python/tree/v1.0.0-alpha.post1/examples/print-setting-rollout) [Download print-setting-rollout.zip](https://github.com/Formlabs/formlabs-api-python/releases/download/v1.0.0-alpha.post1/print-setting-rollout.zip)

A dry run on the sample jobs prints:

```
rolling out: Lab arches v4 (FORM-4-0, FLFMGR01, F4_FAST_ARCHES at 0.2 mm)
dry run, nothing saved

file             current setting      layers        print time  status
case-1041.form   Lab arches v3    150 -> 120  0h 15m -> 0h 12m  changed
case-1042.form   Lab arches v3    156 -> 125  0h 16m -> 0h 13m  changed
case-1043.form   Fast Arch               156                    skipped, other setting

2 build volume(s) changed in 3 file(s)
```

The example contains the script, `roll_out_print_setting.py`, and its sample input:

- `queue/` holds three supported jobs. Two are on the custom setting `Lab arches v3`, and one is on
  the shipped `Fast Arch` setting.
- `lab_arches_v3.fps` and `lab_arches_v4.fps` are two versions of a custom setting. Both are copies
  of the Fast Arch setting for Fast Model V1 on Form 4 with a changed squish wait, and v4 also prints
  at 0.2 mm layers.

## Choose the new setting

You can give the new setting as an `.fps` file, or as the four values of a shipped setting with
`--shipped`:

```python
paths = args.paths
if args.shipped:
    machine, material, setting, thickness = args.shipped
    target = dict(
        machine_type=machine,
        material_code=material,
        print_setting=setting,
        layer_thickness_mm=float(thickness),
    )
elif paths[0].suffix.lower() == ".fps":
    target = {"fps_file": str(paths[0].resolve())}
    paths = paths[1:]
else:
    sys.exit("Give an .fps file or --shipped MACHINE MATERIAL SETTING THICKNESS_MM")
```

Both end up as keyword arguments for the scene calls, so the rest of the script treats them the
same way.

## Connect to PreForm

Make sure [PreForm is ready to accept connections](https://developer.formlabs.com/getting-started/install.md#connect-to-a-running-preform).

The script reads the PreForm address from `FL_LOCAL_BASE_URL`, and uses `http://localhost:44388`
if it isn't set:

```python
PREFORM_URL = os.environ.get("FL_LOCAL_BASE_URL", "http://localhost:44388")
```

If PreForm doesn't answer, the script stops with a hint:

```python
with FormlabsClient() as client:
    try:
        preform = client.preform.connect(base_url=PREFORM_URL)
    except PreFormApiError as exc:
        sys.exit(
            f"{exc}\nNo PreForm at {PREFORM_URL}; "
            "open PreForm and start its API server"
        )
```

## Check the new setting

Before it opens any job, `check_target` makes sure the new setting works. It checks a shipped
setting against PreForm's list, then creates an empty scene on the new setting and reads its name:

```python
def check_target(preform: PreFormApi, target: dict) -> str:
    """Create an empty scene on the target setting, so a bad setting stops the run."""
    if "fps_file" not in target and not preform.validate_print_setting(**target).valid:
        sys.exit(f"No shipped print setting matches {target}")
    scene = preform.create_scene(**target)
    try:
        return describe(preform, scene.scene_id)
    finally:
        preform.delete_scene(scene_id=scene.scene_id)
```

If the `.fps` file is missing or can't be read, the script stops here with `INPUT_ERROR`. The name
on the first line of the report comes from the `.fps` file.

## Skip jobs on other settings

`roll_out` opens each `.form` file. Each build volume in the file opens as its own scene, and the
script reads the name of its print setting from the job metadata. With `--only-from`, it skips the
build volumes on any other setting:

```python
def roll_out(
    preform: PreFormApi,
    form: Path,
    target: dict,
    only_from: str | None,
    out: Path | None,
    with_time: bool,
) -> list[Row]:
    """Change each build volume in a .form file to the target, then save the file."""
    opened = preform.open_form_file(file=str(form.resolve()))
    rows: list[Row] = []
    try:
        for scene in opened.scenes:
            label = form.name
            if len(opened.scenes) > 1:
                label = f"{form.name} / {scene.scene_name}"
            before = setting_name(preform, scene.scene_id)
            layers_before = preform.get_scene(scene_id=scene.scene_id).layer_count
            if only_from and before != only_from:
                rows.append(Row(label, before, str(layers_before), status=SKIPPED))
                continue
```

Each build volume is one row of the report. A skipped one shows the status `skipped, other setting`:

```python
PREFORM_URL = os.environ.get("FL_LOCAL_BASE_URL", "http://localhost:44388")
SKIPPED = "skipped, other setting"


@dataclass
class Row:
    label: str
    setting: str
    layers: str
    print_time: str = ""
    status: str = ""
```

The script matches the setting by the name PreForm shows. For a custom setting, that's the name
stored in its `.fps` file, so give each version of a custom setting its own name.

## Change the setting

`update_scene` takes the same arguments as `create_scene`. After the update, the script reads the
scene again for its layer count and models:

```python
time_before = print_time(preform, scene.scene_id) if with_time else ""
# Models, supports, and layout stay. On another printer's setting, the
# models move into its build volume, and any that don't fit are out of
# bounds.
preform.update_scene(scene_id=scene.scene_id, **target)
after = preform.get_scene(scene_id=scene.scene_id)
outside = [model.name for model in after.models if not model.in_bounds]
rows.append(
    Row(
        label,
        before,
        f"{layers_before} -> {after.layer_count}",
        f"{time_before} -> {print_time(preform, scene.scene_id)}" if with_time else "",
        f"{len(outside)} model(s) outside the build volume" if outside else "changed",
    )
)
```

The layer count changes when the new setting has another layer thickness, as `lab_arches_v4.fps`
does. Models, supports, and layout stay as they are. If a model ends up outside the build volume,
the report says so, and the job is still saved.

## Compare print times

With `--print-time`, the script calculates the print time before and after the change:

```python
def print_time(preform: PreFormApi, scene_id: UUID) -> str:
    seconds = preform.calculate_print_time(scene_id=scene_id).total_print_time_s
    return f"{seconds // 3600}h {seconds % 3600 // 60:02d}m"
```

The calculation slices each build, which adds time to the run. Leave out `--print-time` to skip it.

## Save the job

The script saves the changed job to the `--out` folder, under its original file name, and the
report shows where:

```python
if out and any(row.status != SKIPPED for row in rows):
    out.mkdir(parents=True, exist_ok=True)
    saved = out / form.name
    preform.save_form_file(
        file=str(saved.resolve()),
        scene_ids=[scene.scene_id for scene in opened.scenes],
        project_name=opened.project_name or form.stem,
    )
    for row in rows:
        if row.status != SKIPPED:
            row.status = f"saved to {saved}" if row.status == "changed" else f"{row.status}, saved to {saved}"
```

The original files are never changed. With `--dry-run`, nothing is saved.

## Delete the scenes

The `finally` block deletes every scene the file opened, including when a call fails:

```python
finally:
    for scene in opened.scenes:
        preform.delete_scene(scene_id=scene.scene_id)
```

## Run the example

With pip, [install the Formlabs SDK](https://developer.formlabs.com/getting-started/install.md) first; with [uv](https://docs.astral.sh/uv/getting-started/installation/), `uv run` installs it from the example's `pyproject.toml`.

1. In a terminal, preview the change from the folder you unzipped:

   **pip:**

   ```bash
   python roll_out_print_setting.py lab_arches_v4.fps queue \
       --only-from "Lab arches v3" --dry-run --print-time
   ```

   **uv:**

   ```bash
   uv run roll_out_print_setting.py lab_arches_v4.fps queue \
       --only-from "Lab arches v3" --dry-run --print-time
   ```
2. Roll out the `.fps` file:

   **pip:**

   ```bash
   python roll_out_print_setting.py lab_arches_v4.fps queue \
       --only-from "Lab arches v3" --out queue-v4
   ```

   **uv:**

   ```bash
   uv run roll_out_print_setting.py lab_arches_v4.fps queue \
       --only-from "Lab arches v3" --out queue-v4
   ```
3. Roll out a shipped setting:

   **pip:**

   ```bash
   python roll_out_print_setting.py --shipped FORM-4-0 FLFMGR01 DEFAULT 0.2 queue \
       --only-from "Lab arches v3" --out queue-v4
   ```

   **uv:**

   ```bash
   uv run roll_out_print_setting.py --shipped FORM-4-0 FLFMGR01 DEFAULT 0.2 queue \
       --only-from "Lab arches v3" --out queue-v4
   ```

The script reads the `.fps` file and every `.form` file in `queue/`, and writes the changed jobs to
`queue-v4/`. To roll out another setting, pass its `.fps` file or its four `--shipped` values, and
pass the name of the setting it replaces to `--only-from`.

The preview prints the output at the top of this page. The `.fps` rollout saves both jobs:

```
rolling out: Lab arches v4 (FORM-4-0, FLFMGR01, F4_FAST_ARCHES at 0.2 mm)

file             current setting      layers  status
case-1041.form   Lab arches v3    150 -> 120  saved to queue-v4/case-1041.form
case-1042.form   Lab arches v3    156 -> 125  saved to queue-v4/case-1042.form
case-1043.form   Fast Arch               156  skipped, other setting

2 build volume(s) changed in 3 file(s)
```

The shipped setting rollout saves both jobs on the shipped setting:

```
rolling out: Default (FORM-4-0, FLFMGR01, DEFAULT at 0.2 mm)

file             current setting      layers  status
case-1041.form   Lab arches v3    150 -> 120  saved to queue-v4/case-1041.form
case-1042.form   Lab arches v3    156 -> 125  saved to queue-v4/case-1042.form
case-1043.form   Fast Arch               156  skipped, other setting

2 build volume(s) changed in 3 file(s)
```

## What's next

- [Print settings](https://developer.formlabs.com/core-concepts/print-settings.md)
- [Part labels](https://developer.formlabs.com/examples/part-labels.md)
