Custom Transforms
Reference for writing, registering, and managing Starlark transforms in Prelude Collector — value types, return contract, available globals, and the transforms API.
When the built-in transforms do not cover your case, you can write your own using Starlark — a deterministic, sandboxed scripting language with Python-like syntax. After this first mention, the rest of this page refers to Prelude Collector as "the collector".
Custom transforms are stored in the database, registered into the in-memory transform registry at startup, and hot-reloaded after any change so they become usable in field mappings without a restart.
What is Starlark?
Starlark is a dialect of Python designed for embedded scripting. In the collector it is:
- Python-like — familiar
if/else,for,while, string operations, list comprehensions. - Sandboxed — no file I/O, no network access, no module imports.
The only observable side effect is
print(...), which is wired to the collector's debug log channel (see Debugging below). - Deterministic — the same input always produces the same output.
- Step-limited — execution is capped at 1,000,000 Starlark steps to prevent infinite loops. There is no separate wall-clock timeout; a tight CPU-bound loop will run until the step cap trips.
The following extensions are enabled in addition to the Starlark core:
set()literals and operationswhileloops- Recursion
The expression you write
The collector wraps your code in a def transform(value): function.
You write only the body — the lines that go inside that function.
Function signature
The expression receives a single argument named value and must
return a value:
return value * 2
The collector internally builds:
def transform(value):
return value * 2
Argument types
The value argument is always one of:
| Starlark type | Origin |
|---|---|
int |
Any integer type (e.g. int, int64, uint32). |
float |
Floating-point numbers. |
str |
Strings. |
bool |
Booleans. |
bytes |
Raw byte sequences. Round-tripping is lossy: returning a bytes value from the transform converts it to a Go string via fmt.Sprintf("%v", …), not back to raw bytes. |
None |
A null / missing source value. |
Return contract
Return any of the same types listed above. Returning None is
treated as a failure: a warning is logged and the original value is
kept. Raising or otherwise erroring follows the same rule — see
Error handling on the overview page.
Available globals
All registered built-in transforms are exposed as callable globals inside your Starlark expression. You can call them directly by their registered name:
# Call the built-in lowercase transform from inside a custom transform
return lowercase(value)
This means a custom transform can reuse any built-in conversion as a helper instead of re-implementing it. For the full list of names available this way, see Built-in transforms.
In addition, one Starlark-only helper is provided that is not a pipeline transform:
| Name | Signature | Description | Example |
|---|---|---|---|
round |
round(number [, ndigits]) |
Rounds a number to ndigits decimal places (default 0); returns int when ndigits <= 0. |
round(3.7) → 4 |
Registering a custom transform
Custom transforms are managed through the collector REST API. For the full request and response schemas of every endpoint mentioned here, see the API reference.
Create
export BASE="https://collector.example.com"
export TOKEN="<your-api-token>"
curl -s -X POST "$BASE/api/v1/transforms" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "double_value",
"expression": "return value * 2"
}'
Bruno: 07 Transforms / Create transform
| Field | Type | Constraints | Description |
|---|---|---|---|
name |
string | Matches ^[a-z][a-z0-9_]*$ |
Unique name. Must not collide with a built-in. |
expression |
string | Valid Starlark body | The function body, without the def transform(value): wrapper. |
Response 201 Created with the created transform object.
400 Bad Request if the name is already used by another custom
transform, collides with a built-in, or fails the naming-rule regex.
Validation errors are returned in the standard form-error envelope
with the offending field listed under errors.
The expression is parsed and compile-checked at creation time, so syntactic errors are reported immediately.
List, read, update, delete
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/transforms |
List all user-defined transforms (paginated). |
GET |
/api/v1/transforms/{id} |
Get a transform by ID. |
PUT |
/api/v1/transforms/{id} |
Update an existing transform. |
DELETE |
/api/v1/transforms/{id} |
Delete a transform. |
Naming rules
Transform names must match ^[a-z][a-z0-9_]*$:
| Valid | Invalid |
|---|---|
my_transform |
MyTransform — uppercase start |
transform2 |
2transform — digit start |
x |
my-transform — hyphen not allowed |
my transform — space not allowed |
Names that collide with a built-in transform are rejected with
400 Bad Request and the name field flagged in the form-error
envelope.
Hot-reload behaviour
After a successful create, update, or delete the registry is refreshed and the new state becomes visible to active collections without a restart. Any subsequent collected value flowing through a field that references the transform uses the latest version.
For the equivalent debounce and re-subscription behaviour on the data-model side, see Custom Models — Hot-reload.
Examples
Multiply by a constant
return value * 2
Conditional logic
if value > 1000:
return value / 1000
return value
String manipulation
return value + " suffix"
Composing built-ins
# Convert to Mbps and round to 2 decimal places.
mbps = to_mbps(value)
return round(mbps, 2)
Chaining string operations
cleaned = trim_whitespace(value)
return uppercase(cleaned)
Test before saving
The API compile-checks the Starlark expression at creation time. You can also test a draft expression against a sample value from the web UI before registering it, which catches runtime errors before any collection sees the transform.
Debugging
Starlark's built-in print(...) is enabled and wired to the
collector's debug log channel. Any print() call emits a
starlark print debug record tagged with the transform's name and
the formatted message:
print("debug:", value)
return value * 2
Run the collector with debug logging on (PRELUDE_LOG_LEVEL=debug
or the equivalent for your deployment) to surface these messages.
There is no print rate limit, so remove debug calls from
production transforms — they remain cheap but they do count against
the per-call step budget.
Attaching a custom transform to a field
Once registered, a custom transform behaves exactly like a built-in.
Use its name in the transforms list of any field mapping:
{
"field": "speed_mbps",
"oid": "1.3.6.1.2.1.31.1.1.1.15",
"transforms": ["double_value", "to_mbps"]
}
Built-in and custom transforms can be mixed freely in the same chain, and they execute left to right as documented in Chaining.