Footguns
Things that bit other authors first, organized by where the trap is.
Configuration
Don't add numpy to requirements.txt
pipelogic already depends on numpy at the version the platform expects. Listing it again pulls a different version into the runtime image and produces silent crashes.
# WRONG — never list numpy
numpy==2.0.0
# Right — pipelogic ships numpy
Pin every dependency exactly
==1.2.3, never >=, ~=, or unpinned. Loose pins make builds non-reproducible and quietly pull in new CVEs on rebuild.
build_system matters
The default build_system: 2 is the lean Python runtime. ML components need a wider variant:
| build_system | Includes |
|---|---|
2 | Python + pipelogic + opencv-python-headless |
2-opencv4.11 | + system OpenCV |
2-cuda12.8-onnxrtgpu1.22 | + ONNX Runtime GPU |
2-cuda12.8-torch2.8-onnxrtgpu1.22 | + PyTorch + ORT |
2-cuda12.8-torch2.8-ultralytics | + Ultralytics |
2-cuda12.8-torch2.8-onnxrtgpu1.22-roboflow | + Roboflow inference |
Picking the wrong build_system means importing torch fails at runtime because torch isn't in the image. See the Models page for the full matrix.
Types
Numpy views from Bytes / List are zero-copy and short-lived
import numpy as np
arr = np.asarray(pipe_bytes) # zero-copy view, valid for current tick
arr = pipe_bytes.unsafe_numpy(dtype=np.uint8, shape=(h, w, 3)) # zero-copy with shape/dtype
arr = np.array(pipe_bytes, copy=True) # always-copy
arr = pipe_bytes.safe_numpy() # always-copy
Writes through the zero-copy view flow back into the underlying buffer (intentional for in-place preprocessing). The view is invalidated the next time the runtime advances the stream — don't keep it past the current tick. Use np.array(..., copy=True) or .safe_numpy() for values that need to outlive the call.
Default int is Int64, default float is Double
from pipelogic.types import List
L = List([1, 2, 3]) # element type: Int64
L = List([1.0, 2.0]) # element type: Double
L = List([1, 2, 3], '[Int32]') # explicit Int32
Don't construct dicts for typed values
The high-level wrappers exist for a reason — they handle the named-type registration and field layout for you:
# WRONG — manual dict construction breaks when type fields drift
return {"width": w, "height": h, "data": arr.tobytes(), "format": ...}
# Right — let the wrapper do it
return Image(arr, color_space=ColorSpace.BGR)
The same applies to BoundingBox, Tensor, AudioFrame, Mask, Landmark, etc. Prefer the wrappers in pipelogic.cv and pipelogic.types — they handle field layout and named-type registration for you. Drop down to raw dicts only when you have a reason the wrappers don't cover.
Workers
Virtual input is opt-in by parameter name
To receive virtual-input messages, name the parameter virtual_input on your worker function. The runtime detects the name and switches the worker into virtual-input mode:
def from_vin(virtual_input):
return virtual_input[0]
run(from_vin)
Returning a tuple from a single-output worker wraps it as one output
If component.yml declares one output_type and your function returns (a, b), the runtime serializes the tuple as your one output — it does not auto-split. To return multiple outputs, declare them in component.yml:
worker:
output_types:
- BoundingBox
- Image
then return (boxes, image) in declared order.
Stateful workers must return a dict
def stateful(x, state):
return {"output": x * 2, "state": state + 1} # required keys
Returning just the output value works in non-stateful mode but raises in stateful mode (state must be provided when stateful is True).
Virtual-output workers must return a list
def with_virtual_out(x):
return {"output": x, "virtual_output": [item1, item2]}
virtual_output must be a list (possibly empty) — the runtime calls set_on_push per item.
CV wrappers
Stereo audio is not auto-mixed to mono
AudioFrame.numpy() returns the original (samples, channels) shape. Many audio models expect mono — call .mono() explicitly:
audio_arr = audio.mono() # always 1-D, 16-bit-mixed
Image color space is enforced — don't lie
Image(arr, color_space=ColorSpace.RGB) advertises RGB on the wire. Downstream consumers that expect a specific color space (e.g., Image.to_gray() chain) compute the right conversion based on the declared color space. If you pass BGR data labeled as RGB, every downstream conversion is wrong.
image.numpy() returns the current color space
If the input was BGR, image.numpy() is BGR. If you want a specific space, call to_bgr()/to_rgb()/to_gray() — they always return the right space (and copy when conversion is needed).
Image.resize((h, w)) takes height first, then width
OpenCV is the opposite — cv2.resize(img, (w, h)). Pipelogic's wrapper takes (height, width) to match numpy shape conventions.
File and model paths
find_model_file requires exactly one match
If the directory has zero or two .pt files the helper raises. That's intentional — it forces you to be explicit about which weights file is "the model". For multi-file checkpoints, point at a parent directory and write your own loader.
ensure_local_dir respects offline mode
Set HF_HUB_OFFLINE=1 (or TRANSFORMERS_OFFLINE=1) in your container env to prevent any network calls. ensure_local_dir will refuse to download and the build-time-prefetched cache wins.
Init and startup
Don't import pipelogic lazily
pipelogic connects to the running backend at import time. If you delay the import, your component won't be able to attach to its streams. Put from pipelogic.worker import run at the top of main.py.
What's next
- Quickstart — clean start with no footguns.
- API: types — full reference for everything you might construct.