Skip to main content

Overview

The TypeScript transform lets you execute custom TypeScript (or JavaScript) code on each record in your pipeline. This is useful for:
  • Complex data transformations not supported by SQL
  • Custom business logic with type safety
  • Data parsing and formatting
  • Conditional transformations based on complex rules
  • Expanding one input row into many output rows, or filtering rows out
Code runs in a sandboxed WebAssembly environment. TypeScript is transpiled to ES2020 JavaScript (via SWC) when the pipeline is built, then executed inside a QuickJS interpreter shipped as WebAssembly (Extism’s js-pdk).
The runtime is browser-style JavaScript only — there is no Node.js, no DOM, and no package manager. Anything that depends on a Node API (require, import, process, fs, http, Buffer, __dirname, native modules) or on a browser API (window, document, localStorage, fetch, XMLHttpRequest) will fail at runtime. The sandbox provides standard ES2020 syntax plus the QuickJS-supported built-ins listed under What is available — nothing else. If a Node tutorial or npm package would be the answer in another context, it does not apply here. See Not available for the full list of unsupported APIs.
TypeScript transforms are significantly slower than SQL transforms. Use SQL to filter and project first, and only pass the minimum data required into a TypeScript transform.

Configuration

Parameters

string
required
Must be script
string
required
The source or transform to read data from
string
required
One of typescript, ts, javascript, or js. TypeScript values are transpiled to JavaScript at pipeline build time; plain JavaScript is passed through unchanged.
string
required
The column that uniquely identifies each row
object
Mapping of output field name to Arrow type. Required whenever invoke returns a shape different from the input — any field you return that isn’t declared here will be dropped. If omitted, the output schema is inherited from the input. Supported types: string (alias for utf8), int8, int16, int32, int64, uint8, uint16, uint32, uint64, float16, float32, float64, boolean, binary, date32, date64, timestamp, time32, time64, duration, interval, null.
string
required
Your TypeScript code. Must define a top-level invoke(data) function that receives a single record and returns one of: the transformed record object, null to filter the record out, or an array of record objects to expand one input row into many output rows (empty array emits nothing).
integer
default:"4"
Number of sandboxed script instances that process rows in parallel. Higher values improve throughput on CPU-bound scripts at the cost of more memory. Use 1 if your script relies on rows being processed in order.
integer
default:"0"
Minimum number of rows to accumulate before invoking the script. Smaller upstream batches are combined until this threshold is reached, which reduces per-call overhead on high-volume streams with tiny batches. 0 disables accumulation and processes each batch immediately.

Script structure

Your script must define a top-level invoke function that:
  • Accepts a single data parameter (a plain JS object representing one row)
  • Returns one of:
    • A record object — the transformed row
    • null — filter this row out of the output
    • An array of record objects — expand this input row into many output rows (return [] to emit nothing, include null entries in the array to skip specific rows)
  • Can return a different shape than the input when using the schema configuration
The script must consist of exactly one top-level invoke function. The runtime evaluates the script as a single expression, so top-level const/let declarations or additional top-level functions fail at runtime with errors like unexpected token in expression: 'const' or Unexpected token 'function'. Declare all constants and helper functions inside invoke.
The _gs_op column (insert/update/delete marker) is automatically copied from the input row to each output row, so your script does not need to set it.

Basic example

Declare a schema whenever invoke adds new fields. Without one, Turbo infers the output schema from the input and any new keys you return are dropped.

Filtering records

Return null to filter out records that don’t match your criteria:

Custom output schema

If you omit the schema field, the output schema is inherited from the input. Any new fields you add in invoke will be dropped because they aren’t in the schema. Declare a schema whenever your output differs from the input:

Input/output format

The data parameter is a plain JavaScript object with your record’s fields. Values use native JS types — strings stay strings, integers/floats become numbers, booleans stay booleans, list columns become arrays, struct columns become nested objects.
Return a modified object:

Expanding one row into many

Return an array of objects from invoke to emit multiple output rows for a single input row. This is useful for unpacking nested arrays or cross-joining with a lookup list. Returning [] drops the row entirely; null entries inside the array are skipped.

Examples

Example: Type-safe value formatting

Convert wei to ETH with TypeScript type safety:

Example: Parse JSON fields

Extract data from JSON strings with type safety:

Example: Complex conditional logic

Apply different transformations based on conditions:

Example: String manipulation

Clean and format text data:

Example: Array and object manipulation

Work with complex data structures:

TypeScript features

Type safety benefits

TypeScript provides:
  • Compile-time type checking: Catch errors before deployment
  • IntelliSense: Better IDE autocomplete and suggestions
  • Refactoring support: Safer code changes
  • Self-documenting code: Types serve as inline documentation

Supported TypeScript features

  • Interface definitions - Type aliases - Union and intersection types - Generic types - Optional properties (?) - Readonly properties
  • All ES6+ features (arrow functions, destructuring, spread operator) - JSON.parse() and JSON.stringify() - Math object (Math.floor, Math.random, etc.) - Date object - String methods (split, substring, replace, etc.) - Array methods (map, filter, reduce, etc.) - Object methods (Object.keys, Object.values, etc.) - BigInt for large number handling
  • typeof checks - instanceof checks - Custom type predicates - Discriminated unions

Not available

The following features are not available inside the sandbox. Attempting to use them produces a runtime error when the record is processed (not at pipeline build time), so the failure shows up as a stuck record in goldsky turbo logs rather than as a deploy error.
  • Module loadingrequire(), ES import statements, import() expressions. There is no module resolver and no package manager. Inline every dependency you need directly into the script field.
  • File systemfs, path, file URLs, __dirname, __filename. The sandbox has no filesystem.
  • Networkfetch, XMLHttpRequest, WebSocket, raw sockets. Outbound network access is blocked at the sandbox layer. Use an HTTP handler transform when you need to call an external API.
  • Node.js globalsprocess, Buffer, global, setImmediate, os, http, https, crypto (Node’s, not the Web Crypto subset), child_process, cluster, native addons.
  • Browser DOM and host APIswindow, document, localStorage, sessionStorage, navigator, location, alert, the Worker APIs.
  • TimerssetTimeout, setInterval, setImmediate, queueMicrotask. QuickJS has no event loop in this sandbox.
  • Asynchronyasync / await, Promise.resolve chains intended to perform I/O. Your invoke function must be synchronous and return its result directly.
Keep your scripts self-contained. The runtime supports ES2020 syntax plus the QuickJS built-ins listed below — no Node, no browser DOM, no package imports, no network.

What is available

The runtime is a QuickJS interpreter running inside WebAssembly. In addition to standard ES2020 syntax, you can use:
  • JSON.parse / JSON.stringify
  • Math, Date, BigInt, RegExp, Map, Set
  • All String, Array, and Object prototype methods
  • console.log / console.error — writes to the pipeline’s stderr log (visible via goldsky turbo logs, but not queryable from your sink)

Error Handling

Always include error handling in your scripts:
If your script throws an unhandled error, the pipeline will retry processing that record. Use try/catch to handle errors gracefully and flag problematic records for later review.

Performance tuning

Each transform exposes two optional knobs for throughput: parallelism and batch_size.

Parallelism

Controls how many sandboxed script instances process rows in parallel. Each instance handles a slice of the incoming batch.
  • Default: 4
  • Higher values: More concurrency, proportionally more memory
  • 1: Sequential processing — use this when your script depends on row order

Batch size

Controls how many rows are accumulated before invoke is called. Smaller upstream batches are combined until the threshold is reached, which reduces per-call overhead.
  • Default: 0 (disabled — each upstream batch is processed immediately)
  • Higher values: Better throughput on high-volume streams with tiny batches
  • Trade-off: Higher values increase end-to-end latency as rows wait to accumulate

Example

When to tune these parameters

Start with the defaults and adjust based on observed performance. Monitor memory usage when increasing parallelism, and monitor latency when increasing batch_size.

Performance considerations

  • TypeScript is transpiled to JavaScript once when the pipeline starts (no per-record transpile cost) - Each row is executed inside a QuickJS interpreter, which is significantly slower than native SQL transforms - Every record is evaluated individually - Keep scripts simple and avoid expensive per-row work (regex compilation, JSON.parse on huge blobs, allocating large temporary objects, etc.)
  • Scripts run in a sandboxed environment with limited memory - Avoid creating large data structures - Process records one at a time, don’t accumulate state - Clean up temporary variables - Higher parallelism values increase memory usage proportionally
  • Pre-define types and interfaces outside the function - Use built-in methods (Array.map, filter) instead of manual loops - Avoid nested loops and recursive functions - Cache frequently accessed values in variables - Use parallelism and batch_size to tune throughput for your workload

Debugging

Add debug fields

console.log output goes to the pipeline’s stderr log and is not visible in your sink. To inspect intermediate values in the data you actually ship, add debug fields to the returned record:
Then query your sink to see the debug fields.

Test locally

Before deploying, test your logic in a TypeScript playground or Node.js:

Best practices

1

Use SQL when possible

SQL transforms are faster and more efficient. Only use TypeScript for logic that SQL cannot express.
2

Define clear types

Define interfaces for your input and output types:
3

Use null to filter records

Return null to filter out records that don’t match your criteria:
4

Handle null and undefined

Always check for null/undefined values:
5

Use type guards

Validate data types at runtime:

When to use TypeScript vs SQL vs HTTP handler