TGL: Control flow

TGL: Control flow

TGL has three control-flow primitives: filtering a row out of the output, branching with #if, and looping with #each. All use Svelte-style directives (no curly braces).

Filtering: drop a row from the output

If you assign null to $out (the whole thing, not a field), the row is dropped from the output entirely:

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

$out.user_id = $row.user_id
$out.event = $row.event

When a row is dropped, no output record is emitted. The view simply has fewer records than the source.

This is the only way to filter rows. Everything else (writing to $out fields conditionally, etc.) still emits a record, just with different content.

A few notes on the rule:

  • The skip is sticky: once $out = null runs, later writes have no effect. Use #if/#else if you want to choose between drop and emit.
  • Expressions that evaluate to null also drop. So $out = $row.payload drops the row whenever payload is null (and merges the payload into $out otherwise).
  • Other primitive assignments ($out = "", $out = 42, $out = false) are no-ops, not drops. They leave $out unchanged.
  • $out.field = null writes a null field. It doesn’t drop the row.

Conditionals

#if / #elseif / #else / /if:

#if $row.score > 90
  $out.grade = "A"
#elseif $row.score > 75
  $out.grade = "B"
#elseif $row.score > 60
  $out.grade = "C"
#else
  $out.grade = "F"
/if

The condition can be any expression. TGL uses standard truthiness: null, undefined, 0, "", false, and NaN are falsy; everything else is truthy.

Conditionals can be nested freely:

#if $row.user
  #if !isEmpty($row.user.email)
    $out.email = lowerCase($row.user.email)
  /if
/if

There’s no inline ternary (a ? b : c). Use #if/#else for branching values, or ?? for null-coalescing defaults (covered in Operators).

Loops

#each ... as $item / /each:

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

Note the accumulator pattern: the script initializes $out.total to 0, then the loop builds up the sum. add also treats a missing or null operand as 0, so $out.total = add($out.total, value) works even without the explicit initialization.

You can also bind an index:

#each $row.tags as $tag, $idx
  $out.tags = push($out.tags, lowerCase($tag))
/each

$tag is each element; $idx is the 0-based position.

If the collection isn’t an array (it’s null, an object, or a string), the loop body is silently skipped. That loop expression does not throw merely because it resolves to a different type.

There’s a hard cap on total loop iterations per record (the default is 1,000,000) to prevent runaway scripts. Hit it and the row produces an error and is skipped; the rest of the records continue processing normally.

What’s next

  • Operators: comparison, logical, ??, type semantics.
  • Functions: every built-in.
  • Recipes: real-world patterns combining filtering, branching, and looping.