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 { 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 `@lf/email`                          # a system by its name; no credential
crm   = connect `@lf/attio` using acme_main          # with a stored connection, by its saved name
crm   = connect `@lf/attio` using acme_main { dryRun: true }   # dryRun is universal

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

function `Intake`(m: <inbox-[:Emails]->>) { … }  # 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-[e:Emails WHERE e.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 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
t = n?.Title                               # reads through n: absent when n is
if (n?.Title != null) { … }                # proves n present INSIDE this arm, as exists(n) does
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. Using such a value where a present one is required — a write or traversal target, a field read with ., 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(…) takes related records (exists(msg-[:Attachments]->)) or a value (exists(x), exists(x?.Field): the same test as x?.Field != null); x != null and x == null take a bound name or a field read off one, and a proven field proves the record it came off. A field may hold a record, a value read with . (c.Current.Stage): it has fields only, no edges, and the record as it is now is reached by its edge (c-[:Record]->). A field is never read with . through a value that may be absent: any of the above, a call's result (only(…).F), an index read (xs[0].F), a change's row (c.Previous is absent on a created row) or a map member. Read it with ?. (n?.Title, c.Previous?.Stage), absent when the value is, or test it first: exists(c.Previous) && c.Previous.Stage == "Applied", !exists(c.Previous) || c.Previous.Stage != "Interview", or inside if (exists(c.Previous)) { … }. A test of an index read at a literal (exists(xs[0])) proves every later xs[0]. A WHERE writes them AND, OR, NOT.

listeners #

listen to inbox-[e:Emails WHERE e.Key == "intake"]-> fire intake
listen as "Nightly digest" to cron-[t:Ticks WHERE t.Schedule == "0 9 * * 1" AND t.Zone == "Europe/London"]-> fire `Weekly Digest`
listen to crm-[:Companies]->-[c:Changes WHERE c.Kind == "created"]-> fire `On New`
listen to crm-[:Companies]->-[c:Changes WHERE c.Kind IN ["created", "updated"]]-> fire mirror
listen to manual-[:Invocations]-> fire `On Run`      # manual / on-demand, always connected

A listen is a traversal from a connection, ending on an edge its system announces arrivals on; the fired function's parameter is typed by what lands (<cron-[:Ticks]->>). An edge every team shares (Ticks, Emails) needs a WHERE naming its values. The as alias goes right after listen, before to. A manual run is the submit-text/files channel: the fired function's parameter (go: <manual-[:Invocation]->>) holds it, so read go.Text and traverse go-[:Files]->.File (both optional). Pausing = commenting the listen line out. There is no run statement.

reviews #

A run waits on what arrives on an edge of a record it holds: write the record, then await the edge, in a race with a sleep.

msg = write ops-[:Messages]-> {
  Message: "Pursue this?"
  Buttons: [{ Text: "Pursue", Value: "pursue" }, { Text: "Pass", Value: "pass" }]
}

r = race(
  () => {
    press = await msg-[:Interactions]->        # one press
    return only(press-[:Actions]->)?.Value      # "pursue" | "pass", from the Buttons written
  },
  () => {
    reply = await new msg-[:Replies]->         # only a reply arriving from now
    return reply.Message
  },
  () => { await sleep(2d) },                   # the default when nobody answers in time
)
if (r[0] == "pursue") { … }
if (r[0] == null && r[1] == null) { … }        # nobody answered: the safe default path

form = write forms-[:Forms]-> { Title: "Approve?", Fields: Refund }   # form.Url; its Submissions answer, typed by Refund
q = write asks-[:Check]-> { Prompt: "Pursue this?" }                 # q.Url; its Response answers, Answer a boolean
both = all(f, g)                                                     # all at once, one slot per function
await until(() => { refresh co; return co.Stage == "Won" }, { every: 1h })   # re-check a condition on a cadence

await marks the wait itself — await msg-[:Interactions]->, 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.

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. An until condition may not 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.

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 #