Writing sequences for Transform Hub

July 18, 2026 · View on GitHub

A sequence is a deployable unit of work that Scramjet Transform Hub supervises. Sequences can be written in JavaScript (Node.js), TypeScript (Node.js), Bun, or Python. This guide covers the supported application shapes, the canonical @scramjet/sequence-types author API, and the contracts required by each runtime.

Sequence basics

A sequence is an exported function (or array of functions) from a module. The canonical @scramjet/sequence-types package exposes the sequence-facing SequenceAppContext and application types for readable, writable, transforming, and inert applications. Each application receives the SequenceAppContext as this, an input stream when applicable, and optional arguments from the caller.

The named author-facing types are SequenceApplication, SequenceApplicationFunction, SequenceReadableApp, SequenceWritableApp, SequenceTransformApp, and SequenceInertApp. There is no @scramjet/sequence package; use @scramjet/sequence-types for these contracts.

Input → function1 → function2 → … → functionN → Output

Supported runtimes

RuntimeIdentifierEngine key in package.json
Node.js 18+nodeengines.node
Bun 1.xbunengines.bun
Python 3.9+python3engines.python3

The platform auto-detects the runtime by inspecting package.json engine keys via selectRuntimeKind() from @scramjet/symbols. Resolution priority is nodebunpython3.

Node.js / TypeScript sequences

Node.js sequences are the most common. Export a function (or an array of functions) from the module entry point. An optional exported initialize function runs before the sequence function and is the readiness hook for local validation and API route registration. TypeScript is fully supported — the runner compiles or loads via ts-node in development mode.

Minimal Node.js sequence:

import type { SequenceAppContext } from "@scramjet/sequence-types";

export default async function (this: SequenceAppContext, input: Readable) {
  let count = 0;
  for await (const chunk of input) {
    this.logger.info("received", chunk.toString());
    count++;
  }
  return { processed: count };
}

Pipeline (multi-function sequence):

import type { SequenceAppContext } from "@scramjet/sequence-types";

export default [
  async function (this: SequenceAppContext, input: Readable) {
    // Transform input, return a stream or value
    return input.pipe(new Transform({ objectMode: true, transform(chunk: any, _: any, cb: (error: Error | null, value?: string) => void) { cb(null, chunk.toString().toUpperCase()); } }));
  },
  async function (this: SequenceAppContext, input: Readable) {
    const lines = [];
    for await (const line of input) lines.push(line);
    return lines;
  },
];

Bun sequences

Bun sequences use the same canonical SequenceAppContext surface from @scramjet/sequence-types as Node sequences. The supported Bun wrapper always enters through the hosted runner path and delegates the host-connected execution to the Node runtime, so health, lifecycle, logging, events, Hub/Space clients, local storage, and API exposure have the Node-equivalent contract. There is no separate author-visible direct/headless Bun mode.

Use the same context-aware sequence shape as the Node example above and package the sequence with "engines": { "bun": ">=1.0" }.

Python sequences

Python sequences export a main() or run() function. The function signature is:

def main(context, input_stream, *args):
    pass

The context object is the hosted Python wrapper context. Its hub and space attributes are scoped Broker-backed request clients (not Node-fluent equivalents, not a generic Python REST SDK); use their get()/post() methods for routes available to the sequence. The input_stream is an async iterable of bytes.

Python sequence:

"""my_sequence.py"""
import json

def main(context, input_stream):
    context.logger.info("sequence started")
    total = 0
    async for chunk in input_stream:
        data = json.loads(chunk.decode())
        total += data.get("value", 0)
    context.logger.info(f"total: {total}")
    return {"total": total}

Package the sequence with "engines": { "python3": ">=3.9" } in package.json and set "main" to the Python file path.

SequenceAppContext API

The SequenceAppContext interface (defined in @scramjet/sequence-types) is the primary interaction surface for sequence authors:

Method / PropertyPurpose
this.loggerStructured logger (IObjectLogger)
this.configApplication configuration (partial of <AppConfigType>)
this.addStopHandler(fn)Register a graceful-stop handler
this.addKillHandler(fn)Register a kill handler
this.addMonitoringHandler(fn)Register a health-check handler
this.keepAlive(ms?)Postpone shutdown timeout
this.end()Signal normal completion
this.destroy(error?)Signal fatal error
this.emit(event, message)Send an event to the host
this.hubHub API client (HostClient)
this.hubClient()Canonical v2 Hub API fluent client
this.spaceManager / Space API client
this.spaceClient()Canonical v2 Space API fluent client routed through the connected Manager/space proxy
this.instanceIdCurrent instance identifier
this.apiLocal API expose surface
this.localStorageKey-value local storage
this.exitTimeoutMilliseconds before force exit (default 10000)

Hub and Space API access

Existing sequences can keep using this.hub and this.space; those properties remain legacy v1-compatible clients for backwards compatibility. New sequence code should prefer this.hubClient() and this.spaceClient(), which return v2 fluent clients backed by @scramjet/rest-api2.

import type { AppConfig, SequenceAppContext } from "@scramjet/sequence-types";
import type { HubClient, SpaceClient } from "@scramjet/rest-api2";

type V2Context = SequenceAppContext<AppConfig, unknown, HubClient, SpaceClient>;

export default async function (this: V2Context) {
  const hubHealth = await this.hubClient().health.get();
  const spaceHubs = await this.spaceClient().hubs.get();

  this.logger.info("hub health", hubHealth.body);
  this.logger.info("space hubs", spaceHubs.body.items);
}

hubClient() is scoped to the current Hub v2 API. spaceClient() is scoped to the Manager/Space v2 API and is routed through the Hub's space proxy, so Hub-level and Space-level operations remain separate.

Input and output

Sequences receive input through the first argument after the context. The input is a Readable stream. Sequences can return:

  • A primitive (string, number, boolean, null) — serialized as NDJSON
  • A stream — piped through the output channel
  • An object — serialized as NDJSON
  • void / undefined — no output

See Sequence lifecycle for stream and content-type details.

For the complete readiness example, see Start a local validation service safely. For runtime limits, see the AppContext conformance matrix.