Sequence lifecycle, readiness, input/output streams, and content types

July 18, 2026 · View on GitHub

Lifecycle states

A sequence instance progresses through these states, as defined in InstanceStatus from @scramjet/symbols:

flowchart LR
  INITIALIZING --> STARTING
  STARTING --> RUNNING
  RUNNING --> STOPPING
  RUNNING --> COMPLETED
  RUNNING --> ERRORED
  STOPPING --> COMPLETED
  STOPPING --> KILLING
  KILLING --> GONE
StateDescription
INITIALIZINGInstance record created, setup in progress
STARTINGThe outer runner (start-runner.ts) is launching the child runtime
RUNNINGThe sequence function is executing; input/output streams are active
STOPPINGGraceful stop requested via control channel; stop handlers run
KILLINGHard kill initiated because stop timed out or was explicit
COMPLETEDSequence finished normally
ERROREDSequence terminated with an error
GONEInstance is no longer present on the Hub

Validate before serving

A sequence validates packaged resources and required local configuration before it registers an exposed API route or enters a long-running stream or promise. The listener is deferred until validation succeeds. A failed validation emits structured diagnostics and events, leaves no active service route, and ends the instance as ERRORED; recovery requires a fresh instance start rather than an in-process retry.

Readiness is different from an HTTP process being alive. Callers should poll the Hub or Manager readiness signal and then verify the required instance route. See the local validation service walkthrough for the case-led version.

For Node sequences, the lifecycle order is validation, initialization, route activation, and then the long-running sequence function. Put prerequisite checks in the exported initialize hook. The runner calls it before the main function; route registration belongs after the checks succeed:

import { access } from "node:fs/promises";
import type { SequenceAppContext } from "@scramjet/sequence-types";

export async function initialize(this: SequenceAppContext) {
  await access(this.config.dataFile as string);
  this.api.use("/status", () => ({ ready: true, instanceId: this.instanceId }));
}

export default async function (this: SequenceAppContext) {
  await new Promise(() => {});
}

An initializer rejection emits INITIALIZE_REJECTED, leaves the route inactive, and ends the instance as errored. Start a fresh instance after fixing the resource; do not retry initialization in the failed process.

Stream architecture

Each sequence instance communicates with the host through a set of communication channels defined in the CommunicationChannel enum from @scramjet/symbols.

Primary channels

ChannelIDDirectionPurpose
STDIN0host → childSequence input data
STDOUT1child → hostSequence output data
STDERR2child → hostError/debug output
CONTROL3bidirectionalLifecycle commands and acknowledgements
MONITORING4bidirectionalMonitoring frames and control responses
IN5host → childStructured input data
OUT6child → hostStructured output data
LOG7child → hostStructured log output
REQUESTS8bidirectionalHTTP-style request/response for API-exposed sequences

The outer runner writes a boot config JSON file before spawning the child. The child runtime reads this config to learn how to connect its channels.

Boot config format

{
  "sequencePath": "/path/to/sequence.js",
  "instanceId": "uuid",
  "instancesServerPort": 12345,
  "instancesServerHost": "127.0.0.1",
  "sequenceInfo": { "id": "seq-1", "config": { "engines": { "node": ">=18" } } },
  "sequenceArgs": ["--flag", "value"],
  "appConfig": { "key": "value" },
  "instanceName": "my-instance",
  "logLevel": "DEBUG",
  "exposePath": "/api",
  "exposeHost": "0.0.0.0"
}

Message protocol

All control and monitoring frames use the same wire format: a JSON array [code, data] encoded as bytes and terminated by \r\n. The frame codes are defined in RunnerMessageCode from @scramjet/symbols.

Upstream frames (runner → host)

CodeNameDirectionPayload
3000PINGmonitoringPID, appConfig, args, expose info
3001MONITORINGmonitoringHealth data (healthy, load, memory, etc.)
3002DESCRIBE_SEQUENCEmonitoringAuto-detected function definition
3003ERRORmonitoringSerialized error
3006SEQUENCE_STOPPEDmonitoringExit reason / error
3010ALIVEmonitoringKeep-alive heartbeat
3011SEQUENCE_COMPLETEDmonitoringNormal completion marker
3012PANGmonitoringOutput content type metadata

Downstream frames (host → runner)

CodeNameDirectionPayload
4000PONGcontrolAcknowledgement of PING
4001STOPcontrolGraceful stop request
4002KILLcontrolHard kill request
4003MONITORING_RATEcontrolSet monitoring interval
4005SETcontrolSet configuration value

Content types

Sequences declare their input and output content types. The runner uses content type information for serialization decisions:

  • application/x-ndjson (default): Newline-delimited JSON — each output item is serialized as a JSON line
  • application/octet-stream: Raw binary passthrough
  • text/plain: Plain text — each item is stringified and joined with newlines
  • Any valid MIME type supported by the scramjet stream framework

Content type can be specified in the sequence metadata or via the AppContext config.

Function chaining

When a sequence exports an array of functions, each function's return value becomes the next function's input:

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

export default [
  // parse input
  async function (this: SequenceAppContext, input: Readable) {
    return input.pipe(through2.obj((chunk: any, _: any, cb: (error: Error | null, value?: unknown) => void) => cb(null, JSON.parse(chunk.toString()))));
  },
  // transform
  async function (this: SequenceAppContext, input: Readable) {
    const results = [];
    for await (const obj of input) results.push(transform(obj));
    return results;
  },
];

If a function returns a stream, it is piped as-is. If it returns a non-stream value, the runtime wraps it appropriately. The last function's return value is serialized to the output channel.

Exit codes

The runner maps process exit codes to sequence outcomes via RunnerExitCode (from @scramjet/symbols):

CodeMeaning
0Normal completion
20Invalid environment variables
21Invalid sequence path
22Sequence failed on start
23Sequence failed during execution
137Killed (SIGKILL)
138Stopped (SIGTERM)
139Disconnected
223Cleanup failed
101Uncaught exception