Skip to content

This guide shows you how to create user-defined operators (UDOs) for your package. You’ll learn how to define operators with positional and named arguments, and how to test them with the Test Framework.

User-defined operators (UDOs) are reusable building blocks that you can use in your pipelines. Place operator files in the operators directory of your package.

Tenzir names operators using the convention <package>::[dirs...]::<basename>. For example, a file at operators/ocsf/map.tql in a package with ID acme becomes the operator acme::ocsf::map.

operators/ocsf/auth.tql
// Normalize authentication logs to OCSF Authentication event class
class_uid = 3002
category_uid = 3
activity_id = 1 if outcome == "success" else 2
actor.user.name = username
actor.user.uid = user_id
src_endpoint.ip = src_ip
dst_endpoint.ip = dst_ip
drop username, user_id, src_ip, dst_ip, outcome

After installing the package, use the operator in any pipeline:

from_file "auth.json"
acme::ocsf::auth
publish "ocsf-events"

Operators can accept positional and named arguments, enabling you to create flexible, reusable building blocks that match the calling conventions of built-in operators. Define parameters in a YAML frontmatter block at the beginning of the .tql file.

Positional arguments are passed in order when calling the operator. Define them under the args.positional key:

operators/tag.tql
---
args:
positional:
- name: field
type: field
- name: value
type: string
---
$field = $value

Call this operator with positional arguments:

from {x: 1}
acme::tag name, "Alice"
{x: 1, name: "Alice"}

Named arguments use the name=value syntax and can have default values. Define them under the args.named key:

operators/tag.tql
---
args:
positional:
- name: field
type: field
- name: value
type: string
named:
- name: prefix
type: string
default: ""
---
$field = $prefix + $value

Call this operator with both positional and named arguments:

from {x: 1}
acme::tag name, "Alice", prefix="User: "
{x: 1, name: "User: Alice"}

Each parameter supports the following fields:

FieldRequiredDescription
nameYesParameter name, used as $name in the operator body
typeNoType constraint for the parameter value
descriptionNoDocumentation string for the parameter
defaultNoDefault value if the argument is not provided

The type field constrains what values the parameter accepts:

TypeDescription
fieldA field selector (for example, name). Cannot have defaults
stringA string literal or expression
intAn integer value
doubleA floating-point value
boolA boolean value
ipAn IP address
secretA secret string (accepts string literals)

If you omit the type field, the parameter accepts any value.

Positional arguments with a default value become optional. Callers can omit them, and Tenzir substitutes the default:

operators/greet.tql
---
args:
positional:
- name: name
type: string
default: "World"
---
greeting = "Hello, " + $name + "!"

Calling the operator without arguments uses the default value:

from {}
acme::greet
{greeting: "Hello, World!"}

Passing an explicit argument overrides the default:

from {}
acme::greet "Alice"
{greeting: "Hello, Alice!"}

The field type enables dynamic field selection. The caller passes a field path, and the operator uses it to read or write data:

operators/double_value.tql
---
args:
positional:
- name: target
type: field
---
$target = $target * 2

Using the operator:

from {count: 5, score: 10}
acme::double_value count
{count: 10, score: 10}

Parameterized operators can call other operators, including passing through their own parameters:

operators/transform.tql
---
args:
positional:
- name: field
type: field
named:
- name: multiplier
type: int
default: 2
---
utils::scale $field, factor=$multiplier

Tenzir validates parameter types at compile time when possible:

  • Compile-time checking occurs when arguments are constant values
  • Runtime checking defers validation for expressions containing runtime data

If a type mismatch occurs, Tenzir reports an error with the expected type and shows usage information for the operator.

Last updated: