JavaScript Runtime Behavior
Handler signatures, the process.env polyfill, built-in modules, scoped /tmp, and response auto-conversion for TypeScript/JavaScript steps.
JavaScript Runtime Behavior
Every TypeScript/JavaScript step runs inside a sandboxed execution environment on the HeadlessOps worker. This page documents the exact runtime behavior: what handler signatures are supported, what globals and built-in modules are available, and how responses are converted.
Handler Signatures
You can export either the HeadlessOps-native run(input, ctx) signature or a standard AWS Lambda-style handler(event, context). If both are present in the same file, run takes precedence.
run(input, ctx) — Native Signature
export async function run(input: Record<string, any>, ctx: StepContext) {
return { ok: true };
}This is the signature used throughout the rest of this documentation and is the recommended default.
handler(event, context) — Lambda-Style Signature
export async function handler(event: any, context: any) {
return {
runId: context.awsRequestId,
remainingTime: context.getRemainingTimeInMillis(),
inputValue: event.some_field,
};
}export const handler = async (event, context) => { ... } and CommonJS exports.handler = async function (event, context) { ... } are also supported. This makes it straightforward to port existing AWS Lambda functions onto HeadlessOps with minimal changes.
The Lambda-style context object exposes:
| Field | Value |
|---|---|
awsRequestId | The current run ID |
functionName | The integration ID |
memoryLimitInMB | '512' |
invokedFunctionArn | arn:aws:lambda:us-east-1:000000000000:function:<integrationId> |
getRemainingTimeInMillis() | Milliseconds left before the step timeout |
process.env Polyfill
The runtime provides a read-only process.env object so code that expects standard Node.js environment variables works without modification. Values resolve in order from:
- Integration config values (same as
ctx.config) - Integration credentials (same as
ctx.credentials) TMPDIR/TEMP/TMP— the run's scoped temp directory (see below)
export async function run(input: Record<string, any>, ctx: StepContext) {
const apiBase = process.env.API_BASE_URL; // from ctx.config
const apiKey = process.env.STRIPE_KEY; // from ctx.credentials
return { apiBase, apiKey };
}process.env behaves like a normal object for Object.keys(), Object.entries(), spread, and for...in. It is read-only — assignments throw a clear error, and no host secrets (database URLs, internal tokens, etc.) are ever exposed through it.
Accessing any other process.* property (e.g. process.exit, process.argv) throws:
process is not available; use ctx.config or ctx.credentialsScoped Temporary Files
Each run gets a writable directory at /tmp/iterator-run-<runId>/, created before the run starts and recursively deleted afterward — even if the run fails.
import * as fs from "fs";
import * as os from "os";
export async function run(input: Record<string, any>, ctx: StepContext) {
const tmp = os.tmpdir();
const file = `${tmp}/result.json`;
fs.writeFileSync(file, JSON.stringify({ hello: "world" }));
return { content: fs.readFileSync(file, "utf-8") };
}os.tmpdir()andprocess.env.TMPDIR/TEMP/TMPall resolve to this scoped directory.- The
fsmodule (includingfs/promises) is restricted to this directory — accessing a path outside it throwsAccess denied: path '...' escapes the scoped /tmp directory.
Built-in Node.js Modules
Exposed as both sandbox globals and via require/import:
path, url, util, stream, events, querystring, string_decoder
Exposed only via require/import (not as bare globals):
os— onlyos.tmpdir()fs— scoped to the run's/tmpdirectoryfs/promises— scoped to the run's/tmpdirectory
The node: prefix is supported for all of the above (e.g. require('node:path')). ESM imports of allowed built-ins are automatically rewritten to CommonJS require calls so import * as path from 'path' works transparently inside the sandbox.
The following modules are blocked for security and throw on import: child_process, net, cluster, dgram, repl, vm, worker_threads. Any other unrecognized module throws:
Module '<name>' is not available; only built-in modules are supportedWeb and Utility Globals
The sandbox includes a wide set of standard globals so common code patterns work without polyfills:
fetch— wrapped to route throughctx.http, so timeouts, retries, and run-log tracing still applycrypto,crypto.randomUUID, Web Crypto (crypto.subtle)TextEncoder,TextDecoder,structuredCloneFormData,Blob,Headers,Request,ResponseURL,URLSearchParams,AbortControllerperformance,queueMicrotask,setImmediate,clearImmediate- Timers:
setTimeout,clearTimeout,setInterval,clearInterval Buffer, typed arrays,ArrayBuffer,DataView- Standard constructors:
Error,JSON,Math,Object,Promise,Map,Set,WeakMap,WeakSet,Intl,Reflect,Proxy, and global functions likeparseInt,encodeURIComponent
global and globalThis both point to the sandbox.
Lambda-Style Response Auto-Conversion
If a step's return value has a numeric statusCode field, the runtime automatically converts it into HeadlessOps' native output format — so API Gateway-style Lambda handlers work with no changes.
export async function handler(event: any, context: any) {
return {
statusCode: 201,
headers: { "Content-Type": "application/json", "X-Custom": "yes" },
body: JSON.stringify({ id: 123, created: true }),
extra: "field",
};
}is automatically converted to:
{
"__status": 201,
"__headers": { "Content-Type": "application/json", "X-Custom": "yes" },
"id": 123,
"created": true,
"extra": "field"
}statusCode→__statusheaders→__headers- If
bodyis a JSON string, it is parsed and merged into the output - Any other fields on the response object are preserved
- Existing output conventions (
__html,__base64) continue to work unchanged — see Trigger Types for synchronous webhook response formats
Actionable Error Diagnostics
When a step references a global that is intentionally restricted, the runtime rewrites the resulting error into an actionable message instead of a generic ReferenceError:
| Missing global | Message |
|---|---|
fetch | fetch is not available; use ctx.http or declare packages: [] if you need a custom HTTP client |
process (unsupported property) | process is not available; use ctx.config or ctx.credentials |
fs (path outside scoped tmp) | fs is not available; use the scoped /tmp directory for temporary files |
Imports and the SDK
@iterator/sdk type imports are stripped automatically at bundle time — the integration context is passed as the second argument to run/handler, so no runtime SDK import is needed inside the sandbox.
For adding third-party npm packages to a step, see npm Packages.