When a transform needs loops, regex, or logic the DSL can't express, use a JavaScript
transform. It runs in an embedded, sandboxed runtime: no filesystem, no network, hard
memory + stack limits, and a deadline interrupt. A worker pool (one runtime per scripted
route, sharded by machine id) gives parallelism with backpressure.

```js
// enrich.js: runs once per record
function transform(record, ctx) {
  if (record.metadata.temp > 95) return null;        // drop
  record.metadata.risk = record.metadata.temp * 0.8
                  + record.metadata.vib * 1.2;
  return record;                         // pass on (mutated)
}
```

| Contract | Behavior |
| --- | --- |
| `transform(record, ctx)` | Called per record. `record` is the `CanonicalMachineData` object; `ctx` is `{ source, timestamp, machineId }`. |
| Return an object | The object replaces the record and flows to its sinks (must deserialize back to `CanonicalMachineData`). |
| Return `null` / `undefined` | The record is dropped (same as DSL `drop`). |
| Throw / timeout / bad output | Applies `on_error` policy: `passthrough` (default, never lose data), `drop`, or `fail`. |

> **Warning**
>
> **Choose wisely.** Prefer the [DSL](/docs/transform-dsl) for filters and field math.
> Drop to JS only when you need expressiveness; it costs the marshal + interpreter tax
> per record.