HeadlessOps
Scripting Guide

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:

FieldValue
awsRequestIdThe current run ID
functionNameThe integration ID
memoryLimitInMB'512'
invokedFunctionArnarn: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:

  1. Integration config values (same as ctx.config)
  2. Integration credentials (same as ctx.credentials)
  3. 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.credentials

Scoped 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() and process.env.TMPDIR / TEMP / TMP all resolve to this scoped directory.
  • The fs module (including fs/promises) is restricted to this directory — accessing a path outside it throws Access 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 — only os.tmpdir()
  • fs — scoped to the run's /tmp directory
  • fs/promises — scoped to the run's /tmp directory

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 supported

Web and Utility Globals

The sandbox includes a wide set of standard globals so common code patterns work without polyfills:

  • fetch — wrapped to route through ctx.http, so timeouts, retries, and run-log tracing still apply
  • crypto, crypto.randomUUID, Web Crypto (crypto.subtle)
  • TextEncoder, TextDecoder, structuredClone
  • FormData, Blob, Headers, Request, Response
  • URL, URLSearchParams, AbortController
  • performance, 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 like parseInt, 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__status
  • headers__headers
  • If body is 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 globalMessage
fetchfetch 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.