TGL: Recipes

TGL: Recipes

Real-world patterns. Each recipe is small, copy-pasteable, and combines features from the rest of the guide.

Filter test users

#if startsWith($row.user_id, "internal_")
  $out = null
/if
$out = $row

Drop incomplete records

#if isEmpty($row.email)
  $out = null
/if

$out.email = lowerCase($row.email)
$out.name = $row.name

isEmpty catches null, "", whitespace-only strings, [], and {} in one shot, so a customer with " " (a single space) for their email is filtered out the same as one with null.

Empty string to null

#if isEmpty($row["Type 2"])
  $out.secondary_type = null
#else
  $out.secondary_type = $row["Type 2"]
/if

When a source field might be "" (especially with CSV ingestion, where empty cells parse as empty strings), use isEmpty to coerce blanks to null. Rollups treat null and "" as the same series, but downstream consumers (charts, exports, monitoring rules) read cleaner data when blanks are normalized at the transform layer.

Numeric coercion (CSV to math)

$out.line_total = multiply(float($row["Price"]), int($row["Quantity"]))

Default values

$out.country = $row.address.country ?? "US"
$out.label = $row.label ?? "untitled"
$out.is_active = $row.is_active ?? true

Sum line items

$out.total = 0
#each $row.line_items as $item
  $out.total = add($out.total, multiply($item.price, $item.qty))
/each

Build a tag array

$out.tags = null
#each $row.raw_tags as $tag
  #if !isEmpty($tag)
    $out.tags = push($out.tags, lowerCase(trim($tag)))
  /if
/each

Conditional output shape

$out.id = $row.id
$out.created_at = $row.timestamp

#if $row.type == "purchase"
  $out.amount_cents = round(multiply($row.amount, 100))
  $out.currency = $row.currency
#elseif $row.type == "refund"
  $out.amount_cents = round(multiply(subtract(0, $row.amount), 100))
  $out.refund_reason = $row.reason
#else
  $out = null
/if

Bucket by week

$out.week_start = toDateString(startOf(date($row.timestamp), "week"))
$out.event = $row.event
$out.user_id = $row.user_id

Normalize an email

$out.email = lowerCase(trim($row.email))
$out.email_domain = split($out.email, "@")[1]

Limits

TGL enforces hard limits to bound compilation and each record execution. Compile limits apply to the script as a whole. Runtime limits reset for every input record.

LimitDefaultWhat happens when exceeded
Transform script request65,536 UTF-16 code unitsRequest validation error; the script is not compiled or stored
Statement count10,000Compile error; script rejected at storage time
Expression or control-flow depth200 levelsParse or compile error; script rejected at storage time
Output path nesting50 levelsCompile error; script rejected at storage time
String length for one value10,485,760 UTF-16 code unitsThat value becomes null
Loop iterations per record1,000,000The record fails; processing continues with the next record
Output writes per record10,000The record fails; processing continues with the next record
Output array length10,000Oversized values become null; an out-of-range indexed write fails
Values and containers copied out100,000The record fails; processing continues with the next record
Cumulative output string/key bytes64 MiBThe record fails; processing continues with the next record

Tailglow rejects a transform request longer than 65,536 code units. Other compile limits reject the script before it is saved. Runtime string and array limits can turn an individual value into null; iteration, write, and cumulative output limits skip the affected record.

Errors and validation

Storage-time validation

When you create or update a transform, Tailglow validates the script before saving it as active. Scripts that fail compilation or output-field validation are not stored.

The output-field check compares the script’s statically known top-level writes with the view’s output schema. It is skipped for the exact mirror script $out = $row and for scripts with a dynamic top-level key such as $out[$row.key], because their field sets cannot be known at compile time. Other scripts are not stored until they pass both compilation and output-field validation.

Runtime errors

An invalid input record or a row that reaches a runtime safety limit is skipped while processing continues with the remaining records.

A structural output error, such as assigning an array to the root with $out = arrayValue, can mark a live transform as error or cause a backfill to fail after retries. Fix and save the script from the transform drawer.

Per-record skip

$out = null is not an error. It’s a normal filter signal. The record is dropped silently and the view continues.