---
title: "PreForm client"
description: "The PreForm client is the SDK's handle to the PreForm API of one PreForm or PreForm Server process."
url: https://developer.formlabs.com/core-concepts/preform-client/
---

# PreForm client

The *PreForm client*, `client.preform`, is the SDK's handle to the PreForm API client that talks to your instance of PreForm.

Most methods on the client do a step you already do in the PreForm window, such as the **Job Setup**
choices, importing a file, or **Upload to Queue**. A script that calls them in order
runs the same steps for every incoming order, without an operator repeating the
clicks.

## Connect or launch

The `connect()` method attaches to a PreForm that's already running, and the `launch()`
method starts one:

|  | `connect()` | `launch()` |
| --- | --- | --- |
| **Process** | Attaches to PreForm or PreForm Server on the port | Starts PreForm or PreForm Server from the executable you pass |
| **Login** | Leaves the login alone unless you pass `login=True` | Logs PreForm in with the client's account, when the client has one |
| **On close** | Leaves PreForm running | Closes PreForm |
| **Failure codes** | `SERVER_UNREACHABLE` | `STARTUP_FAILED`, `PORT_IN_USE` |

Both methods default to port 44388 and take `port=` for another one.

## Connect to a running PreForm

The `connect()` method attaches to a PreForm that's already open, or to a PreForm Server
another process started:

```python
# connect() attaches to PreForm on port 44388 and leaves its login alone.
preform = client.preform.connect()
print(preform.version, preform.owns_process)
```

`owns_process` is `False` for a connected process, so closing the client leaves it
running. To reach PreForm Server on another machine, pass `base_url=`, for example
`http://printhost:44388`. A server in a container is one that `connect()` attaches to;
see [PreForm Server in Docker](https://developer.formlabs.com/guides/preform-server-docker.md).

The PreForm you connect to may already be logged in: `current_user()` returns the
account, or `None`. For more information, see [Log PreForm in](#log-preform-in).

## Launch PreForm

A `with` block scopes the PreForm process to the code that uses it:

```python
# launch() waits up to 60 seconds for PreForm to print READY FOR INPUT.
with client.preform.launch(PREFORM) as preform:
    print(preform.version, preform.base_url, preform.pid)
```

The handle reports the `version` and `base_url` of PreForm, and `pid` holds the process
ID of a PreForm that `launch()` started. When another PreForm or PreForm Server
already listens on the port, `launch()` fails with `PORT_IN_USE`.

## Log PreForm in

PreForm works without a login: it prepares and saves jobs, and prints to printers on
your local network. Logged in to your Formlabs account, it also reaches the printers
registered to the account and its Printer Groups, as **Log In** in PreForm's
**Account** menu does:

![The PreForm Account menu while logged out, with the line Log in for remote printers and groups and the Log In button.](https://developer.formlabs.com/assets/images/screenshots/authentication-account-menu.png)

*The Account menu in PreForm.*

Pass the account to the client, and `connect(login=True)` logs PreForm in:

```python
import os

from formlabs_sdk import FormlabsClient

# The client holds the account, and connect(login=True) logs PreForm in to it.
with FormlabsClient(
    username=os.environ["FORMLABS_USERNAME"],
    password=os.environ["FORMLABS_PASSWORD"],
) as client:
    # A rejected login raises PreFormApiError with the code LOGIN_FAILED, and
    # PreForm keeps running, logged out.
    with client.preform.connect(login=True) as preform:
        user = preform.current_user()
        print(f"Logged in as {user.username}")
```

The login lasts until PreForm exits or `logout()` ends it, and a new login replaces the
one before. Without `login=True`, `connect()` leaves the login as it is. To log in
later, call `preform.login()`. A rejected login raises `PreFormApiError` with the code
`LOGIN_FAILED`, and PreForm keeps running, logged out.

**Caution:** Before you replace the login of a PreForm someone else uses, check
`current_user()`.

## Call the PreForm API

Each endpoint has a method named after its operation. The following table maps
common PreForm actions to the methods that do the same work:

| In PreForm | Method |
| --- | --- |
| Choose printer, material, and layer thickness on the **Job Setup** screen | `create_scene`, `update_scene` |
| Open a `.form` file | `open_form_file` |
| Import an STL or CAD file | `import_models` |
| Orient, support, and lay out models automatically | `auto_orient_models`, `auto_support_models`, `auto_layout_models` |
| Save the build as a `.form` file | `save_form_file` |
| Send the job to a printer with **Print Now** or **Upload to Queue** | `print_scene` |

A long-running endpoint starts an operation, and the method returns when the
operation finishes. For more information, see [Operation](https://developer.formlabs.com/core-concepts/operation.md).

## Handle errors

A failed call raises `PreFormApiError`, which carries the error `code`, its
`message`, and the HTTP `status_code`:

```python
try:
    preform.create_scene(
        machine_type="FORM-4-0",
        material_code="NOT-A-MATERIAL",
        layer_thickness_mm=0.1,
    )
except PreFormApiError as exc:
    # code is the field to match on.
    print(exc.code, exc.status_code, exc.message)
```

A scene setting that Formlabs doesn't ship fails with `INPUT_ERROR` and status `400`.
For more information about error codes, see [Errors](https://developer.formlabs.com/api-reference/schemas/errorcode.md).

## Blocking and async clients

`FormlabsClient` gives a blocking handle, and `AsyncFormlabsClient` gives an async one
with the same methods. In async code, await `connect()` and each method:

```python
async def main() -> None:
    # A logged-out PreForm can still prepare a scene and save it, so the account
    # is optional here.
    async with AsyncFormlabsClient(
        username=os.environ.get("FORMLABS_USERNAME"),
        password=os.environ.get("FORMLABS_PASSWORD"),
    ) as client:
        # Awaiting connect() returns once PreForm answers on port 44388. Leaving this
        # with block leaves PreForm running.
        async with await client.preform.connect() as preform:
```

The blocking client runs the async one on a private event-loop thread. The samples on
the Core concepts pages use the blocking client.

## What's next

- [Scene](https://developer.formlabs.com/core-concepts/scene.md)
- [API reference](https://developer.formlabs.com/api-reference.md)
