Authoring Operators#

Operators are pieces of code that perform work on data coming from other operators. You chain them together to build a data pipeline.

Getting operators into podman storage#

Premade operators live under operators/. Podman needs to know about them before a pipeline can run.

docker bake + podman pull#

We can use docker bake and podman pull for operators defined in docker-bake.hcl.

cd interactEM
make setup # no need to run this if you already have

# Build all operators and pull them into podman storage
make operators

# Or build a specific operator and pull it into podman storage
make operator target=center-of-mass-partial

For faster iteration during development, you can omit the --build-base flag by using the lower-level bake.sh script directly:

./operators/bake.sh --push-local --pull-local --target center-of-mass-partial

If you create an operator, you can add it to the HCL file so it becomes part of this build process. This does not happen automatically—update the file manually when you are ready.

podman build#

If you create an operator using the instructions below, you can also use a regular podman build.

For example, if you have an operator named my-operator in the my-operator/ directory:

cd my-operator
# tag should match what is found in the "image" field in your operator.json
podman build -t ghcr.io/nersc/interactem/my-operator:latest .

Creating an operator#

At minimum, each operator needs three files:

  1. run.py

  2. Containerfile

  3. operator.json

The CLI ships with templates to get you started:

uv sync
uv run interactem operator new

or with poetry:

poetry install
poetry run interactem operator new

Or, if you prefer pip:

pip install .
interactem operator new

Note: Operators refresh only in the operators panel during a UI refresh. Existing pipelines are not updated automatically—replace operators manually if needed.

run.py#

Implement the operator logic in run.py. Incoming messages are BytesMessages that carry data, metadata, and tracking information. Before building, you can quickly check importability with:

python -m py_compile run.py

Containerfile#

Use the operator base image as the parent image for your Containerfile. For example, the distiller-streaming image includes utilities for processing 4D Camera frames, so other operators can build FROM it.

Specification#

Operator specifications live in operator.json. The schema is defined in spec.py, and an example is provided in operators/center-of-mass-partial/operator.json.

Examples (docs only)#

@operator
def com_partial(
    inputs: BytesMessage | None, parameters: dict[str, Any]
) -> BytesMessage | None:
    if not inputs:
        logger.warning("No input provided to the subtract operator.")
        return None

    center = None
    init_center_x = parameters.get("init_center_x")
    init_center_y = parameters.get("init_center_y")
    if init_center_x is not None and init_center_y is not None:
        center = (init_center_x, init_center_y)

    crop = None
    crop_to_x = parameters.get("crop_to_x")
    crop_to_y = parameters.get("crop_to_y")
    if crop_to_x is not None and crop_to_y is not None:
        crop = (crop_to_x, crop_to_y)

    batch = BatchedFrames.from_bytes_message(inputs)
    com = com_sparse(batch, init_center=center, crop_to=crop, replace_nans=False)

    return COMPartial(header=batch.header, array=com).to_bytes_message()

Containerfile examples#

ghcr.io/nersc/interactem/distiller-streaming#
FROM interactem-operator

WORKDIR /app
COPY ./pyproject.toml ./poetry.lock ./README.md /app/

# Base image installs interactem-core at /interactem/core. 
# Locally, the project uses ../../backend/core which
# resolves to /backend/core inside the container. We symlink it here 
# so poetry can find it.
RUN mkdir -p /backend && \
	if [ -d /interactem/core ]; then \
        ln -sfn /interactem/core /backend/core; \
    fi && \
    if [ -d /interactem/operators ]; then \
        ln -sfn /interactem/operators /backend/operators; \
    fi

RUN poetry install --no-root --without dev

COPY ./distiller_streaming/ /app/distiller_streaming/
RUN poetry install --only-root
ghcr.io/nersc/interactem/center-of-mass-partial#
FROM distiller-streaming

COPY ./run.py /app/run.py

Specification model#

Specification model#
class OperatorSpec(BaseModel):
    id: OperatorSpecID
    label: str  # Human readable name of the operator
    description: str  # Human readable description of the operator
    image: str  # Contain image for operator
    inputs: list[OperatorSpecInput] | None = None  # List of inputs
    outputs: list[OperatorSpecOutput] | None = None  # List of outputs
    parameters: list[OperatorSpecParameter] | None = None  # List of parameters
    tags: list[OperatorSpecTag] | None = None  # List of tags to match on
    parallel_config: ParallelConfig | None = None  # Parallel execution config
    triggers: list[OperatorSpecTrigger] | None = None  # List of triggers
Example operator.json#
{
  "id": "70dd71a7-5ebf-4515-8bf9-941d1284328c",
  "image": "ghcr.io/nersc/interactem/center-of-mass-partial",
  "label": "Partial Center of Mass",
  "description": "Calculates the center of mass for a frame",
  "inputs": [
    {
      "name": "in",
      "label": "The input",
      "type": "frame",
      "description": "Input frame"
    }
  ],
  "outputs": [
    {
      "name": "com_partial",
      "label": "The output",
      "type": "com_partial",
      "description": "Partial center of mass"
    }
  ],
  "parameters": [
    {
      "name": "crop_to_x",
      "label": "Crop To X",
      "type": "int",
      "default": "255",
      "description": "X-coordinate to crop to",
      "required": false
    },
    {
      "name": "crop_to_y",
      "label": "Crop To Y",
      "type": "int",
      "default": "255",
      "description": "Y-coordinate to crop to",
      "required": false
    },
    {
      "name": "init_center_x",
      "label": "Initial Center X",
      "default": "255",
      "type": "int",
      "description": "Initial Center X-coordinate for center of mass calculation",
      "required": false
    },
    {
      "name": "init_center_y",
      "label": "Initial Center Y",
      "default": "255",
      "type": "int",
      "description": "Initial Center Y-coordinate for center of mass calculation",
      "required": false
    }
  ],
  "parallel_config": {
    "type": "embarrassing"
  }
}