---
title: "Component Builder SDK"
description: "Write custom components in Python, package them as a library, and bundle the dependencies they need"
source_url: https://ai-ops.com/docs/python-packages/component-builder
---

# Component Builder SDK

`koios-component-builder` is the SDK and command-line tool for building
component libraries. A component is a reusable block of logic with typed
inputs and outputs that Koios wires together and executes at a configurable
scan rate.

This page is the author's reference. For using components once they are
installed, see [Components](https://ai-ops.com/docs/components/introduction.md) and
[The Component Canvas](https://ai-ops.com/docs/components/canvas.md).

## Installation

```bash
pip install koios-component-builder
```

Requires Python 3.12 or newer.

## Your first component

```python
# my_library/math_ops.py
from koios_component_builder import (
    Component,
    ComponentCategory,
    ComponentIcon,
    Input,
    Output,
)

class SimpleAdder(Component):
    """A component that adds two numbers."""

    class Meta:
        icon = ComponentIcon.SUM
        category = ComponentCategory.MATH

    a: Input[float] = Input(default=0.0, description="First number")
    b: Input[float] = Input(default=0.0, description="Second number")
    result: Output[float] = Output(default=0.0, description="Sum of a and b")

    def execute(self) -> None:
        self.result = self.a + self.b
```

Group components into a library:

```python
# my_library/__init__.py
from koios_component_builder import ComponentLibrary
from .math_ops import Multiplier, SimpleAdder

class MyLibrary(ComponentLibrary):
    """My custom component library."""

    name = "my-library"
    major = 1
    minor = 0
    patch = 0
    description = "Custom math components"

    components = [SimpleAdder, Multiplier]

__all__ = ["MyLibrary", "SimpleAdder", "Multiplier"]
```

Then package it:

```bash
koios-component-builder export my_library/
```

That writes a `.kcl` package into `dist/`, ready to upload. See
[Component Libraries](https://ai-ops.com/docs/components/libraries.md).

## How a component runs

Each execution cycle does four things:

1. Input and configuration values are set on the component
2. `setup()` runs, once, before the first execution
3. `execute()` runs with the current input values
4. Output values are sent wherever they are wired

You write `setup()` and `execute()`. Koios handles wiring and data flow.

### Doing expensive work once

Override `setup()` for anything that should not repeat every cycle — loading a
model, building a registry, parsing configuration. Every field value is
available by the time it runs.

```python
class UnitConverter(Component):
    """Converts Fahrenheit to Celsius."""

    class Meta:
        icon = ComponentIcon.TRANSFORM
        category = ComponentCategory.TRANSFORM

    value: Input[float] = Input(default=0.0, description="Temperature in °F")
    decimals: NumberConfig = NumberConfig(
        default=2, min_value=0, max_value=6, description="Decimal places"
    )
    result: Output[float] = Output(default=0.0, description="Temperature in °C")

    def setup(self) -> None:
        from pint import UnitRegistry
        self._ureg = UnitRegistry()

    def execute(self) -> None:
        temp = self._ureg.Quantity(self.value, self._ureg.degF)
        self.result = round(temp.to(self._ureg.degC).magnitude, int(self.decimals))
```

If `setup()` raises, the instance is marked failed and retried next cycle.

For something shared across *every* instance of a class rather than per
instance, guard at class level instead:

```python
class MyComponent(Component):
    def execute(self) -> None:
        if not hasattr(MyComponent, "_shared_model"):
            MyComponent._shared_model = load_model()
        self.result = MyComponent._shared_model.predict(self.input)
```

## Fields

### Inputs and outputs

Supported types are `float`, `int`, `bool`, `str`, `list`, and `dict`.

```python
class ExampleComponent(Component):
    temperature: Input[float] = Input(default=0.0, description="Temperature in Celsius")
    enabled: Input[bool] = Input(default=True, description="Enable processing")

    alarm: Output[bool] = Output(default=False, description="High temperature alarm")
    status: Output[str] = Output(default="ok", description="Current status")

    def execute(self) -> None:
        if self.enabled and self.temperature > 100:
            self.alarm, self.status = True, "overtemp"
        else:
            self.alarm, self.status = False, "ok"
```

### Configuration

Configuration fields are set when an operator creates the instance and stay
constant while it runs. They appear as controls on the component node.

```python
from koios_component_builder import (
    BoolConfig, ChoiceConfig, Component, Input, NumberConfig, Output, StringConfig,
)

class ConfigurableComponent(Component):
    threshold: NumberConfig = NumberConfig(
        default=75.0, min_value=0.0, max_value=100.0, description="Alert threshold"
    )
    mode: ChoiceConfig = ChoiceConfig(
        default="average", choices=["average", "median", "max"],
        description="Calculation mode",
    )
    label: StringConfig = StringConfig(default="Sensor", description="Display label")
    verbose: BoolConfig = BoolConfig(default=False, description="Enable verbose output")

    value: Input[float] = Input(default=0.0)
    alert: Output[bool] = Output(default=False)

    def execute(self) -> None:
        self.alert = self.value > self.threshold
```

### Files

`FileConfig` gives the operator an upload control. At runtime your component
receives a handle, not a raw path, pointing at the file uploaded for that
instance. Each instance has its own file, and uploads are versioned, so an
operator can swap a model and revert.

```python
from koios_component_builder import Component, FileConfig, Input, Output

class Scorer(Component):
    reading: Input[float] = Input(default=0.0)
    score: Output[float] = Output(default=0.0)

    model_file: FileConfig = FileConfig(
        description="Trained ONNX model",
        extensions=[".onnx", ".tflite"],
        mime_types=["application/octet-stream"],
        max_bytes=200 * 1024 * 1024,
        required=True,
    )

    def setup(self) -> None:
        import onnxruntime
        self.session = onnxruntime.InferenceSession(str(self.model_file.path))

    def execute(self) -> None:
        self.score = float(self.session.run(None, {"x": [[self.reading]]})[0])
```

Load the file in `setup()` rather than `execute()`. Koios re-runs `setup()`
after an operator replaces the file, so a swap takes effect without a restart.

`required` decides what happens when nothing is uploaded, so you never write
that check yourself:

| | Behavior |
|---|---|
| `required=True` | The instance fails with a specific message *before* `setup()` runs, so your code can assume the file is there |
| `required=False` | The field is `None`. Guard with `if self.field:` |

The handle exposes `path`, `name`, `suffix`, `size_bytes`, `content_type`,
`sha256`, `uploaded_at`, and `version`, plus `read_bytes()`, `read_text()`,
`open()`, and `exists()`. Reading through the handle instead of the `open`
builtin keeps your component out of the security audit's file-I/O tier.

```python
class Lookup(Component):
    table: FileConfig = FileConfig(extensions=[".csv"])

    def setup(self) -> None:
        self.rows = self.table.read_text().splitlines() if self.table else []
```

Koios enforces your constraints on the server, so the declaration holds no
matter what uploads the file. Extension is the reliable gate; MIME type only
filters the file dialog, since a browser can claim anything and `.onnx` is in
no MIME registry. `max_bytes` is optional — omit it and a platform ceiling
applies. A value above that ceiling is clamped to it, so a field can lower the
cap but never raise it. Files are never executed; they are handed to the
component that asked for them.

See [Files for components](https://ai-ops.com/docs/components/canvas.md).

### Historical data

A `HistoryInput` reads recorded values from historical storage. It must be
wired to a history connector on the canvas.

```python
from koios_component_builder import Component, HistoryInput, Output

class TrendAnalyzer(Component):
    """Calculates trend from historical data."""

    sensor_history: HistoryInput = HistoryInput(description="Historical sensor readings")
    trend: Output[float] = Output(default=0.0, description="Trend slope")

    def execute(self) -> None:
        if self.sensor_history is None:
            return

        df = self.sensor_history.get_history(period_seconds=3600, num_samples=200)
        if not df.empty:
            values = df["value"].tolist()
            self.trend = values[-1] - values[0]
```

The returned frame has `timestamp` and `value` columns.

## Appearance

```python
class MyComponent(Component):
    """Component description shown to operators."""

    class Meta:
        icon = ComponentIcon.CHART_LINE
        category = ComponentCategory.ANALYSIS
        canvas_width = 8          # 4-15 grid units, default 6
        canvas_minimal = False    # compact mode, no header or footer

        major = 2                 # optional, overrides the library version
        minor = 1
        patch = 0
        prerelease = "beta"
```

Icons take any Tabler icon name in kebab-case; constants include `SUM`,
`CALCULATOR`, `CHART_LINE`, `GAUGE`, `THERMOMETER`, `FILTER`, `WAVE_SINE`,
`TOGGLE_LEFT`, `ALERT_TRIANGLE`, and `TRANSFORM`. Categories include `MATH`,
`STATISTICS`, `LOGIC`, `ANALYSIS`, `TRANSFORM`, `FILTER`, `CONTROL`, and
`MONITORING`, and custom strings are accepted.

### Arranging pins

Pins follow class-body order by default. Where grouping matters — setpoints
together, tuning constants together, status outputs apart — declare the
arrangement in `Meta` and insert gaps. It ships in the package, so every
instance starts from the same layout, and an operator can still adjust one
instance.

```python
from koios_component_builder import Component, ComponentIcon, Gap, Input, Output

class PIDController(Component):
    """Discrete PID controller with anti-windup."""

    class Meta:
        icon = ComponentIcon.GAUGE
        canvas_width = 8

        inputs_layout = [
            "setpoint",
            "process_variable",
            Gap(),
            "kp", "ki", "kd",
            Gap(size=2),
            "enable", "reset",
        ]
        outputs_layout = ["control_output", Gap(), "saturated", "integral"]

    setpoint: Input[float] = Input(default=0.0, description="Target value")
    process_variable: Input[float] = Input(default=0.0, description="Measured value")
    kp: Input[float] = Input(default=1.0, description="Proportional gain")
    ki: Input[float] = Input(default=0.0, description="Integral gain")
    kd: Input[float] = Input(default=0.0, description="Derivative gain")
    enable: Input[bool] = Input(default=True, description="Enable control")
    reset: Input[bool] = Input(default=False, description="Reset integrator")

    control_output: Output[float] = Output(default=0.0, description="Manipulated variable")
    saturated: Output[bool] = Output(default=False, description="Output is at a limit")
    integral: Output[float] = Output(default=0.0, description="Current integrator state")
```

`Gap(size=1)` inserts a spacer measured in pin heights. Names are validated
when the class is created, so a typo raises on import rather than at runtime.
Pins you leave out of a layout are appended in declaration order, so adding a
pin does not force a layout edit.

> [!WARNING] order= is deprecated on pins
> The per-field `order=` parameter on `Input`, `Output`, and `HistoryInput` is
> deprecated in favor of `inputs_layout` and `outputs_layout`, which support
> gaps and explicit grouping. `order=` on configuration fields is unaffected.

## Dependencies

A library can declare third-party packages. The builder resolves them against
the platform manifest to work out what is already available.

```python
class MyProtocolLibrary(ComponentLibrary):
    name = "my-protocol-library"
    major = 1
    minor = 0
    dependencies = ["crcmod", "minimalmodbus>=2.0"]
    components = [MyDevice]
```

| Tier | Meaning | Example |
|---|---|---|
| Platform | Already installed | `numpy`, `pandas`, `scipy` |
| Bundled | Downloaded into the package | `crcmod`, `pint` |
| SDK | Always available | `pydantic`, `click` |

```bash
koios-component-builder export my_library/
koios-component-builder platform-packages
```

> [!WARNING] Bundling into a library is deprecated
> `--include-deps` still works and prints a warning, but it will be removed in
> a future major release. Attach a package stack to the component environment
> instead: stacks are curated per environment, isolated from the platform's own
> versions, and shared by every library running there. See
> [Stacks](https://ai-ops.com/docs/components/stacks.md).

### Tools that are not Python packages

`platform-packages` lists Python distributions only. Some libraries shell out
to a solver instead, installed as a system package — those never appear in that
listing, and declaring them as a dependency will not work.

| Tool | Provides | Available from |
|---|---|---|
| `glpsol` | Linear and mixed-integer programming solver | Koios 1.2.0 |

```python
import pyomo.environ as pyo

results = pyo.SolverFactory("glpk").solve(model)
```

`pyomo` itself is not pre-installed, so declare it as a dependency. For a
solver that ships as a wheel and needs no system package, `highspy` bundles
like any other package and is generally faster on larger problems.

## Building a stack bundle

A stack is a named, isolated set of packages that component environments can
attach to. It exists for dependencies that conflict with the platform's own — a
library capping a package below the version the platform ships, for example.

Wheels can be uploaded individually, or exported here as a single `.kps`
bundle:

```bash
koios-component-builder export-stack -r requirements.txt

koios-component-builder export-stack --from-env --name ml-tools

koios-component-builder export-stack -r requirements.txt \
    --platform manylinux2014_x86_64 --python-version 3.12 --timeout 1800
```

Wheels are downloaded for the target platforms — linux amd64 and arm64 by
default — never copied from your local environment, so a bundle built on a Mac
installs on the server. Packages the platform already provides are pinned
during resolution and then left out.

**Every target platform must resolve.** A package with no wheel for one
architecture fails the whole export rather than producing a bundle that
installs on one and silently claims both. Either pass `--platform` for the
architecture you deploy on, or pin the package to a version publishing wheels
for both. The same applies if the platforms resolve to *different* versions:
the manifest records one version per package, so the export names them and
stops.

Requirements files may include others with `-r other.txt`; other option lines
are rejected. Every export ends by listing what it withheld and why.

> [!CAUTION] A stack runs with full privileges
> Code installed into a stack runs with the same privileges as the platform on
> your server. Only bundle wheels from sources you trust.

No Koios distribution is ever bundled — those are the packages the engine loads
your components *with*, and a stack carrying its own copy replaces them at
import time and the worker cannot start. Naming one in a requirements file
stops the export.

```text
ml-tools.kps
├── manifest.json
└── wheels/
    ├── mlflow-3.13.0-py3-none-any.whl
    └── pandas-2.3.3-cp312-cp312-manylinux2014_x86_64.whl
```

## Package format

```text
my-library-1.0.0.kcl
├── manifest.json
├── my_library-1.0.0-py3-none-any.whl
└── deps/
    ├── crcmod-1.7-cp312-...-x86_64.whl
    └── crcmod-1.7-cp312-...-aarch64.whl
```

## Security audit

Every export runs a static analysis of your source and sorts what it finds into
three tiers.

| Tier | Meaning | Effect |
|---|---|---|
| Allow | Safe | Not reported |
| Review | Worth attention | Logged as a warning, and marked when the library is uploaded |
| Deny | Blocked | Export fails unless you override it |

**Denied** are patterns with no legitimate use in a component: imports such as
`os`, `subprocess`, `socket`, `threading`, `pickle`, `ctypes` and `sys`; the
`eval`, `exec`, `compile` and `__import__` builtins; and sandbox-escape
attributes like `__subclasses__`, `__builtins__`, `__code__` and `__globals__`.

**Reviewed** are patterns that are often legitimate: `pathlib`, `io` and `csv`,
calls to `open`, and third-party packages outside the platform allow-list.

```bash
koios-component-builder audit my_library/
koios-component-builder audit my_library/ --json
koios-component-builder audit my_library/ --policy security_policy.yaml
```

The audit exits `1` when anything is denied, which makes it usable as a build
step.

### Custom policies

```yaml
policy_version: "1.0"

imports:
  allow:
    - my_internal_sdk
    - requests
  deny:
    - pandas

builtins:
  review:
    - open

attributes:
  deny:
    - __dict__
```

Overrides work in both directions: move something to a looser tier or a
stricter one, and it is removed from any conflicting tier automatically.

If a component genuinely needs a flagged pattern — loading a model from disk in
`setup()`, say — prefer `pathlib` or `io`, which are reviewed rather than
denied. For the rare case that needs a denied import, `--allow-unsafe` still
produces the package, but the manifest records that the audit did not pass and
the library is marked accordingly when uploaded.

## Command reference

```bash
koios-component-builder export <source_path> [OPTIONS]
  -o, --output PATH        Output directory (default: dist/)
  --include-deps           Bundle non-platform dependencies (deprecated)
  --platform TEXT          Target platform tag (repeatable)
  --allow-unsafe           Package even with denied findings
  --policy PATH            Custom security policy file

koios-component-builder export-stack [OPTIONS]
  -r, --requirements PATH  Requirements file to bundle (repeatable)
  --from-env               Bundle the current environment instead
  --name TEXT              Stack name (required with --from-env)
  -o, --output PATH        Output directory (default: dist/)
  --platform TEXT          Target platform tag (repeatable)
  --python-version TEXT    Python version to resolve for
  --timeout INTEGER        Seconds to allow for the download (default: 900)

koios-component-builder audit <source_path> [OPTIONS]
  --policy PATH            Custom security policy file
  --json                   Output results as JSON

koios-component-builder platform-packages
```

## Testing locally

Components run outside Koios, so you can exercise one directly:

```python
from my_library.math_ops import SimpleAdder

adder = SimpleAdder("test-instance")
adder.a = 5.0
adder.b = 3.0
adder.execute()

print(adder.result)
```
