Skip to content

Transforms & CXL — compute new fields

The question for this lesson: your source gives you the columns that were in the file. But you almost always need new columns: a flag, a category, a cleaned-up value. How do you compute a new field for every record?

  • Read and write a transform node that adds fields with CXL.
  • Use emit, comparisons, if/then/else, and a method like .to_int().
  • Pull a value out of a channel with $vars, so one pipeline serves many deployments.
  • Validate a pipeline’s config and declared types with --dry-run before you run it.

This lesson assumes you’ve met the pipeline mental model and the source/transform/output nodes from What a pipeline is. If “record” or “node” feels fuzzy, start there.

CXL is the little language inside a transform’s cxl: block. You write one statement per line. The ones you’ll use constantly:

StatementWhat it doesExample
emit <name> = <expr>add or overwrite a columnemit total = price * qty
let <name> = <expr>a temporary value (not a column)let net = price - discount
filter <condition>drop rows where the condition is falsefilter status == "active"
distinctkeep only unique rowsdistinct by id

Expressions use the operators you’d expect (+ - * /, == != > <, and or not), plus if <cond> then <a> else <b>, and methods on values like .to_int(), .upper(), .trim().

🌱 New here? — why .to_int()?

A value read from a CSV starts life as text. "4000" is the characters 4-0-0-0, not the number 4000, so "4000" > 10000 would compare text, not numbers. .to_int() converts the text to a whole number first, so the comparison means what you expect. (There’s also .to_float(), .to_string(), and lenient try_int() that yields nothing instead of an error on bad input.)

This is the canonical customer_etl pipeline. The second transform tags each customer by lifetime value. Predict the output, then run it.

pipeline.yaml // editable

// quick check

Why is it `lifetime_value.to_int() > 10000` and not just `lifetime_value > 10000`?

Add a third column. After the final_flag transform computes tier, add one more emit line so the output also includes a greeting column equal to "Hello " + status.

Validate: catch a config mistake with --dry-run

Section titled “Validate: catch a config mistake with --dry-run”

Before you run a pipeline over real data, --dry-run reads the config and checks its structure and declared types, without processing a single record. This pipeline has a mistake of that kind: the schema declares lifetime_value as dollars, which is not a real type. Predict what --dry-run does, then run it.

pipeline.yaml // editable

--dry-run catches config and type mistakes before any data is read: a missing required field, or, as here, a schema type that doesn’t exist. The error names the valid types and the run stops with exit 1, so you fix it in a second instead of after a long job. (Fix it by declaring lifetime_value as string, the way the working pipeline above does.)

Not every mistake surfaces this early, though. --dry-run validates the config; it does not trace your data flow, so a logic error like pointing a node’s input: at a misspelled name isn’t caught here. That one only shows up when you actually run the pipeline.

Hard-coding 10000 means every team gets the same threshold. Instead, read it from a channel variable with $vars. The pipeline declares the variable; a channel file overrides it per deployment.

# customer_etl.yaml — declare the variable, then read it
pipeline:
name: customer_etl
vars:
gold_threshold: { type: int, default: 10000 }
nodes:
# ... source as before ...
- type: transform
name: final_flag
input: customers
config:
cxl: |
emit tier = if lifetime_value.to_int() > $vars.gold_threshold then "gold" else "standard"
# channels/acme-corp/customer_etl.channel.yaml — Acme wants a higher bar
channel:
name: acme-corp
target: ./customer_etl.yaml
vars:
static:
gold_threshold: { type: int, default: 50000 }

Run it with clinker run customer_etl.yaml --channel channels/acme-corp/customer_etl.channel.yaml, and Acme’s customers need 50000 to reach “gold”: same pipeline, no logic changed.

// quick check

What does a channel let you change about a pipeline?