Builtins
Builtins — every function and helper family, one entry each #
Every built-in function, one section each — fetch one by name (builtins#List.sum) instead of the whole chapter. Each entry gives the signature, what it means, whether the result is always there or T | absent, an ordering requirement where one exists, and an example. The reference chapter's operators-and-functions is the one-page version of this list. Entries run in groups: the core words, then each namespace with its members. Core words are always there and never qualified; a namespace member is always written with its namespace (Text.upper(x)). Every argument is checked when you save, and a call is typed by what it gives back (Text.upper(x) is text), so a built-in's call written into a number field is refused. A call can sit inside any expression and runs left to right.
Inside walk brackets -[…]-> a function is spelled as it is outside them, as in ch-[m:Messages WHERE Text.lower(m.Text) CONTAINS "urgent"]->; only the logic words keep their capitals there (AND, OR, NOT). Each section is headed by the name you write (Text.upper, List.sum), which is what you fetch it by.
Core words #
x == null #
x == null → boolean, always present — the presence test inverted (x != null is true when there is a value). Takes a bound name or a field read off one (x.Field== null), and proving a field absent proves nothing about the record it came off — proving it present (!= null) is what narrows the record.
if (`Suggested Action` == null) { fail("nothing to act on") }
exists #
exists(walk) → boolean, always present — whether a relationship has any records. Takes a walk only, with an optional WHERE through an alias (exists(m-[a:Attachments WHERE a.Name CONTAINS "pdf"]->)); on a value it is refused with the != null rewrite. !exists(…) is has-none. if (x != null) { … } narrows x present inside that arm only; x != null ? a : b narrows the same way inside a.
if (exists(msg-[:Attachments]->)) { … }
coalesce #
coalesce(a, b, …) → the first argument with a value, present whenever at least one argument is unconditionally present (a literal, or anything already known to be there). It stops at the first argument that has a value and never evaluates the rest, so a later argument that calls a model, reads a file or fails the run does nothing when an earlier one is present. A coalesce whose arguments might all be empty is itself T | absent and discharges nothing.
Name: coalesce(msg-[:Sender]->.Name, msg-[:Sender]->.Email, "unknown")
count #
count(list | walk) → number, always present, reads any order. The member count; 0 for null.
n = count(co-[t:Team]->)
only #
only(list | walk) → T | absent, reads any order — the one that matched: answers that record or value, and fails the run if there turns out to be more than one. Reach for it whenever a WHERE narrows to a single thing; over a bare walk it binds the whole record.
channel = only(chat-[ch:Channels WHERE ch.Name == "ops"]->)
first #
first(list) → T | absent — needs an ordered sequence: a walk answers a set, in no order, so put an ORDER BY on the hop or sort it. Refused otherwise (the message points at only when the source is really a single-match lookup).
newest = first(crm-[c:Companies ORDER BY c.`Created At` DESC]->.Name)
sort #
sort(list) / sort(list, { order: "desc" }) / sort(list, (r) => r.score) / sort(list, (r) => r.score, { order: "desc" }) → the same members in order, always present (commutative — reads any order in). Plain values sort by themselves; a list of records or dicts takes a key function reading one field. The settings record is last, and "asc" is the default order.
ranked = sort(rows, (r) => r.score, { order: "desc" })
map #
map(list, f) or map(list, f, settings) → a list of what f returned, member by member, in the order given — always present (each slot may itself be absent when f's value is). f is a function (member) => { … } or (member) => expression and takes its parameter's type from the collection; it may await (each member waits on its own). map alone among the five may skip return — each slot is then absent and the closure's writes still run — and a bare map(...) with no binding is a legal statement on its own.
bulleted = map(lines, (t) => { return "• ${t}" })
upper = map(lines, (t) => Text.upper(t))
map(ch-[m:Messages]->, (m) => { write graph-[:note]-> { text: m.Text } })
settings is a record written in place, last: onError ("error", the default, "warn" or "ignore": what a member whose function fails does — the other two leave it out of the answer, "warn" with a warning on the trace), concurrency (members run at once) and initialConcurrency (a first batch of that size runs to the end before the rest start, so a prompt cache is warm before the fan-out). The answer stays in input order, and members writing the same unique by key take turns, so the record is made once. See map-and-filter-settings in the expressions chapter.
filter #
filter(list, f) or filter(list, f, settings) → the members f answered true for, in order, always present. f must give a boolean — return one, or write the condition as the body. settings is the same record map takes; a member whose predicate fails is not kept. A test that keeps only members that are there ((x) => x != null) answers a list of present members.
kept = filter(lines, (t) => { return Text.length(t) > 0 })
reduce #
reduce(list, f, start) → whatever f last returned, always present when start is. Needs an ordered list: a walk answers a set, in no order, so sort it or put an ORDER BY on the hop. f is (carried, member) => { … } and must return the next carried value.
total = reduce(lines, (carried, t) => { return carried + Text.length(t) }, 0)
members #
members(<T>) → a list of a closed type's values, always present, in the order they were declared — a refinement you wrote, or a field's option set borrowed from a system. Refused on a field whose values are merely known (other values are legal there too), since that has no complete membership.
theses = members(<Thesis>)
ai #
ai("…") → a value from a language model, resolving to a real null when nothing applies — gate with != null. Optional settings record last: { tier: "quick" } (default), "careful", "thorough". See ai in the expressions chapter for the full treatment — prompting, tiers, and the null-gating idiom.
Category: ai("one of: bug, feature, question — for: ${msg.Subject}", { tier: "careful" })
Operators and indexing #
conditional #
c ? a : b — the value-level conditional; if (c) { … } branches statements instead. The : b is required, so a conditional always has a type. Only the arm that is taken is evaluated, so a call in the other arm never runs. The condition guards its own a the way a statement if guards its arm, so a presence test on the condition narrows inside a only.
line = domain != null ? "${domain}" : ""
unary minus #
-x → number — arithmetic negation. The operand must already be a number that is there: coerce first if it isn't (-coalesce(Number.parse(x), 0)), and narrow or coalesce one that may be absent. Parenthesise a sub-expression the same as any arithmetic: -(a + b).
delta = -(after - before)
remainder #
a % b → number — same precedence as * and /, left-associative. TypeScript semantics: the sign follows the dividend (-7 % 20 is -7, not 13). Both operands must already be numbers that are there (narrow or coalesce one that may be absent), the same rule every arithmetic operator has; division by zero answers null.
leftover = amount % batch_size
xs[i] #
Indexing, by what you index. xs[n] → T | absent — a number index into a list, needing an ordered sequence exactly as first does (refused otherwise). A list literal is a tuple, so [a, b][0] reads the first slot exactly and is always present. d.k → present when k is one of the dict's own written keys (a typo is refused with a did-you-mean). d[key] → T | absent when the key is computed at run time, or when the dict's per-key shape isn't known (a system value, a List.groupBy/List.keyBy result — its keys are data, not declarations).
top = sort(names)[0]
copy = { intro: "Welcome", body: "…" }
bucket = copy.intro
Text #
Text.trim #
Text.trim(text) → text, and text | absent when text may be absent (null in, null out) — the text with leading and trailing whitespace removed.
name = Text.trim(msg.Subject)
Text.lower #
Text.lower(text) → text, and text | absent when text may be absent (null in, null out) — the text, lowercased.
key = Text.lower(msg.From)
Text.upper #
Text.upper(text) → text, and text | absent when text may be absent (null in, null out) — the text, uppercased.
code = Text.upper(Text.trim(msg.Subject))
Text.includes #
Text.includes(text, part) → boolean — whether the text contains part, ignoring case. The same test as CONTAINS in a WHERE.
urgent = Text.includes(msg.Subject, "urgent")
Text.length #
Text.length(text) → number, always present — the character count of a text (an emoji is one character). 0 for null. A list's length is count(xs).
big = Text.length(msg.Body) > 500
Text.from #
Text.from(value) → text, and text | absent when value may be absent (null in, null out) — the value's text form: a number, date, or boolean prints as its usual text; a record or list is refused (write a field off it instead).
label = Text.from(amount)
Text.split #
Text.split(text, separator?) → a list of text, always present (empty when the text is empty). Each piece is trimmed and empty pieces are dropped; separator defaults to ",".
tags = Text.split(msg.Subject, ";")
Text.concat #
Text.concat(a, b, …) → text, always present — joins its arguments as text. A part that is T | absent is accepted and prints as nothing, the same rule interpolation follows ("${x}" desugars to this same call). A structured value (a dict, a record) is refused — write a field off it first.
line = Text.concat(msg.From, " — ", msg.Subject)
Text.join #
Text.join(list, separator) → text, always present — the separator is text written in place (", "), never a name or a template. Needs an ordered sequence: a walk answers a set, in no order, so put an ORDER BY on the hop or sort it (a relationship the source keeps in order needs neither); refused otherwise naming the fix. Over a bound block it joins what the iterations returned.
roster = Text.join(sort(co-[t:Team]->.Name), ", ")
Text.chunks #
Text.chunks(text, { size | entities, overlap }) → a list of text, always present — cuts a long text into pieces. size bounds each piece by characters; entities instead sizes each piece by the records it is expected to yield, cutting on whole lines; exactly one of the two is written, and overlap (characters repeated from the piece before) works with either. unit: takes only "chars".
pieces = Text.chunks(text, { size: 40000, overlap: 2000 })
Text.regexExtract #
Text.regexExtract(text, pattern, group?) → text | absent, absent when nothing matches — the first regex match, or the first capture group when the pattern has one. The pattern reads whole characters (. matches an emoji) and is absent when malformed: escape only punctuation (\\., \\-) or a known escape (\\d, \\s, \\x41, \\u{1F600}); an octal escape or an unknown letter escape is malformed.
ref = Text.regexExtract(m.Subject, "(DEAL-\\d+)")
Text.slug #
Text.slug(text) → text, and text | absent when text may be absent — a lowercase-hyphen slug.
slug = Text.slug("Acme Corp Ltd.") # "acme-corp-ltd"
Text.pairs #
Text.pairs(record, separator?) → text, and text | absent when record may be absent — a record or a dict rendered as key=value pairs, keys in the order they were written (a dict literal's own order; a declared or extracted record's field order), joined by separator (default " | "). It takes a record from a walk or a name — Text.pairs(e) inside deduped-[e:entries]-> { … }, an extracted node, a first(…) answer — as well as a literal. Each value renders exactly as ${…} interpolation would: absent is empty, a boolean is true/false, a number is Text.from's form. A nested node or edge field is SKIPPED, not stringified — only scalar fields make it into the line. Refused on <json>: it looks keyed but the checker cannot see its keys, so name the fields into a dict first.
Project A's composer posts one Slack line per candidate row:
summary = Text.pairs({ name: c.Name, url: c.Website, stage: c.Stage })
# "name=Acme | url=acme.com | stage=Series A"
line = "candidate — ${Text.pairs({ name: c.Name, url: c.Website }, ", ")}"
# "candidate — name=Acme, url=acme.com"
Json #
Json.stringify #
Json.stringify(value) → text, always present — any value written out as JSON text for a prompt, the same bytes every time it is the same value (so a prompt that opens with it stays cacheable). The JSON has two-space indent and keys sorted at every level, so the order you wrote fields in never shows. A record is its fields, plus one key per edge it carries holding the nodes on that edge as a list (always a list, even for one node). A dict, list or record inside the value is written out the same way. Absent is null (a record's absent field keeps its key; a whole absent value is the text null). Dates and datetimes are ISO text; a file is { contentType, kind: "file", name, size }, never its contents. A record read live from a system is refused (build a dict of the fields you want).
card = Json.stringify({ name: "Acme", stage: c.Stage })
# two lines of "name" and "stage", sorted, indented two spaces, inside braces
Number #
Number.abs #
Number.abs(number) → number, the absolute value.
diff = Number.abs(before - after)
Number.round #
Number.round(number) → number, rounded to the nearest whole number (a half rounds away from zero: 2.5 is 3, -2.5 is -3).
pct = Number.round(score * 100)
Number.floor #
Number.floor(number) → number, rounded down.
pages = Number.floor(Text.length(text) / 3000)
Number.ceil #
Number.ceil(number) → number, rounded up.
batches = Number.ceil(count(rows) / 50)
Number.parse #
Number.parse(value) → number | absent — coerces text to a number, and is absent when the text is no number. Guard it (n != null) or give it a fallback (coalesce, ?:) where a value is required.
n = Number.parse(msg.Subject) ?: 0
crm-[t:Teams WHERE t.`Team Size` <= Number.parse(m.Body)]->
Number.format #
Number.format(number, style) → text | absent — a number written out. style is a literal, checked when you save: "compact" abbreviates with K/M/B/T, one decimal at most and no trailing .0; "grouped" adds thousands separators, keeping a decimal part up to two places.
short = Number.format(1200000, "compact") # "1.2M"
long = Number.format(1200000, "grouped") # "1,200,000"
Date #
Date.from #
Date.from(value) → date when value is a date or datetime that is there, and date | absent when it may be absent or is anything else (text may read as no date) — coerces any readable date or timestamp to a calendar day (midnight UTC). Reach for it when a comparison spans two kinds of value.
due = Date.from(m.Body)
if (due == null) { fail("no date in the body") }
crm-[s:Tasks WHERE s.`Snoozed Until` <= due]->
Date.parse #
Date.parse(text) → date | absent — a written date read as an ISO date; unreadable text (including an ambiguous numeric form like "12/03/2026") is absent rather than a guess.
d = Date.parse("12 March 2026") # "2026-03-12"
Date.addDays #
Date.addDays(date, days) → date when date is a date or datetime that is there, and date | absent when it may be absent or is text (text that reads as no date answers absent) — a date shifted by whole days. It reads the calendar date written at the start of its argument, any time of day and offset dropped ("2026-03-12T00:30+01:00" is the 12th), exactly as Date.daysBetween does.
deadline = Date.addDays(@current_date, 14)
Date.daysBetween #
Date.daysBetween(from, to) → number when both arguments are dates or datetimes that are there, number | absent when either may be absent or is text (text that reads as no date answers absent: Date.parse it first and test it) — the whole calendar days from from to to, negative when to is the earlier. Each argument counts as the calendar date written at its start, any time of day and offset dropped ("2026-03-12T00:30+01:00" is the 12th), exactly as Date.addDays reads its date, so Date.daysBetween(d, Date.addDays(d, n)) is n. Dates can't be subtracted with -; this is the difference.
days = Date.daysBetween(Date.today("Europe/London"), event) # 14 when event is two weeks away
urgent = days < 14
Date.format #
Date.format(value, pattern) → text when value is a date or datetime that is there, and text | absent when it may be absent or is text — a date written out for a person to read, from the tokens YYYY YY MMMM MMM MM M DD D dddd ddd HH H mm m ss s; anything that is not a letter prints as written. The pattern is a literal, checked when you save.
line = Date.format(@current_date, "MMMM D, YYYY") # "August 31, 2026"
Date.today #
Date.today(zone) → date, always present — the calendar date it is right now in a place, reading the run's pinned instant (not a live clock: every read in one run gives the same day). zone is a literal IANA name, checked when you save.
today = Date.today("Europe/Berlin")
DateTime #
DateTime.from #
DateTime.from(value) → datetime when value is a date or datetime that is there, and datetime | absent when it may be absent or is anything else — coerces to a full UTC instant.
sent = DateTime.from(m.Date)
DateTime.at #
DateTime.at(date, time, zone) → datetime, and datetime | absent when date may be absent (text that reads as no date fails the run) — the instant a wall-clock time on a date names in a place. time and zone are literals, checked when you save. Move days with Date.addDays and anchor each end of a window separately — a daylight-saving change then takes care of itself.
win_end = DateTime.at(today, "07:00", "Europe/Berlin")
DateTime.toIso #
DateTime.toIso(value) → absent when value may be absent or is text that reads as no date, otherwise any readable date or timestamp normalised to a full ISO 8601 UTC timestamp. A text with a time of day is read as ISO 8601 / RFC 3339 only (2026-03-05T10:00, 2026-03-05 10:00:30+01:00); with no offset it is UTC.
iso = DateTime.toIso(m.Date)
Currency #
Currency.numberFromFigure #
Currency.numberFromFigure(figure) → number | absent, absent when the text holds no figure — the numeric amount in a money figure.
amount = Currency.numberFromFigure("£1.2m") # 1200000
Currency.codeFromFigure #
Currency.codeFromFigure(figure) → text | absent, absent when the text names no currency — the ISO currency code in a money figure.
code = Currency.codeFromFigure("£1.2m") # "GBP"
Currency.formatFigure #
Currency.formatFigure(number, code) → text | absent — a number as a money figure, the inverse of Currency.numberFromFigure: a symbol where the code has one, the ISO code after a space where it doesn't, the bare number for an empty or unrecognised code. code may be any expression, not only a literal.
line = Currency.formatFigure(1200000, "EUR") # "€1.2M"
chf = Currency.formatFigure(1200000, "CHF") # "1.2M CHF"
Url #
Url.host #
Url.host(text) → text | absent — the host, lowercased and otherwise verbatim (www. kept, no port, no path). Takes a full URL (https://acme.com) or a scheme-less address (acme.com, acme.com:8080, www.Acme.com/path) — the bare form is read as if http:// were in front of it. Absent for an email (joe@acme.com names an account, not a host) and for text that names no address at all.
host = Url.host("https://WWW.Acme.com:8080/deals/1") # "www.acme.com"
host2 = Url.host("acme.com") # "acme.com" — no scheme needed
List #
List.sum #
List.sum(list of number) → number, always present, reads any order — the total. 0 over an empty list. A list that may hold an absent member is refused: List.compact it first, or filter(xs, (x) => x != null).
total = List.sum(co-[o:Orders]->.Amount)
List.includes #
List.includes(list, value) → boolean — whether the list holds value. The same test as IN in a WHERE.
known = List.includes(["won", "lost"], deal.Stage)
List.avg #
List.avg(list of number) → number, always present, reads any order — the mean.
average = List.avg(co-[o:Orders]->.Amount)
List.min #
List.min(list) → T | absent, reads any order — the smallest member; absent over an empty list. The element must be orderable (a number, date, datetime, or text).
earliest = List.min(co-[o:Orders]->.`Placed At`)
List.max #
List.max(list) → T | absent, reads any order — the largest member; absent over an empty list.
biggest = List.max(co-[o:Orders]->.Amount)
List.last #
List.last(list) → T | absent — the same ordering requirement as first, reading the other end.
oldest = List.last(sort(co-[o:Orders]->.`Placed At`))
List.from #
List.from(list | walk) → a list, always present (empty rather than absent), reads any order but keeps the source's order when it has one. Materialises a walk into an ordinary list value — the form to reach for when a later fold (Text.join, first, xs[n]) needs one and the walk itself has none to give.
names = List.from(co-[p:People]->.Name)
List.compact #
List.compact(a, b, c, …) → a flat list of the arguments with nulls dropped, always present. A list argument flattens one level in, so List.compact is the way to combine several possibly-empty values (or lists of them) into one list.
Tags: List.compact("inbound", region, [`Sub Tag`])
List.groupBy #
List.groupBy(list, key) → a dict of lists, always present — files each member under the key its function answers. The values carry the members' own shape (a dict literal's fields read straight off a group); the keys are data, so a lookup with by[key] is always T | absent even against a literal string. Dict.keys(by) lists the keys and Dict.entries(by) each key with its group, in no order: sort them for a line per key (patterns#group-and-count).
by = List.groupBy(rows, (r) => { return r.thesis })
List.keyBy #
List.keyBy(list, key) → a dict of members, one per key, always present — a repeated key fails the run, naming it. Same shape-carrying and key-lookup rules as List.groupBy.
byId = List.keyBy(co-[p:People]->, (p) => { return p.Email })
List.distinct #
List.distinct(list) → a list, always present — each member once, two members the same when == says so. The members are plain values (text, numbers, dates): a walk already reaches each record once. Each is kept where it first appears, so an ordered list stays in order; a walk's members, which come in no order, stay in none, and need a sort before first, Text.join or reduce.
owners = sort(List.distinct(map(open, (t) => coalesce(t.Assignee, "Unassigned"))))
Dict #
Dict.keys #
Dict.keys(dict) → a list of text, always present — each key once, in no order (a dict holds its keys as a set, as a walk answers its records). sort it before first, Text.join or reduce; each is refused on it unsorted.
by = List.groupBy(open, (t) => coalesce(t.Assignee, "Unassigned"))
lines = map(sort(Dict.keys(by)), (k) => "${k}: ${count(coalesce(by[k], []))}")
Dict.values #
Dict.values(dict) → a list of the dict's values, always present, in no order — for a List.groupBy dict, a list of its groups.
largest = List.max(map(Dict.values(by), (g) => count(g)))
Dict.entries #
Dict.entries(dict) → a list of { key, value }, always present, in no order — each key with its value, read as e.key and e.value (e.value always present). sort by the key for an order.
lines = map(sort(Dict.entries(by), (e) => e.key), (e) => "${e.key}: ${count(e.value)}")
File #
File.from #
File.from(content, "text") → a file value, always present — turns a composed string into a text file for a file-typed field. The content is kept as written, line breaks preserved.
File: File.from(digest, "text")
pdf.render #
A PDF is drawn by the @lf/pdf plugin: pdf.render(content) → a file value, the PDF, always present. The content is drawn as plain text, line breaks preserved. { pageSize: "a4" | "letter", margin, name } is the settings record, last: margin in points from 0 to 200, name the file's name (.pdf is added when missing; the default is movement-artifact.pdf). A character the default writer has no glyph for (anything outside Latin-1 and the common punctuation) is drawn as ? and the run is warned, naming @lf/pdf.
import { `@lf/pdf` as pdf } from plugins
File: pdf.render(digest)
File: pdf.render(digest, { pageSize: "letter", name: "Q3 report" })
web.render #
web.render(content) → a file value, the PDF, always present: the @lf/web plugin draws text or HTML with the host's browser, so HTML keeps its layout. It takes the same settings record as pdf.render ({ pageSize, margin, name }) and is refused on a host with no browser.
import { `@lf/web` as web } from plugins
File: web.render(emailBody, { name: "Message" })
File.read #
File.read(file) → text | absent — the text in a file. An extraction reads a file in its content itself; reach for File.read when you want the text in hand to cut, measure, or pass to a plugin.
text = File.read(f.File)