In a Python algorithm, the decorator and the Params dataclass in main.py are the interface. Everything else in the file is just the work. Keeping the two apart is what lets the platform show your ports in the editor, check workflow connections and propose version numbers, all without running your code.
When you save, the platform reads the interface statically and freezes it with the version as algorithm.yaml. At run time the SDK rebuilds the interface from the decorator and refuses to start if it differs from the frozen one (ALGORITHM_MANIFEST_DRIFT).
Decorator arguments
| Argument | Required | Rule |
|---|---|---|
name, title | yes | name is lowercase letters, digits and -, 2 to 63 long, starting with a letter. It is fixed for the algorithm's life. A team name may not start with ef- or embodiflow-. |
description | no | Shown in the library. |
level | no | check and enrich: recording (default) or episode. |
reads | check at recording level | raw or recording (default). |
inputs | no | Extra features inputs: Input(requires=[…], optional=False). Names are lowercase letters, digits and _, up to 32 long, and not a data type name, a default port name or a run argument name. |
features | enrich | At least one Feature(...). |
checks | check; any category that writes checks | Check(id, title, description=""). |
labels | segment | The label keys its episodes carry: {key: semantic type}. |
requires | no | Facts the data must have. See below. |
params | no | A dataclass whose every field is param(default, …). |
resources | no | See below. |
requirements | no | pip requirement strings. Binary wheels only. Locked at save. |
retries, cache | no | Default: one retry on transient errors, cache on. |
services | no | Built-in algorithms only: ["vlm"]. |
privacy | no | Built-in algorithms only: "redacts" or "numbers_only". |
You don't write a version in the code. When you save, the platform proposes the next version from what changed in the interface.
Data types
Ports carry one of eight data types, and a connection is only allowed between ports of the same type.
| Type | What it is |
|---|---|
raw | One recording's uploaded files. |
recording | The canonical recording: a LeRobot v3 tree plus meta/embodiflow/recording.json. |
features | New values about a recording or its episodes. |
checks | Judgements, each with a status. |
episodes | Time ranges with a task text. |
version | A dataset version. |
selection | Per-episode decisions: keep, split, task. |
artifact | Files to download. |
Feature
A feature is declared as Feature(name, type, per, dtype=…, shape=…, unit=…, frame=…, names=…, source=…, describe=…).
| Field | Meaning |
|---|---|
name | A short name. The platform stores it under a name that belongs to your team. |
type | A semantic type from the table below. |
per | frame, recording, episode or file. |
dtype | float32 (default), float64, int8, int16, int32, int64, uint8 or uint16. |
shape | The shape of one value. Array types have a fixed default shape. |
unit, frame | Required for geometric types, except per file. |
names | Names of the components, for example joint names. |
source | recorded, imported, estimated, computed (default) or retargeted. |
describe | Extra description, for example {"targetRig": "tron2av3"} for a retargeted action. |
Parameters
Parameters are the settings a workflow can change without touching your code. Every field of Params is param(default, minimum=…, maximum=…, enum=…, title=…, description=…, widget=…), and its type comes from the annotation: int, float, str, bool, or a list of strings, integers or floats.
A list field takes a literal list default and also items_enum, min_items and max_items. The widget tags takes free strings; multiselect needs items_enum. A workflow sets values in with, and the platform checks them against these limits.
Semantic types
A feature's semantic type tells the next step what the numbers mean; it's how the editor knows which outputs can feed an input that requires, say, keypoints3d.hand21. The registry is closed, and teams use these types as they are.
| Type | Meaning | Shape or value |
|---|---|---|
scalar | One number | [1] |
vector | Numbers | [n] |
label.bool | Yes or no label | bool |
label.category | One of a declared set | string; describe.values |
label.text | Free text | string |
spans | Time ranges inside an episode or recording | list of { startS, endS, label } |
embedding | An embedding vector | [n]; describe.model |
pose.se3 | Position and quaternion | [7]: x y z qx qy qz qw; unit, frame |
keypoints3d.hand21 | 21 hand landmarks in 3D, MediaPipe order | [63]; unit, frame |
skeleton.hand21 | 21 hand joints with a 6-DoF frame each, MediaPipe order | [147] = 21 x (x y z qx qy qz qw); unit, frame |
skeleton.body22 | 22 body joints with a 6-DoF frame each, SMPL order (pelvis first) | [154] = 22 x (x y z qx qy qz qw); unit, frame |
keypoints2d | 2D image keypoints | [2k]; frame is the camera |
confidence | Per-sample confidence from 0 to 1 | [n] |
calibration.stereo | Rectified stereo extrinsics | { R[9], T[3] } |
calibration.cam_imu | Camera to IMU transform and time offset | { R[9], T[3], dtS } |
depth.video | 16-bit depth video sidecar | file |
mask.video | Per-frame mask video | file |
video.preview | Light browser preview copy of one camera video, same frames | file, embodiflow-preview/v1 |
ros2.mcap | ROS 2 MCAP file (TF tree, images, point clouds, markers) | file |
action.joint | Joint-space action | [n]; names |
action.ee_pose | End-effector action | [7], or [8] with the gripper |
json | Any small JSON | object, at most 1 MiB; matched by requires only as name:<feature> |
Facts and requires
Facts are what the platform knows about the data, derived from the data itself and never typed in by a person. Your code reads them as ctx.facts, and requires uses them to decide whether your algorithm applies at all.
| Fact | Type | Known from |
|---|---|---|
kind | robot or human | The project |
format | string: mcap, lerobot, video, or x-<team format> | The upload |
privacy_required | bool | The recording profile |
duration_s | number | The upload summary, then the recording, then the episode window |
cameras | integer | The upload summary, then the recording |
stereo | bool | The recording: a stereo pair with intrinsics |
imu | bool | The recording: IMU columns |
action | bool | The recording: an action feature |
recorded_hand_pose | bool | The recording: recorded hand keypoints |
gripper_width | bool | The recording: a gripper-width column |
episodes | integer | The dataset version: number of episodes |
requires maps a fact to true, false, a string equal to the value, or a comparison such as ">=2" or "<600". If any condition is false, the step ends as not applicable: grey, never a failure. An unknown fact never satisfies a condition, so the step is not applicable rather than run on a guess. Your code can also decide at run time with ctx.not_applicable(code, message).
Resources
| Key | Default | Meaning |
|---|---|---|
cpu | 4 | CPU cores. |
memory | 8Gi | Memory, for example 8Gi or 16Gi. |
disk | 10Gi | Scratch space for ctx.tmp. |
gpu | None | None, or {"count": 1, "memoryGb": 16}. |
timeout | 30m | A duration such as 20m, or {"perInputMinute": …, "min": …, "max": …}. |
Every step gets at least 4 CPU cores and 8Gi of memory, and at least 8 GB of GPU memory when it asks for a GPU; a smaller value runs with that floor. The resources must fit on one machine, and a GPU larger than any compute pool offers is refused with ALGORITHM_GPU_UNSATISFIABLE.
An algorithm that asks for a GPU runs on the CUDA SDK base automatically, with PyTorch and CUDA 13 preinstalled, so you never choose a base image. Its test runs also use a GPU and wait until one has room; a deployment without a GPU refuses them with ALGORITHM_SAMPLE_RUN_GPU_UNSUPPORTED.
How code ends
How your code finishes tells the platform what happened, and each way ends the step differently. An image algorithm signals the same outcomes with its exit code.
| Python | Exit code (image) | Meaning | The step ends |
|---|---|---|---|
| return | 0 | Done. | succeeded |
ctx.not_applicable(code, msg) | 85 | This data is not for me. | not applicable |
ctx.input_unusable(code, msg) | 87 | The data is broken. | failed; the recording is quarantined |
raise TransientError(msg) | 88 | Try again: network, GPU busy. | retried, then failed |
| any other exception | 1 | A bug. | failed; the traceback is in the log |
Codes are capital letters, digits and _, 3 to 64 long. The step message reads <code>: <message>.
Versions and references
Workflows refer to an algorithm as embodiflow/<name>@<major> or team/<name>@<major>, and each run uses the highest saved version of that major. That way a fix reaches every workflow on its next run, while a breaking change waits until you switch to the new major.
A change to the ports, level, requires, the declared features, checks or labels, or the type of a parameter is a breaking change and needs a new major version; saving it as a minor version is refused with ALGORITHM_INTERFACE_CHANGED. A saved version never changes.
How run is called
run is called once per recording, once per episode at episode level, or once per dataset version for curate and export. Its arguments are bound by name, so take only the ones you need, in any order.
| Argument | Gets |
|---|---|
raw, recording, version | The main input. |
episode | Episode level only: the episode being processed. |
| An extra input name | A Features object, or None when the input is optional and absent. |
out | The writer of the category. |
ctx | The context: params, facts, recording_id, run_id, step_id, tmp, log, progress(done, total, message), not_applicable and input_unusable. |
state | What setup(ctx) returned. |
Use setup(ctx) to load a model once. Today the platform calls it once per job; a later version may process many items in one job and still call setup only once, with no change to your code. That's why run must not keep per-item state in globals.