Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft

The platform for test development and orchestration
that lets you and your AI Agents run the entire testing operation

Watch the demo

Orcraft Endpoints

An endpoint is a machine that executes steps on the controller's behalf. Registering them is a setup task - see Adding endpoints - and this page is about the other half: how a test says which endpoints it needs, what gets sent to them, and how a step ends up running there.

Endpoints are configuration, not code. A suite developed entirely against localhost becomes a distributed suite by changing values in the run panel, with no edit to the test body.

Endpoint fields

Every test config already declares test_endpoint, defaulting to localhost. A test that needs more than one, or a different kind of selection, declares its own:

class MyTestConfig(OrcraftTestConfig):
	test_endpoint = OrcraftConfigField.Endpoint(default="localhost", label="Test Endpoint")
	aux_endpoints = OrcraftConfigField.MultiSelectEndpoints(default=("localhost",), label="Aux Endpoints")
Type Value Meaning
Endpoint str One endpoint. Declare several fields when a test needs distinct roles - a producer and a consumer, a host and a device.
MultiSelectEndpoints tuple[str, ...] A set of endpoints used together within one run, iterated by the test itself.
EndpointFanout tuple[str, ...] A set of endpoints that each get their own run. Selecting three endpoints spawns three test runs.

The difference between the last two is what happens when several endpoints are selected. MultiSelectEndpoints gives one run with several endpoints available to it - the test loops over them and decides what each one does. EndpointFanout gives several independent runs of the same test, one per endpoint, executing in parallel. Use fanout to run the same verification everywhere; use multi-select when the endpoints play different parts in one scenario.

Redeclaring test_endpoint is how a test changes its own dispatch model, because the base config already declares it as a plain Endpoint:

test_endpoint = OrcraftConfigField.EndpointFanout(
	default=("localhost",),
	label="Test Endpoint",
	shared=True,
)

Field options

Option Default Meaning
port 50051 Port used when the configured value carries no port of its own.
secure_enabled None Forces mutual TLS on or off for this field. None follows the endpoint's own registration.
shared False Lets concurrent runs target the same endpoint at once instead of queueing for it.

By default an endpoint is held for the duration of a run, and another run wanting the same endpoint waits its turn - which is what you want when a test owns a device. shared=True lifts that, treating each run as an independent client, which is what you want when several fanout-expanded runs legitimately share one host.

Endpoint pools

A pool asks for any available endpoint instead of naming one. It is declared as the field's default, in place of a machine name, and the machines are picked when the run starts. The test declares its roles and how many endpoints each takes; which endpoints play them is decided by what the fleet has free at that moment:

class MyTestConfig(OrcraftTestConfig):
	test_endpoint = OrcraftConfigField.Endpoint(
		default=OrcraftConfigField.EndpointPool(
			OrcraftConfigField.Query.match("os", "Ubuntu 24.04"),
		),
		label="Test Endpoint",
	)
	device_endpoints = OrcraftConfigField.MultiSelectEndpoints(
		default=OrcraftConfigField.EndpointPool(
			OrcraftConfigField.Query.match("usb", "ftdi"),
			OrcraftConfigField.Query.distinct("os"),
			count=2,
		),
		label="Device Endpoints",
	)

Conditions

EndpointPool takes its conditions as positional arguments, the way Matrix takes its dimensions, and count as a keyword. The conditions combine as AND. A condition is a Query record of one of two kinds:

Condition Meaning
Query.match(attribute, value) Keeps the endpoints whose attribute has the value. A tuple of values means any of them: Query.match("os", ("Ubuntu 24.04", "macOS 26.2")).
Query.distinct(attribute) The picked endpoints differ on the attribute: two endpoints with distinct os are two different operating systems.

Attributes

The attributes are what the controller records about each machine. os is the operating system exactly as the endpoint reports it, such as Windows 11, Ubuntu 24.04 or macOS 26.2, and a condition names it in full: the one who declares the pool knows the fleet, and a test that cares about the operating system says which one. The other attributes are the labels a person gives an endpoint in the controller, such as gpu: discrete or usb: ftdi, and the machine facts chosen there, such as the architecture. Names and values compare regardless of case.

Count

What count means follows the field type:

Type Pool
Endpoint One endpoint. The count is one and a distinct condition has nothing to spread over, so neither is accepted.
MultiSelectEndpoints count endpoints in one run, picked together.
EndpointFanout count runs, one endpoint each, picked when the fan-out expands.

Picking

The pick follows rules a test cannot change. localhost is never picked. A test's own endpoint, the one its test_endpoint holds, is never picked again for its other fields, and no test takes an endpoint twice. Among the qualifying endpoints the first free ones win, in the order the endpoints are listed; a qualifying endpoint that another run holds is waited for, as a named one would be, and the run shows as queued until the pick is made.

Refusals

A pool the fleet cannot fill is refused before anything runs, with a message that names the test and the field: the test's config names no endpoints file, no endpoint carries the attribute a condition names, fewer endpoints qualify than the count asks for, fewer distinct values exist than the count, or the test's pools together ask for more endpoints than the fleet can give at once.

In the run

The test body never sees a pool. By the time it runs, the field holds endpoint values in the field's own shape - one for Endpoint, a tuple for the other two - and the log's configuration table and the run history record the endpoints that were picked. In the controller the run panel shows the pool and may replace it for a run: other conditions, fewer candidates, another count, or endpoints picked by hand, as the tutorial's Endpoint Pools page shows.

Endpoints file

Outside the controller, the fleet a pool picks from is the controller's saved endpoints file, named by the test's own config in endpoints_file_path. A relative path is relative to the file of the config class that sets it:

class MyTestConfig(OrcraftTestConfig):
	test_endpoint = OrcraftConfigField.Endpoint(
		default=OrcraftConfigField.EndpointPool(
			OrcraftConfigField.Query.match("os", "Ubuntu 24.04"),
		),
		label="Test Endpoint",
	)
	endpoints_file_path = OrcraftConfigField.Text(default="../endpoints.json")

The test then picks the same way however it runs: on its own from the command line, in a group, a suite or a batch. Each test names its own file. In the controller the field is never shown: the controller picks from its own endpoints. A CI snapshot that holds a pool is exported with the session's file beside its batch, and each test that asks for a pool names that file, as the tutorial's Exporting Test Batch for CI page shows. A test without pools needs no file.

Step dispatch

Steps are the unit of remote execution. A flow chooses where each one runs:

produce_result = self.run_step(
	MyProduceStep,
	endpoint=self.config.endpoint_1,
	payload_size=self.config.payload_size,
)

consume_result = self.run_step(
	MyConsumeStep,
	endpoint=self.config.endpoint_2,
	input_file=transferred_path,
)

With no endpoint argument, a step runs on the endpoint the test is configured for. The framework establishes the connection, sends the step and its config, executes it there, and returns the result - so from the flow's point of view a remote step and a local one are the same call.

Because the step may run on another machine, everything it needs must arrive through its config, and everything it produces must leave through its result. A step cannot reach back into the test.

Package upload

Before a step runs remotely, the endpoint needs the code. The controller packages the suite and uploads it, governed by upload_test_package:

Value Meaning
always Upload on every run. The safe choice while the suite is changing.
if_missing Upload only when the endpoint does not already have the package. The default.
never Never upload - the endpoint is expected to have the package already.

Suite requirements

A suite that imports packages beyond the Python standard library lists them in a requirements.txt at the root of its repository, in pip's own format. After the suite package is activated on an endpoint, the controller has the endpoint application's Python install that file with pip, once per uploaded package; a package the endpoint already holds costs one short check. The outcome appears in the log next to the upload lines and names the requirements. When the install fails, the test errors before any step runs and the log carries pip's message.

The FTDI example suite drives FT2232H modules through pyftdi, which needs a libusb library that Windows does not ship, so its file lists both:

pyftdi
libusb-package

The controller's own Python is never touched. A test that runs on localhost uses the controller's Python directly, so those packages must be installed there by hand. A suite without a requirements file skips the step entirely.

Data files are a separate matter from code. A file a test needs on an endpoint is declared as a package field, and the endpoints option binds it to the endpoint fields that actually need it:

input_file = OrcraftConfigField.PackageFile(
	default="",
	label="Input File",
	required=True,
	endpoints=[endpoint_1],
)

input_folders = OrcraftConfigField.PackageFolderList(
	default=(),
	label="Input Folders",
	required=True,
	endpoints=[test_endpoint, aux_endpoints],
)

Inside the step, the value resolves to the path on the machine the code is executing on. Without endpoints, a package field is uploaded to every endpoint the test uses - fine for a small binary, wasteful for a large data set that only one endpoint reads. The field types are listed in Configuration fields.

Endpoint application update

update_endpoint_app decides whether the endpoint application itself is brought up to date before the run:

Value Meaning
always Update the endpoint application before every run.
on_mismatch Update only when the endpoint application does not match the controller. The default.
never Leave the endpoint application as it is.

File transfer

Package upload sends files from the controller. Moving files between endpoints during a run - produce on one machine, verify on another - is what a flow's transfer is for:

transfer_result = self.transfer(
	produce_result.output_file,
	src_endpoint=self.config.endpoint_1,
	dst_endpoint=self.config.endpoint_2,
)

if transfer_result:
	return

consume_result = self.run_step(
	MyConsumeStep,
	endpoint=self.config.endpoint_2,
	input_file=transfer_result.dst_paths[0],
)

paths takes a single file or folder path, or a list of file paths, and they travel as one zip archive rather than as a call per file. The returned object is truthy when the transfer failed, matching the handle_errors convention, and dst_paths holds one absolute path per element of paths, pointing at where each landed on the destination.

Endpoint state

Tests that deliberately disrupt an endpoint - a reboot, a service restart, a network drop - need to wait for it to come back. OrcraftEndpointState names the three states:

State Meaning
OFFLINE The endpoint is not reachable.
ONLINE The endpoint answers, but is not yet able to accept work.
READY The endpoint is reachable and ready to execute steps.
from orcraft import OrcraftEndpointState

...

self.run_step(
	MyRebootStep,
	endpoint=endpoint,
	expect_endpoint_disconnect=True,
)

if self.wait_for_endpoint_state(
	OrcraftEndpointState.OFFLINE,
	endpoint=endpoint,
	timeout_sec=self.config.disconnect_timeout_sec,
):
	return

if self.wait_for_endpoint_state(
	OrcraftEndpointState.READY,
	endpoint=endpoint,
	timeout_sec=self.config.connect_timeout_sec,
	poll_interval_sec=5,
):
	return

wait_for_endpoint_state returns truthy on failure, like the other flow helpers, so if ...: return reads as the error path throughout. restart_endpoint wraps the whole sequence - restart, wait for the disconnect, wait for readiness - for the common case.

The step that causes the disconnection must be marked expect_endpoint_disconnect=True. Without it, the connection dropping mid-step is exactly what an infrastructure failure looks like, and the framework reports it as an error.

Security

Communication with an endpoint is secured with mutual TLS and token-based authentication, negotiated from how the endpoint was registered with the controller. Tests do not handle credentials: the security context follows the endpoint field, and flows and steps - including those called from setup and teardown methods - inherit it automatically.

secure_enabled on an endpoint field overrides the negotiated setting for that field, which is mainly useful for tests that exist to verify the secured and unsecured paths themselves.

Type to search the docs.

    ↑↓ navigate ↵ open Esc close