WEL cookbook
This page contains 26 worked examples of common integration problems, grouped by theme. Each example is self-contained, so you can paste it into the wel binary and run it as-is.
FEATURE AVAILABILITY
WEL is currently available to select customers. Contact your Customer Success Representative to confirm whether it is available in your workspace.
Reshape and extract
The following solutions reshape payloads and extract values from nested structures:
Normalize contact names from a CRM
A CRM sends names with inconsistent spacing and casing. Clean each field, then build a display name from the cleaned values.
Formula
let raw = {first: ' dana ', last: ' SMITH '}
do {
first: raw.first >> trim >> capitalize,
last: raw.last >> trim >> capitalize,
full: trim(raw.first) ++ ' ' ++ trim(raw.last) >> titleize
}Output
{"first": "Dana", "last": "Smith", "full": "Dana Smith"}trim clears the stray whitespace, then capitalize and titleize fix the casing, so all three fields come out clean regardless of how the source formatted them.
Extract fields safely, with fallbacks
Two optional fields are missing. Supply a default for each rather than passing null through to the destination.
Formula
let contact = {name: 'Kai', email: null, phone: null}
do {
name: contact.name,
email: contact.email | '[email protected]',
phone: contact.phone | 'N/A'
}Output
{"name": "Kai", "email": "[email protected]", "phone": "N/A"}The fallback operator (|) only replaces a null value, so name remains unchanged while the two missing fields get their defaults.
Extract identifiers from a deeply nested response
An API response contains the same field name at several levels. Extract every occurrence without specifying each path.
Formula
let api_response = {
data: {
users: [
{profile: {Name: 'Dana'}, orders: [{Name: 'Order-1'}]},
{profile: {Name: 'Kai'}, orders: [{Name: 'Order-2'}, {Name: 'Order-3'}]}
]
}
}
do deep_collect(api_response, 'Name')Output
["Dana", "Order-1", "Kai", "Order-2", "Order-3"]deep_collect walks every level of the structure, so it finds a Name field whether it belongs to a profile or an order, without naming either path.
Transform values inside a deeply nested structure
A document carries file attachments at an unpredictable depth. Encode every binary value in place and leave everything else untouched.
Formula
let doc = {
title: 'Report',
attachments: [
{name: 'logo', data: Binary('PNG')},
{name: 'csv', data: Binary('CSV')}
]
}
do deep_map_values_by(
doc,
v ~> encode_base64(v),
v ~> type_of(v) == 'Binary'
)Output
{"title": "Report", "attachments": [{"name": "logo", "data": "UE5H"}, {"name": "csv", "data": "Q1NW"}]}The condition lambda is what makes this safe. deep_map_values_by only applies the transform to values where type_of(v) == 'Binary', so title and name pass through as they are.
Lists and aggregation
The following solutions combine, reduce, or reorganize a list of records into the shape a destination expects:
Flatten nested order line items
Orders arrive with their line items nested underneath them. Produce one flat row per line item, carrying the parent order ID along.
Formula
let orders = [
{id: 'A1', items: [{sku: 'X', qty: 2}, {sku: 'Y', qty: 1}]},
{id: 'A2', items: [{sku: 'Z', qty: 5}]}
]
do orders
>> map_by(o ~> o.items >> map_by(i ~> {order_id: o.id, sku: i.sku, qty: i.qty}))
>> flattenOutput
[
{"order_id": "A1", "sku": "X", "qty": 2},
{"order_id": "A1", "sku": "Y", "qty": 1},
{"order_id": "A2", "sku": "Z", "qty": 5}
]The inner map_by produces one list of line items for each order. flatten combines those lists into one flat list.
Group line items by SKU across orders
The same SKU appears across several orders. Total the quantity for each one.
Formula
let lines = [
{sku: 'X', qty: 2}, {sku: 'Y', qty: 1},
{sku: 'X', qty: 3}, {sku: 'Y', qty: 4}
]
do lines
>> group_by(l ~> l.sku)
>> entries
>> map_by(e ~> {sku: e.key, total: e.value >> map_by(row ~> row.qty) >> sum})Output
[{"sku": "X", "total": 5}, {"sku": "Y", "total": 5}]group_by produces a Map keyed by SKU. entries converts the map to a list, and sum calculates the total quantity for each group.
Deduplicate records, keeping the most recent
The same record ID appears multiple times with different timestamps. Keep only the most recent version of each record.
Formula
let events = [
{id: 'a', ts: 1, status: 'pending'},
{id: 'b', ts: 2, status: 'active'},
{id: 'a', ts: 3, status: 'active'}
]
do events
>> group_by(e ~> e.id)
>> values
>> map_by(group ~> group >> sort_by(e ~> e.ts) >> last)Output
[{"id": "a", "ts": 3, "status": "active"}, {"id": "b", "ts": 2, "status": "active"}]A group-by on ID collects every version of a record together. Within each group, a sort by timestamp followed by last then keeps only the most recent version.
Pivot rows into columns
A metrics API returns one row per metric. Reshape it into a single record with one field per metric.
Formula
let rows = [
{metric: 'cpu', value: 72},
{metric: 'mem', value: 85},
{metric: 'disk', value: 40}
]
do rows
>> map_by(r ~> {key: r.metric, value: r.value})
>> from_entriesOutput
{"cpu": 72, "mem": 85, "disk": 40}from_entries is the inverse of entries: it takes a list of {key, value} pairs and rebuilds them into a single Map. This operation turns rows into columns.
Partition records into success and failure buckets
A batch job needs to route successes and failures differently. Split a mixed list of results into the two groups.
Formula
let results = [
{id: 1, status: 'ok'}, {id: 2, status: 'error'},
{id: 3, status: 'ok'}, {id: 4, status: 'error'}
]
do let parts = partition_by(results, r ~> r.status == 'ok')
do {
succeeded: parts[0] >> map_by(r ~> r.id),
failed: parts[1] >> map_by(r ~> r.id)
}Output
{"succeeded": [1, 3], "failed": [2, 4]}partition_by returns a two-element list. The first element contains matching records, and the second contains the remaining records. In this example, parts[0] contains successes and parts[1] contains failures.
Join data sources
The following solutions combine records from two separate lists by a shared key:
Join two lists on a shared key
Two lists share a key. Return only records with a match in both lists.
Formula
let users = [{id: 1, name: 'Dana'}, {id: 2, name: 'Kai'}],
orders = [{user_id: 1, item: 'Book'}, {user_id: 1, item: 'Pen'}, {user_id: 3, item: 'Tape'}]
do users
>> map_by(u ~> orders
>> filter_by(o ~> o.user_id == u.id)
>> map_by(o ~> {name: u.name, item: o.item}))
>> flattenOutput
[{"name": "Dana", "item": "Book"}, {"name": "Dana", "item": "Pen"}]filter_by keeps only the orders matching each user's ID. Users without matching orders produce empty lists, which flatten removes from the result. This behavior creates an inner join.
Keep every record in a left join, even without a match
Every record on the left side of a join must appear in the result, matched or not, unlike an inner join.
Formula
let users = [{id: 1, name: 'Dana'}, {id: 2, name: 'Kai'}],
orders = [{user_id: 1, item: 'Book'}]
do users
>> map_by(u ~>
let matches = orders >> filter_by(o ~> o.user_id == u.id)
do if length(matches) > 0
then matches >> map_by(o ~> {name: u.name, item: o.item})
else [{name: u.name, item: null}])
>> flattenOutput
[{"name": "Dana", "item": "Book"}, {"name": "Kai", "item": null}]The else branch supplies a single-element list with item: null instead of an empty one when no order matches a user, so that user still contributes a row after flatten.
Merge and reconcile two data sources
The same entity exists in two systems, sometimes only in one of them. Combine both into a single record per ID, noting which system each one came from.
Formula
let crm = [{id: '1', name: 'Dana', source: 'crm'}, {id: '2', name: 'Kai', source: 'crm'}],
erp = [{id: '2', name: 'Lucian', source: 'erp'}, {id: '3', name: 'Sasha', source: 'erp'}]
do let all_ids = (crm ++ erp) >> map_by(r ~> r.id) >> unique
do all_ids >> map_by(id ~>
let from_crm = crm >> find_by(r ~> r.id == id),
from_erp = erp >> find_by(r ~> r.id == id)
do {
id: id,
name: if from_crm != null then from_crm.name else from_erp.name,
in_crm: from_crm != null,
in_erp: from_erp != null
})Output
[
{"id": "1", "name": "Dana", "in_crm": true, "in_erp": false},
{"id": "2", "name": "Kai", "in_crm": true, "in_erp": true},
{"id": "3", "name": "Sasha", "in_crm": false, "in_erp": true}
]This collects every ID from both lists first, before looking either system up, so a record from only one source still appears in the result.
Numbers and money
The following solutions keep a number exact through arithmetic and parsing, where approximation would be a real bug:
Convert currency without losing precision
An invoice amount needs converting to another currency. Round the result to cents for display, without losing the exact source amount.
Formula
let invoice = {amount: Decimal('1234.56'), rate: Decimal('0.85')}
do let converted = invoice.amount * invoice.rate
do {
original: invoice.amount,
eur: round_places(converted, 2),
label: String(round_places(converted, 2)) ++ ' EUR'
}Output
{"original": 1234.56, "eur": 1049.38, "label": "1049.38 EUR"}Both amount and rate are Decimal from the start, so the multiplication is exact and round_places is the only place any precision is lost, and only for display.
Parse large integers and decimals from JSON without losing precision
Most languages silently lose precision parsing large integers or decimals from JSON. A JavaScript JSON.parse, for example, truncates 9999999999999999999 to 10000000000000000000.
Formula
let json = '{"tx_id": 9999999999999999999, "balance": 1234567890.1234567890}'
do let data = parse_json(json, {decimal: true})
do {
tx_id: data.tx_id,
balance: data.balance,
balance_type: type_of(data.balance),
cents: data.balance * 100
}Output
{"tx_id": 9999999999999999999, "balance": 1234567890.1234567890, "balance_type": "Decimal", "cents": 123456789012.3456789000}Integer has no upper bound, so tx_id survives intact. The decimal: true option is what keeps balance as an exact Decimal instead of an approximate Float. Without it, fractional numbers parse using standard Integer/Float rules.
Dates
The following solution derives several routing facts from a single date at once:
Route a record by its date
A record requires routing based on whether its date falls on a weekend, the number of days until month end, and its quarter.
Formula
let event_date = PlainDate('2025-03-15')
do let dow = day_of_week(event_date)
do let month_end = end_of_month(event_date)
do {
is_weekend: dow == 6 or dow == 7,
days_until_month_end: month_end - event_date,
quarter: ceil(month(event_date) / 3.0)
}Output
{"is_weekend": true, "days_until_month_end": 16, "quarter": 1}A PlainDate subtraction gives a day count directly, so days_until_month_end needs no separate duration handling. Refer to day_of_week for what the numbering depends on.
Validation and errors
The following solutions reject or route invalid data before it reaches downstream steps:
Validate an incoming webhook payload
A webhook delivers its body as a JSON string. Parse it, then reject the payload immediately if the amount it carries isn't usable.
Formula
let payload = '{"amount": "42.50", "currency": "USD"}'
do let data = parse_json(payload)
do let amount = Decimal(data.amount)
guard amount > 0 else 'amount must be positive'
do {amount: amount, currency: data.currency}Output
{"amount": 42.50, "currency": "USD"}The guard stops a zero, negative, or unparseable amount from reaching the rest of the transformation, rather than letting a bad value flow through silently.
Filter and summarize API error responses
A batch of API responses mixes successes and failures. Report how many of each, with the failure details.
Formula
let responses = [
{code: 200, body: 'ok'},
{code: 422, body: 'validation failed'},
{code: 200, body: 'ok'},
{code: 500, body: 'internal error'}
]
do let failures = responses >> filter_by(r ~> r.code >= 400)
do {
total: length(responses),
failed: length(failures),
errors: failures >> map_by(r ~> String(r.code) ++ ': ' ++ r.body)
}Output
{"total": 4, "failed": 2, "errors": ["422: validation failed", "500: internal error"]}The let binding for failures filters the failed responses a single time, reusing the result for both the count and the error list, rather than filtering twice.
Clean and validate a batch of email addresses
A batch of email addresses arrives with inconsistent casing, stray whitespace, blanks, and at least one malformed entry. Keep only the ones that are actually usable.
Formula
let raw = ['[email protected]', 'bad-email', ' [email protected] ', '', '[email protected]']
do raw
>> map_by(e ~> trim(e) >> lower)
>> filter_by(e ~> not blank?(e))
>> filter_by(e ~> valid_email?(e))Output
["[email protected]", "[email protected]", "[email protected]"]The normalization step runs before validation, so it treats [email protected] and [email protected] as the same address, and valid_email? removes bad-email, which was never a valid address.
Guard a multi-step calculation against bad input
A tax calculation depends on a rate that arrives as a string and must fall within a sane range before anything downstream trusts it.
Formula
let payload = {items: [{price: 10, qty: 2}, {price: 25, qty: 1}], tax_rate: '0.08'}
do let tax = Decimal(payload.tax_rate)
guard tax >= 0 and tax < 1 else 'invalid tax rate'
do let subtotal = payload.items >> map_by(i ~> i.price * i.qty) >> sum
do {
subtotal: subtotal,
tax: round_places(Decimal(subtotal) * tax, 2),
total: round_places(Decimal(subtotal) * (1 + tax), 2)
}Output
{"subtotal": 45, "tax": 3.60, "total": 48.60}The guard runs immediately after converting tax, before any of the arithmetic that depends on it, so an out-of-range rate fails the job instead of producing a silently wrong total.
Process a batch without failing the whole job on one bad record
A batch of values needs converting to Integer, but some of them aren't valid numbers. One bad record shouldn't fail the rest of the batch.
Formula
let inputs = ['42', 'bad', '100', '', '7.5']
do inputs >> map_by(v ~> {
raw: v,
parsed: Integer(v) |? null,
ok: (Integer(v) |? null) != null
})Output
[
{"raw": "42", "parsed": 42, "ok": true},
{"raw": "bad", "parsed": null, "ok": false},
{"raw": "100", "parsed": 100, "ok": true},
{"raw": "", "parsed": null, "ok": false},
{"raw": "7.5", "parsed": null, "ok": false}
]The try-fallback operator |? catches the conversion error per element, so one unparseable value produces a null for that record instead of stopping the whole batch.
Generate output formats
The following solutions safely build a string in a format another system expects:
Build a SOQL query without breaking on special characters
A search term entered by a user can contain characters that would otherwise break out of the query's string literal.
Formula
let search = "O'Brien & Sons"
do "SELECT Id, Name FROM Account WHERE Name = '" ++ escape_for_soql(search) ++ "'"Output
SELECT Id, Name FROM Account WHERE Name = 'O\'Brien & Sons'escape_for_soql escapes only the search value and preserves the surrounding query syntax. The escaped apostrophe can't terminate the string literal.
Build CSV rows from structured data
A destination expects a CSV file, and some field values contain commas. Escape these values so the commas don't act as column separators.
Formula
let records = [
{name: 'Dana', email: '[email protected]', amount: 100},
{name: 'Kai, Jr.', email: '[email protected]', amount: 250}
]
do let header = 'Name,Email,Amount'
do let rows = records
>> map_by(r ~> escape_for_csv(r.name) ++ ',' ++ r.email ++ ',' ++ String(r.amount))
do [header] ++ rows >> join_to_string('\n')Output
Name,Email,Amount
Dana,[email protected],100
"Kai, Jr.",[email protected],250escape_for_csv quotes only the field containing a comma, "Kai, Jr.", so it survives as one column instead of splitting into two.
Issue a JWT for API authentication
An outbound API call needs a signed, time-limited token proving who issued it.
Formula
let issued_at = now() >> to_epoch >> floor,
key = '0123456789abcdef0123456789abcdef'
do let payload = {sub: 'service-account', iat: issued_at, exp: issued_at + 3600}
do let token = jwt_encode(payload, key, 'HS256')
do {
authorization: 'Bearer ' ++ token,
decoded: jwt_decode(token, key, 'HS256')
}Output
{
authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
decoded: {header: {alg: "HS256", typ: "JWT"}, payload: {sub: "service-account", iat: ..., exp: ...}}
}Use at least 32 random bytes from a secure secret store for the key in production. Refer to jwt_encode for why HS256 enforces that minimum.
Generate a numbered list of items
A list of items needs rendering as numbered, uppercased lines for a plain-text report.
Formula
let items = ['alpha', 'beta', 'gamma', 'delta']
do items
>> map_with_index_by((item, i) ~> String(i + 1) ++ '. ' ++ upper(item))
>> join_to_string('\n')Output
1. ALPHA
2. BETA
3. GAMMA
4. DELTAmap_with_index_by supplies the zero-based position alongside each element, deriving the line number instead of tracking it separately.
Structure expressions
The following solutions use WEL's own constructs, Skip and fun, to keep an expression clean:
Map fields conditionally, omitting some with Skip
An outbound record should omit a field entirely when it's blank, and never forward an internal field at all.
Formula
let src = {name: 'Dana', nickname: '', age: 30, internal_id: 'x-123'}
do {
name: src.name,
nickname: if not blank?(src.nickname) then src.nickname else Skip(),
age: src.age,
internal_id: Skip()
}Output
{"name": "Dana", "age": 30}Skip removes a key from the result entirely, which is why nickname and internal_id are both absent rather than present as null.
Reuse logic with a named function
The same masking rule applies to more than one field. Name it once instead of writing the same lambda out repeatedly.
Formula
fun mask = s ~>
if length(s) > 4
then substring(s, 0, 2) ++ '***' ++ substring(s, length(s) - 2, 2)
else '****'
do let records = [{name: 'Dana', ssn: '123-45-6789'}, {name: 'Kai', ssn: '987-65-4321'}]
do records >> map_by(r ~> {name: r.name, ssn: mask(r.ssn)})Output
[{"name": "Dana", "ssn": "12***89"}, {"name": "Kai", "ssn": "98***21"}]fun binds the masking rule to a name once, so both records apply the same logic instead of writing it twice.
Related
- Quickstart: An example showing different WEL workflows.
- Standard library: More information about every function used on this page.
- Data types: The data types these solutions convert between.
- Error codes: Troubleshoot a failing expression.
Last updated: