# Lambda bodies (JavaScript)

> Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`. Lambda workers are a bounded shared resource, each surface binds a different set of identifiers, and the hazards are not guessable.

**A lambda is an escape hatch, not a default.** The body runs outside the request's own
runtime, and a workspace has a BOUNDED pool of lambda workers every lambda in it shares
— so a call both crosses a process boundary and draws on a workspace-wide resource.
Reach for one only when the typed surface cannot express the work: if a native filter,
an `expr(...)`/`obj(...)` expression, or a plain statement can, use that. The crossing
is per CALL, not per element — an iterating filter sends the body ONCE and loops on the
other side, so one body over a whole list beats one called from inside a stack loop.

The lambda statement (`s.lambda({ as, code, timeout? })`) and eight filters run a
JavaScript body. **Write the body as a FUNCTION, not a `c.text` string** — the
bindings are its parameters, so the editor supplies them and a wrong name is a
compile error instead of a wrong value at runtime. Write it inline and the surface
is implied by where it sits; nothing names one:

```ts
fl.map(({ $this }) => $this * 2)                                   // map's bindings, typed from the position
fl.reduce({ initial_value: 0, code: ({ $result, $this }) => $result + $this })
s.lambda({ as: "total", code: ({ $var }) => $var.subtotal * 1.2 })  // ambient only — $this is a compile error
```

The parameters are a fiction — only the BODY is sent, and the engine injects the
bindings as free identifiers — so DESTRUCTURE them. `(b) => b.$this` emits
`return b.$this`, and `b` is undefined at runtime (the SDK refuses it).

⚠ An inline `code:` arrow receives BINDINGS ONLY. `capture` is an option of
`lam.fn`, not a field of `s.lambda` or of a filter — to pass data in, move the
body into `lam.fn(fn, { capture })` (below). Writing `capture:` beside `code:`
is a type error, and the fix is to relocate the body, not to drop the field.

For a body built away from its call site:

- `lam.fn(({ $result, $this }) => $result + $this, { surface?, capture? })` — name a `surface` to check it here, or omit it and the call site checks it.
- `lam.raw("return 1", { surface })` — text, same validation.
- `lam.file("./lambdas/total.ts")` — a default-exported function in its own type-checked module, read as text at build time. The deterministic option under a bundler, where a function's source is whatever the bundler emitted. NODE ONLY, and it is the `lam` import that changes: `import { lam } from "@xanots/core/node"`. The isomorphic `lam` has no `file` (no filesystem in a browser bundle); its `fn` and `raw` are the same functions.

Nothing from the enclosing scope crosses implicitly. The body is sent as TEXT and runs
in a different process, so a closed-over `const rate` is undefined there, and the body
throwing on it returns the diagnostic text with HTTP 200 — a wrong VALUE, not an error.
Put what the body needs in `capture`; it arrives as the SECOND parameter and is emitted
ahead of the body as a `const` prelude:

```ts
lam.fn(({ $this }, { capturedRate }) => $this * capturedRate, { surface: "map", capture: { capturedRate: rate } })
```

⚠ A capture key must NOT share its name with a module-scope binding. An inline body is
recovered with `toString()`, and a `.ts` loader renames one of two same-named bindings —
so the body reads `rate2` while the prelude declares `rate`, and `rate2` is undefined at
runtime (the body throws and the engine returns that text in the value slot with HTTP
200 — a wrong value, not an error). Build time refuses it. The key does not have to keep
the name of what it carries: `capture: { capturedRate: rate }` above is the safe form.

Capture JSON data only — string, number, boolean, null, object, array. A function,
`undefined`, `symbol` or `bigint` has no JSON form that survives, and a `Date`/`Map`/
`Set`/`RegExp` has one that LIES (a `Date` arrives as a string, the rest as `{}`), so
all of them are refused at build time — the class ones at any depth. Capture the plain
form and rebuild in the body (`d.getTime()` → `new Date(d)`). The captured type flows into
that second parameter (no explicit type arguments), and an object may be declared as an
`interface` or a `type` alias alike.

A body is a FUNCTION BODY: it must `return` its value. Bindings by surface — an
identifier outside its surface's set is undefined at runtime, and the SDK refuses
it at build time whichever spelling you use:

- every surface: `$env` · `$input` · `$var` · `$auth` (+ the `console` / `crypto` globals)
- `fl.lambda`: + `$this`
- `fl.map` · `fl.filter` · `fl.some` · `fl.every` · `fl.find` · `fl.findIndex`: + `$this` · `$index` · `$parent`
- `fl.reduce`: + `$this` · `$index` · `$parent` · `$result`
- `s.lambda`: ambient only — no `$this`, no `$parent`, no `$result`.

`$result` is `reduce`'s ACCUMULATOR (there is no `$acc`). `$this` is the element in
an iterating filter and the piped value in `fl.lambda`; `$parent` is the whole array
and exists only on the iterating filters. A stack variable is reached as
`$var.name` — it is NOT also injected as a bare `$name`.

Four hazards and the dependency route, all live-verified:

- ⚠ A body that THROWS does not fail the request: the engine returns its diagnostic
  TEXT as the value with HTTP 200, so the failure reads as bad data. Validate before
  consuming a lambda result numerically, and prefer a `lam.*` body, which cannot fail
  this way for a binding reason.
- ⚠ `timeout` bounds a body that AWAITS, not one that spins. It is COOPERATIVE — only
  observed at an `await` — so synchronous work runs to completion however long it takes:
  a 1s `timeout` over a body that busy-loops for 3s lets it run all 3 and return
  normally. Treat it as a bound on WAITING (a slow `fetch`), not a kill switch on
  compute — if a loop could run away, bound it yourself inside the body.
- ⚠ A top-level `import`/`export` is a syntax error — the body is a function body, not
  a module. Reach a dependency through the PRELOADED globals below, which need no
  specifier. A dynamic `import("…")` or `require("…")` with a LITERAL specifier is not
  portable: on an instance that bundles the body before running it, every literal
  specifier is resolved ahead of time against a filesystem where none of them exist, so
  `await import("node:crypto")` comes back as the TEXT `Could not resolve "node:crypto"`
  with HTTP 200. Other instances resolve it at run time and it works — so it is
  instance-dependent, and only the globals are not.
- Preloaded globals, live-probed — no specifier, so these work everywhere:
  `_` · `aws4` · `axios` · `cryptojs` · `DateTime` · `ethers` · `fastXmlParser` ·
  `jose` · `luxon` · `mailparser` · `math` · `moment` · `nodemailer` · `socks` ·
  `uuid` · `utils`
  …plus `fetch`, `Buffer`, `TextEncoder`/`TextDecoder`, and the `crypto` above
  (`randomUUID`, `createHmac`, `subtle` all present). `Object.keys(globalThis)`
  inside a body lists whatever else a given instance carries.
- ⚠ `console` output goes to the request LOG, not stdout. `log` · `error` · `warn` ·
  `info` · `debug` · `trace` all route there; the body's `console` is a purpose-built
  object, so anything outside that set is undefined and CALLING it throws — which,
  per the first hazard, replaces the return value with the error text at HTTP 200.

TypeScript annotations survive in the body, and top-level `await` works.
