Reference — the whole surface on one page (forms, operators, modifiers, pitfalls)

Reference — the whole surface on one page #

A terse lookup, not a lesson. The teaching chapters explain why; this one is for refreshing a single rule mid-author — the exact spelling of an operator, the write modifiers, the unique by forms, a traversal head. Learn the language from the other chapters; come back here to check a detail. For one function's full entry — signature, presence, ordering, an example — the builtins chapter has a section per function (builtins#List.sum).

top-level #

import { `@lf/email` as email, `@lf/attio` as attio } from adapters         # adapter types
import { acme_main } from credentials          # stored connections, by saved name
import { fetchUrl } from plugins              # functions the platform ships, called by position, settings last
import { `Log Lead`, Lead } from "lib/intake"  # exported declarations, by file path
import { `Files Out` as send } from "lib/out"  # rename on import with: as

inbox = connect email                                # no credential
crm   = connect attio using acme_main          # with a credential
crm   = connect attio using acme_main { dryRun: true }   # dryRun is universal

prompt = "the company this email is about"     # file-scope binding

function `Intake`(m: <inbox-[:Email]->>) { … }  # declaration; one typed parameter — the email ITSELF (read `m.Subject` straight off it)
export function `Log Lead`(l: <Lead>) { … }    # shared across files
export node Lead { Name: <text> }              # a named structure; nest `node <edge> { … }` for related records
node Lead: "each lead" { Name: <text> "their name" }   # described: `extract(content, Lead)` uses its fields and words
node Contact extends Lead { Email: <text> }    # Lead's fields and nested nodes, then its own; redefining one is refused
type Thesis = <"Consumer" | "Infra">           # a written set of values: an annotation, and an extract constraint

listen to inbox { key: "intake" } fire intake  # the one way an automation runs
`Log Lead`(graph<Lead> { Name: msg.Subject })  # call a function; arguments are positional
doc = `Email To Doc`(msg)                      # a call's value: what the callee returned
function `Label`(name: <text>, tags: <text[]>, cfg: <{ prefix: text, suffix?: text }>) { … }   # value parameters; convention: data, then a function, then a settings record last
msg = graph<Message> { Body: m.Body, Files: m-[a:Attachments]-> { Name: a.Name } }   # a record built in memory, checked against a declared shape
return { title: msg.Subject, n: 2 }            # hand a value out of a body — several results in one dict or graph
f = (day: <date>) => { … }                     # a closure: a body run later, holding what was in scope here
inc = (v: <number>) => v + 1                   # an expression body; a closure bound to a name is called like any function: inc(2)

= binds a name (instance, handle, extract graph, a bound block, a call's value, a closure, a plain value). Statements run top-to-bottom; a handle must be written before it is read. A function may call itself, declaring what it returns (f(n: <number>): <number>). A listened automation takes exactly one parameter; a library automation takes any number.

return is the one way a value leaves a body — an automation's, a block's, or a closure's — and a return inside an if arm belongs to the body around it. A body with no return hands nothing back, so it runs as a statement and cannot be bound. A closure is a value but not data: keep it in a variable, hand it to callback, until, race or all, or pass it to a parameter whose type is a closure type (<(c: <Company>) => <text>>) — never in a list, under a key or in a field.

types-and-brackets #

Types always wear angle brackets; positions, scalar values, and handles never do.

naming-and-backticks #

operators-and-functions #

meta-fields #

@-prefixed values from the run itself; read like any other value. Eight, in three groups:

A meta-field the run can't resolve is simply empty.

writes #

company = write crm-[:Companies]-> {
  Name:      msg.Subject      # plain — overwrites every run
  Owner   ?: @user_email      # set-if-empty — leaves an existing value alone
  Tags    +: ["inbound"]      # append to a multi-value field (duplicates allowed)
  Sources +?:["email"]        # append only what's missing (set-union)
  ?...e                       # every field of e (extracted, <Decl>-typed, or a graph), set-if-empty; `...e`: plain
  unique by (Domain)          # identity — repeats update instead of duplicate
}

unique by forms:

Connecting records (write related records along their edges, never as flat rows):

A reference field set by assigning a handle (Partner: pa) is rejected — use the structural forms above. Required fields must all be set on a create.

traversal-and-query #

Repetition is a position-valued path followed by a block — there is no loop keyword.

msg-[a:Attachments]-> { … }                       # block; a names this item's position
msg-[f:Attachments WHERE f.type == "application/pdf"]-> { … }   # hop filter
msg-[:Sender]->.Name                              # terminal read off a hop
alias-[:edge]->-[:edge]->                          # chained hops
crm-[c:Companies WHERE c.Stage == "Open" ORDER BY c.`Created At` DESC LIMIT 10]-> { … }   # query

branching #

if (Text.includes(msg.Subject, "order")) { … } else if (rec is <crm-[:People]->>) { … } else { … }
fail("unexpected record kind")                   # abort the whole run with a reason

== compares (not =). An else already means "not the if" — don't re-test it. A plain if with no else already does nothing; keep fail(…) for genuine failures.

presence #

n = only(company-[t:Notes WHERE t.Title == "Brief"]->)   # T | absent — a lookup that might match nothing
d = first(company.Domains)                 # T | absent — same, over a field
if (n == null) { fail("no notes found") } # guard clause — n is present for every line after
if (n != null) { … }                       # narrows n present INSIDE this arm only
x = d != null ? "${d}" : ""                # narrows d present in the true arm only
Field ?: d                                 # '?:' takes a maybe-absent value, no guard needed

first/List.last/List.min/List.max/xs[n] over anything that can come up empty, and a named match that found nothing, type T | absent, same as an await first(…) on a landing a cancel can settle empty. Using such a value where a present one is required — a write or traversal target, a dot-plane field read, an ordered comparison (< <= > >=) — is refused until narrowed, naming the fix. ==/!= against null is the presence test (loose: null and absent mean the same thing); against any other value a maybe-absent side needs no guard (absent is never equal to a present value), and a condition (if, ? :, &&/||/!) reads a missing boolean as false. Both sides must still match in kind. exists(…) is only for related records (exists(msg-[:Attachments]->)); for a value, x != null and x == null take a bound name or a field read off one (x.Field != null), and a proven field proves the record it came off.

listeners #

listen to inbox { key: "intake" } fire intake
listen as "Nightly digest" to timer { schedule: "0 9 * * 1" } fire `Weekly Digest`   # 5-field cron, UTC
listen to crm { events: ["record.created"] } fire `On New`
listen to crm { events: ["record.created", "record.updated"] } fire mirror
listen to go {} fire `On Run`                      # manual / on-demand (go = connect manual)

Connect the instance first, then listen to it. The as alias goes right after listen, before to. A manual run is the submit-text/files channel: read go.Text and traverse go-[:Files]->.File (both optional). Pausing = commenting the listen line out. There is no run statement.

reviews #

Connect with connect ask, write a question along a family edge (the record mints a readable Url), deliver the Url, then await its Response. Families: Check (approve/decline → boolean), Provide (supply a value — set Answer Type), Choose (one of Options — buttons), Select (a subset of Options — checklist), Review (acknowledge and move on), Form (collect named values — literal Fields type the answer). Every family takes Prompt, optional Detail.

asks = connect ask
q = write asks-[:Check]-> { Prompt: "Pursue this?" }
ops = only(team-[ch:Channels WHERE ch.Name == "ops"]->)
if (ops == null) { fail("no #ops channel") }
write ops-[:Messages]-> { Message: "Decide: ${q.Url}" }    # deliver the link
answer = await first(q-[:Response]->)                       # park until answered
if (answer.Answer) { … }

# Timeout / escalation are COMPOSED from race + sleep (no fallback clause):
r = race(
  () => { a = await first(q-[:Response]->)
          return a.Answer },
  () => { await sleep(2d) },          # the default when nobody answers in time
)
if (r[0] == null) { … }             # nobody answered — the safe default path
if (r[0] == true) { … }             # approved

r2 = all(f, () => { … }) # all at once, every slot filled

The answer is readable only after await first(q-[:Response]->). Durations: 30s 90m 4h 1d 1h30m. Interpolate ${q.Url} into the delivery message or nobody can answer. Response is an ordinary writable edge, so anything that can write can answer it — including an in-chat button: mint a callback per answer and carry its id on the control (which field on a control holds it is that system's own, and its chapter names it); see in-chat-answers in the reviews chapter for the full shape. An await on its own waits forever — race it against sleep for an unattended run.

await marks the wait itself — await first(q-[:Response]->), await sleep(2d) — and race(…) only says how several waits combine (the first one), so it is written without await: its arms wait, and the run parks where they do. An arm is a function, passed as an argument — () => { … } in place, or the name of one declared elsewhere — taking no parameters and capturing everything in scope where it is written. The value is a receipt of one slot per arm in the order written, read as r[<index>] and holding what that arm returned: only the arm that settled first has its slot filled, so a null test on a slot is the branch (arms settling in the same moment all land, and arms still parked are dropped where they stand — what they had already done stays done). To wait for every one, use all: r = all(f, g) runs them at once, needs no await (an arm that waits parks the run), and r[i] is what function i returned, every slot filled. all(f, g, { concurrency: 1, onError: "warn" }) takes map's settings: how many run at once, and whether a failing arm leaves its slot null instead of failing the run. A function that hands nothing back has an always-null slot either way.

cb = callback({ … })                      # id + url; one use, dies with the run
cb = callback({ … }, { once: false, ttl: 2d })          # repeatable / time-bounded
cb = callback((day: <date>) => { … })     # a value supplied when someone acts
cb = callback(() => `Chase`(company))         # defer a call
await first(cb-[:Called]->)               # wait for it (landing: At + one field per parameter)

await until(() => { refresh co; return co.Stage == "Won" }, { every: 1h })   # re-check a condition on a cadence

cb.id goes in a control's payload; cb.url is the link form (its page confirms before it acts). until re-checks its condition every every: (1m floor) and resumes when it holds; the condition is a closure or a plain boolean expression, reads only, and takes no parameters. refresh <handle> re-reads a written record so the next check sees it as it is now. Neither a callback's body nor an until condition may wait, directly or through a function it calls: a run cannot be resumed there (MOV_WAIT_NOT_RESUMABLE). Do the waiting in the automation itself, outside both.

extraction #

companies = extract([msg.Body, ...msg-[a:Attachments]->.File], Company, { tier: "careful" })   # a list of Company records
details   = extractOne([...content, Json.stringify(c)], CompanyDetail, { tier: "careful" })   # the single record, or absent
people    = extract(content, node Person: "each person named" { name: <text> "their name" })   # a shape written in place
page      = fetchUrl(c.website)                                                              # a plugin: by position, settings last; absent when it finds nothing

Content is a list (shared content first, record-specific content last, so repeated calls hit the prompt cache); the shape is a declared node; settings are tier ("quick" / "careful" / "thorough"), model, effort. The description carries cardinality ("the company" = one; "each company" = all). map(…, f, { initialConcurrency: 1 }) warms the cache before the fan-out. An extraction writes nothing — writes are separate statements.

pitfalls #