Data types

WEL has 16 data types. Use the right type when data enters or leaves an expression to ensure bad values don't cause a transformation to silently fail.

_ is the input your expression receives in these examples. Refer to Input variables for more information.

FEATURE AVAILABILITY

WEL is currently available to select customers. Contact your Customer Success Representative to confirm whether it is available in your workspace.

Two rules govern how every type in WEL behaves:

  • WEL doesn't coerce by default: Passing a number where a String is expected raises E100 rather than converting it. Nothing is silently reinterpreted to make an expression succeed, except inside string interpolation, which always converts automatically, and two Transform data flags that configure specific coercions.
  • Collections never become strings by accident: A List or Map is only serialized by an explicit call such as to_json.

Both rules exist because a transformation that fails is visible, and one that silently produces the wrong value isn't.

Quick reference

WEL contains the following data types:

TypeDescriptionLiteral
NullThe absence of a valuenull
BoolA booleantrue, false
IntegerA whole number, arbitrary precision42
FloatA 64-bit IEEE 754 floating-point number3.14
DecimalAn exact decimal, arbitrary precisionDecimal('1.00')
StringUTF-8 text'hello'
BinaryRaw bytes0x"48656C6C6F"
PlainDateA calendar date, no time, no zonePlainDate('2026-03-15')
DateTimeAn exact instant, with an offsetDateTime('2026-03-15T09:30:00Z')
PlainDateTimeA date and time, no zonePlainDateTime('2026-03-15T09:30:00')
PlainTimeA time of day, no date, no zonePlainTime('09:30:00')
DurationA length of timeDuration('PT2H30M')
ListAn ordered collection[1, 2, 3]
MapA key-value collection{a: 1}
SkipA marker meaning "omit this"Skip()
LambdaAn inline functionx ~> x * 2

CHECK A VALUE'S TYPE

Use type_of to check a value's type.

Numbers

WEL has three numeric types, and the choice between them is the most consequential type decision in most integrations.

Integer

An Integer is a whole number, with arbitrary precision. There is no 64-bit ceiling and no silent loss above 2^53.

No 64-bit ceiling

The result in the following example is past the point where a JavaScript number would lose precision. The result remains precise because the numbers are integers which have no 64-bit ceiling:

FormulaResult
9007199254740993 + 19007199254740994

This allows record identifiers from systems that issue large integer keys to arrive intact.

Float

A Float is a binary floating-point number. Float arithmetic is fast, and appropriate for measurements, ratios, percentages, and scientific values. Float can't represent most decimal fractions exactly. Use Decimal in cases where fraction precision matters, such as when working with money.

Fraction imprecision

The following example shows the imprecision of floats when working with fractions:

FormulaResult
0.1 + 0.20.30000000000000004

This happens because floats store numbers in base 2, where most decimal fractions have no exact representation. The same rounding appears in any language that uses binary floating point.

Decimal

Decimal provides exact decimal arithmetic at arbitrary precision. Use it for cases where fraction precision matters, such as when working with money.

Decimals preserve scale

Decimal preserves scale, so results in the following examples retain their digit count rather than picking up extra digits or collapsing:

FormulaResult
Decimal('0.1') + Decimal('0.2')Returns 0.3 with no additional digits.
Decimal('12.50') + Decimal('0.00')Returns 12.50 without collapsing to 12.5.

This lets currency keep its exact value all the way to a destination.

USE STRINGS FOR DECIMAL CONVERSIONS

Pass the Decimal() conversion a string such as Decimal('19.99'), not a float such as Decimal(19.99). The float form parses 19.99 as a Float first, which passes an imprecise value to Decimal. The string form gives Decimal the digits 19.99 directly, with nothing lost.

Mix numeric types

Integer can combine with both Decimal and Float. Decimal and Float can't directly mix:

FormulaResult
1 + Decimal('1')2
1.5 + Decimal('1')Raises E103

The restriction prevents a Float from silently making a result approximate. Raising E103 forces the approximation to be explicit. Fix E103 by converting the Float operand to a Decimal, not by demoting the Decimal operand to a Float.

SCHEMA INFERENCE DOESN'T PRODUCE DECIMALS

Pasting sample JSON into the Transform data action infers 12.50 as a Float. Set the type manually if fraction precision matters for your use case, such as when working with money.

Text and bytes

The following two types hold text and raw byte data:

String

A String is UTF-8 text, in single or double quotes (' or ").

Measure length by code points or grapheme clusters

The length function counts code points, not user-perceived characters or bytes:

FormulaResult
length('👨‍👩‍👧')5
grapheme_length('👨‍👩‍👧')1

Some emoji and accented letters are made of several code points, even though a reader would see one character. Use grapheme_length to count user-perceived characters (grapheme clusters).

Concatenate strings

Concatenation with ++ requires matching types on both sides:

FormulaResult
'a' ++ 'b'ab
'x' ++ 1Raises E101

Convert the element first with String(...), or use an f-string, which converts as it interpolates:

text
f"Order {_.id} has {_.count} items"

Binary

Binary is raw bytes, written in hexadecimal format. For example: 0x"48656C6C6F".

BINARY STRING CONVERSION

The String() conversion rejects a Binary rather than selecting a representation for it. Use one of the following conversions instead:

Dates and times

WEL has five temporal types. Use the type that fits your use case to avoid common date bugs.

DateTime

A DateTime is an exact instant with a time zone or offset. Use it to record the specific time of an event, such as the time something was created, shipped, or logged. For example: DateTime('2026-03-15T09:30:00Z').

PlainDate

A PlainDate is a calendar date with no time and no zone. Use it for a calendar date that's the same for everyone looking at it, such as an invoice date, a birth date, or a contract start date. For example: PlainDate('2026-03-15').

USE PLAINDATE TO IGNORE TIMEZONES

The same point in time can have different dates across timezones. For example, 2026-03-15T00:00:00Z is the 15th at 9am in Tokyo and the 14th at 5pm in Los Angeles.

A PlainDate has no timezone, so it reads as the same date for everyone. Storing an invoice date as a DateTime instead of a PlainDate is the most common source of off-by-one-day errors.

PlainTime

A PlainTime is a time of day with no date and no zone. Use it for a time of day with no date attached, such as a store's opening time or a daily cutoff. For example: PlainTime('09:30:00').

PlainDateTime

A PlainDateTime is a date and time with no zone. Use it for a wall-clock reading before its time zone is known, such as a local time a source system sends with the zone supplied separately. For example: PlainDateTime('2026-03-15T09:30:00').

Duration

A Duration holds a fixed clock duration, not a calendar unit, written Duration('PT2H30M'). Use it for a length of time, such as an age, an elapsed interval between two events, or a timeout.

Clock durations vs calendar units

A calendar unit such as a day can vary in actual length across a daylight-saving change, so Duration only accepts a fixed clock length such as hours or minutes, not a calendar unit such as days. For example:

FormulaResult
Duration('PT72H')3 days
Duration('P3D')Raises E020

Refer to the Temporal functions documentation for more information.

Collections

WEL has two collection types. A List contains ordered data and a Map contains key-value data.

List

A List is an ordered collection that can contain elements of mixed type. Wrap the list in brackets and separate values with commas. For example: [1, 2, 3].

Refer to the List functions documentation for more information.

Map

A Map is a key-value collection and the WEL type for JSON objects. Keys are always String, and insertion order is preserved. Wrap the map in curly braces and separate pairs with commas. For example: {a: 1, b: 2}.

Refer to the Map functions documentation for more information.

Collections don't become strings

A List or Map never becomes a String automatically:

FormulaResult
String([1, 2])Raises E100

The restriction exists because an accidentally stringified collection, such as a whole array arriving in a destination field as [object Object] or a bracketed blob, is one of the hardest integration bugs to trace. Raising E100 forces conversions to be explicit. Call to_json for JSON, to_wel for a WEL literal, or join_to_string for a delimited list, depending on what the destination expects.

Null and Skip

null and Skip both describe an absence, but their behavior differs in many destination systems:

  • null: The field has no value. The key is still emitted.

  • Skip: Don't emit this at all. The key is left out of a map.

FormulaResult
{a: 1, b: Skip()}{a: 1}

This distinction is especially common in CRM update APIs. Sending null clears a field, while omitting the key with Skip leaves it untouched.

Filter and transform a list with map_by

Returning Skip() from a map_by lambda drops that element from the result, so a single pass can filter and transform together:

FormulaResult
[1, -2, 3] >> map_by(n ~> if n > 0 then n else Skip())[1, 3]

Test for values

present? is the general test, and it treats every kind of absence the same way:

FormulaResult
present?('SO-1001')true
present?(' ')false
present?(null)false
present?([])false
present?(false)true

present?(false) returns true, because it's a real value a pipeline can carry deliberately, not a stand-in for missing data the way null or an empty string is.

presence reports every kind of absence as null so one fallback operator (| ) catches them all, including empty strings, whitespace-only strings, empty lists, and empty maps.

Convert between types

Each type has a constructor, such as Integer(...), Decimal(...), or PlainDate(...). Constructors are strict, for example: Integer('3 items') raises E102 rather than partially parsing.

The most reliable shape for a transformation is to convert every field at the start:

text
{
  order_id: String(_.id),
  qty: Integer(_.qty),
  total: Decimal(_.total),
  ship_date: PlainDate(_.ship_date)
}

Refer to the Conversion functions documentation for more information.

Last updated: