Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

@ironflow/core

Shared types, schemas, constants, and utilities for the Ironflow JavaScript SDK.

Note: You typically don’t need to install this package directly. It’s automatically included as a dependency of @ironflow/browser and @ironflow/node.

Terminal window
npm install @ironflow/core

Import type definitions for type-safe development:

import type {
// Event types
IronflowEvent,
EventFilter,
EventSourceType,
// Function types
IronflowFunction,
FunctionConfig,
FunctionContext,
FunctionHandler,
Trigger,
RetryConfig,
OnExhausted,
CircuitBreakerConfig,
ThrottleConfig,
ConcurrencyConfig,
ExecutionMode,
// Step types
StepClient,
Duration,
ParallelOptions,
// Run types
Run,
RunInfo,
RunStatus,
ListRunsOptions,
ListRunsResult,
CancellationEvent,
RunEvent,
RunVersionFilter,
// Trigger & emit types
InvokeResult,
InvokeSyncOptions,
InvokeSyncResult,
EmitOptions,
EmitResult,
EmitSyncResult,
// Subscription types
Subscription,
SubscribeOptions,
SubscriptionCallbacks,
SubscriptionEvent,
SubscriptionErrorInfo,
EventMetadata,
ConnectionState,
BufferConfig,
AckHandle,
AckableSubscription,
// Consumer group types
AckMode,
BackpressureMode,
ConsumerGroupConfig,
ConsumerGroup,
ConsumerGroupStatus,
// KV types
KVBucketConfig,
KVBucketInfo,
KVEntry,
KVPutResult,
KVListKeysResult,
KVListBucketsResult,
KVWatchEvent,
KVWatchCallbacks,
KVWatchOptions,
KVWatcher,
// File storage types
FileBucketConfig,
FileBucketInfo,
FileInfo,
FileObject,
ListFilesOptions,
ListFilesResult,
PutFileOptions,
GetFileOptions,
MoveFileOptions,
CopyFileOptions,
SignUploadOptions,
SignedUrl,
// Config types
ConfigResponse,
ConfigEntry,
ConfigSetResult,
ConfigWatchCallbacks,
// Branded ID types
RunId,
FunctionId,
StepId,
EventId,
// Logger
Logger,
// Server types
ServerCapabilities,
} from "@ironflow/core";

Represents an event in the system.

interface IronflowEvent<T = unknown> {
id: string;
name: string;
version: number;
data: T;
timestamp: Date;
idempotencyKey?: string;
source?: string;
metadata?: Record<string, unknown>;
emittedBy?: string;
}

emittedBy is the authenticated emitter that the engine recorded, for example apikey:<id> or user:<id>. The engine sets it from the credential of the caller. Function trigger events, stored event reads, entity stream reads, and waiting-step results expose it when the engine reports one; it is undefined when absent.

The metadata field carries user-defined key-value pairs attached at emit time (via the metadata option in client.emit() or the REST API). It is available in function handlers as event.metadata and in subscription events when includeMetadata is enabled.

Represents a workflow run.

interface Run {
cancellationCause?: string;
cancellationEvent?: CancellationEvent;
id: string;
functionId: string;
eventId: string;
status: RunStatus;
attempt: number;
maxAttempts: number;
input?: unknown;
output?: unknown;
error?: {
message: string;
code?: string;
};
startedAt?: Date;
endedAt?: Date;
createdAt: Date;
updatedAt: Date;
}

CancellationEvent (eventId, eventName, matchPath, matchValue) is the matched rule and event retained for a cancelled run.

type RunStatus =
| "running"
| "completed"
| "failed"
| "cancelled"
| "paused"
| "waiting_for_capacity"
| "waiting";

Configuration for an Ironflow function.

interface FunctionConfig {
id: string;
name?: string;
description?: string; // Human-readable description shown in the dashboard
triggers: Trigger[];
retry?: RetryConfig;
timeout?: number; // milliseconds (unset: no per-function budget)
concurrency?: ConcurrencyConfig;
circuitBreaker?: CircuitBreakerConfig; // Push-endpoint circuit breaker overrides
throttle?: ThrottleConfig; // Soft start-rate limit
debounce?: DebounceConfig; // Collapse rapid-fire events into a single invocation
mode?: ExecutionMode; // "push" | "pull"
actorKey?: string; // JSON path for an actor key. Stored and returned by the server; dispatch does not use it for routing yet
schema?: ZodType; // Zod schema; validated at runtime before the handler runs
secrets?: string[]; // Secret names this function requires
stepTimeout?: string; // Default timeout for step.run() calls ("30s", "5m")
recording?: boolean; // Enable audit recording
recordingProfile?: "all" | "run_lifecycle" | "steps"; // Workflow event families to capture
cancelOn?: CancelOnConfig[]; // Auto-cancel running runs when matching events arrive
metadata?: Record<string, unknown>; // Custom metadata (e.g., service, team, owner)
}

A pull run has no function timeout. For a push run, timeout cannot go above the engine pushTimeout ceiling (default 10s).

The nested config types:

interface Trigger {
event: string;
expression?: string; // CEL filter
cron?: string; // schedule, for example "0 9 * * *"
}
interface RetryConfig {
maxAttempts?: number; // total attempts, the first included. Server default 25
initialDelayMs?: number; // server default 1000
backoffFactor?: number; // server default 2.0
maxDelayMs?: number; // server default 300000
onExhausted?: "pause" | "fail"; // server default "pause"
}
// The server raises a window shorter than the longest probe (21.3s with the
// default push timeout) to that minimum; see the circuit breakers how-to.
interface CircuitBreakerConfig {
failureThreshold?: number; // consecutive failed attempts that open the circuit. Server default 5
openDurationMs?: number; // first open window; each failed probe doubles it. Server default 60000
maxOpenDurationMs?: number; // cap for the doubling window. Server default 600000; at least openDurationMs
}
interface ThrottleConfig {
limit: number; // runs allowed to start per period, at least 1
periodMs: number; // window length, at least 1000
key?: string; // path into the event data; omitted means one lane for the whole function
}
interface ConcurrencyConfig {
limit: number;
key?: string; // Path into the event data, e.g. "customerId" (not "event.data.customerId")
}
interface DebounceConfig {
periodMs: number; // quiet period, minimum 1000
key?: string; // JSON path; omitted means one global lane
maxWaitMs?: number; // fire at least this often; must be >= periodMs
}
interface CancelOnConfig {
event: string;
match: string; // JSON path that must equal the same field on the running run
}

debounce works only for asynchronous calls. A synchronous call to a debounced function fails with a FailedPrecondition error.

The SDK sends only the retry fields you set. The server fills in the rest when each run is created, from spec.engine.retry: 25 attempts (the first included), a 1 second initial delay, a backoff factor of 2.0, a 5 minute maximum delay, and onExhausted: "pause". A function registered by an earlier SDK version keeps the values stored with it until it registers again.

onExhausted decides what happens when a retryable failure has used every attempt:

  • "pause" holds the run, with its completed steps, until you resume it. The run is not terminal, so its compensations do not run.
  • "fail" ends the run as failed and runs its compensations.

A non-retryable error always fails the run at once.

Throw RetryAfterError to choose the delay before the next attempt. The engine waits retryAfterMs instead of the computed backoff, capped at maxDelayMs. It is retryable, so onExhausted applies to it as to any retryable error. A RetryAfterError thrown inside step.run() keeps its delay when the step wraps it in a StepError.

import { RetryAfterError } from "@ironflow/core";
throw new RetryAfterError("rate limited", 30_000);

Unlike the Go and Python SDKs, IronflowError here has no retryAfter field, so a Retry-After header on an HTTP error is not forwarded as a run retry hint; only RetryAfterError sets the delay. A zero, negative or non-finite retryAfterMs means no hint and the normal backoff applies.

A handler can read ctx.run.attempt, ctx.run.maxAttempts and ctx.run.onExhausted to see where it is in the budget. maxAttempts is 0 and onExhausted is "" when the engine is too old to send them.

createFunction rejects, before registering, a retry.onExhausted other than "pause" or "fail", a negative circuitBreaker field, a circuitBreaker.maxOpenDurationMs below openDurationMs, a throttle.limit below 1 and a throttle.periodMs below 1000.

A throttle is soft. At most limit new runs start per periodMs (per key value when key is set). Runs over the limit wait in the queue and are never dropped. Retries and resumed runs are not throttled.

Client for recorded execution (durable steps).

interface StepClient {
run<T>(name: string, fn: () => Promise<T>, options?: StepRunOptions): Promise<T>;
sleep(name: string, duration: Duration): Promise<void>;
sleepUntil(name: string, until: Date | string): Promise<void>;
waitForEvent<T>(name: string, filter: EventFilter): Promise<IronflowEvent<T>>;
parallel<T extends unknown[]>(
name: string,
branches: { [K in keyof T]: (step: StepClient) => Promise<T[K]> },
options?: ParallelOptions,
): Promise<T>;
map<T, R>(
name: string,
items: T[],
fn: (item: T, step: StepClient, index: number) => Promise<R>,
options?: ParallelOptions,
): Promise<R[]>;
compensate(stepName: string, fn: () => Promise<void>): void;
invoke<T = unknown>(functionId: string, input?: unknown, options?: { timeout?: string }): Promise<T>;
invokeAsync(functionId: string, input?: unknown): Promise<{ runId: string }>;
publish(topic: string, data: unknown, options?: PublishOptions): Promise<PublishResult>;
}

Logger interface used throughout the SDK.

interface Logger {
debug(message: string, data?: Record<string, unknown>): void;
info(message: string, data?: Record<string, unknown>): void;
warn(message: string, data?: Record<string, unknown>): void;
error(message: string, data?: Record<string, unknown>): void;
}

Configuration for creating a KV bucket.

interface KVBucketConfig {
name: string;
description?: string;
ttlSeconds?: number;
maxValueSize?: number;
maxBytes?: number;
history?: number;
}

A key-value entry returned by get.

interface KVEntry {
key: string;
value: unknown;
revision: number;
created_at: Date;
operation: string; // "put" | "delete"
}

A change event delivered over WebSocket.

interface KVWatchEvent {
type: "kv_update";
key: string;
value: string; // the stored bytes, base64-encoded; "" for a delete
revision: number;
operation: "put" | "delete";
bucket: string;
}
interface KVWatchCallbacks {
onUpdate: (event: KVWatchEvent) => void;
onError?: (error: Error) => void;
onClose?: () => void;
}

Options for emitting events.

interface EmitOptions {
version?: number; // Event schema version (default: 1)
idempotencyKey?: string;
metadata?: Record<string, unknown>;
namespace?: string; // default: "default"
}

One run’s outcome from emitSync(). emitSync() returns EmitSyncResult[]: an event can match several triggers, and every matched run is reported. An event that matches nothing returns [].

interface EmitSyncResult {
runId: string; // ID of the triggered run
functionId: string; // ID of the function that handled the event
status: RunStatus; // Final run status
output: unknown; // Function return value
error?: { message: string; code?: string };
durationMs: number; // Wall-clock time from trigger to completion
waitTimedOut: boolean; // Wait budget expired; the run is still going
}

emitSync() never throws on a run outcome — with N results there is no unambiguous choice of which failure to raise. Inspect status, error and waitTimedOut per element. Transport, protocol and validation errors still throw. See ADR 0067.

Argument and return type of the ID-keyed invoke() method, which targets one function and produces exactly one run.

interface InvokeSyncOptions<TInput = unknown> {
data: TInput;
timeout?: number; // server-side wait budget in ms (default 30000)
idempotencyKey?: string;
metadata?: Record<string, unknown>;
}
type InvokeSyncResult = Omit<EmitSyncResult, "waitTimedOut">;

Because it guarantees a single run, invoke() does throw RunFailedError, RunCancelledError and non-retryable RunWaitTimeoutError — which is why waitTimedOut is absent from the result.

timeout is a server-side wait budget, not a transport deadline. InvokeFunctionSync ties the run’s lifetime to the request context, so aborting the HTTP request cancels the run server-side.

Return type of emit(). Returned immediately after the event is accepted — the runs may still be in progress.

interface EmitResult {
runIds: string[]; // IDs of created runs
eventId: string; // ID of the stored event
}

A subscription to system.run.> delivers frames for run transitions; one write can publish two (.failed and .updated). The payload of a system.run.{runId}.{event} frame is a RunEvent. Step frames (system.run.{runId}.step.{stepId}.{event}) have a different shape.

interface RunEvent {
id: string;
functionId: string;
eventId: string;
executionMode: string;
status: string;
attempt: number;
maxAttempts: number;
version?: number; // Rises with every write to the run; absent on `deleted` frames, when the committed version is unknown, and on older servers
createdAt: string;
updatedAt: string;
startedAt?: string;
completedAt?: string;
output?: unknown;
error?: unknown;
timestamp: string;
}
type RunVersionFilter = (event: SubscriptionEvent) => boolean;
function createRunVersionFilter(options?: { maxRuns?: number }): RunVersionFilter;

Frames for one run can arrive out of order, so a completed frame can land before the running one. createRunVersionFilter() returns a filter that drops older run and step frames and late frames for a deleted run. It passes frames outside system.run., frames with no usable version, and frames whose version ties the highest seen. It remembers up to maxRuns run and step identities (default 10000), then forgets the least recent. Use one filter per subscription.

const keep = createRunVersionFilter();
await client.subscribe('system.run.>', {
onEvent: (event) => {
if (keep(event)) render(event.data as RunEvent);
},
});

Runtime validation schemas using Zod:

import {
// Push mode schemas
PushRequestSchema,
PushRequestEventSchema,
CompletedStepSchema,
ResumeContextSchema,
// Response schemas
RunStatusSchema,
TriggerResponseSchema,
TriggerSyncResponseSchema,
RunResponseSchema,
ListRunsResponseSchema,
HealthResponseSchema,
ErrorResponseSchema,
// Consumer group schemas
ConsumerGroupResponseSchema,
ListConsumerGroupsResponseSchema,
// Worker schemas
JobAssignmentSchema,
JobEventSchema,
// WebSocket schemas
WSServerMessageSchema,
WSEventMessageSchema,
// Validation helpers
parseAndValidate,
validate,
} from "@ironflow/core";
// Type-safe parsing with error handling
const result = RunStatusSchema.safeParse(rawStatus);
if (result.success) {
console.log(result.data);
}

Default configuration values:

import {
DEFAULT_SERVER_URL, // 'http://localhost:9123'
DEFAULT_WS_URL, // 'ws://localhost:9123/ws'
DEFAULT_PORT, // 9123
DEFAULT_HOST, // 'localhost'
DEFAULT_RECONNECT, // { ENABLED, MAX_ATTEMPTS, ... }
DEFAULT_WORKER, // { MAX_CONCURRENT_JOBS, ... }
DEFAULT_TIMEOUTS, // { CLIENT, FUNCTION, TRIGGER_SYNC, INVOKE_FUNCTION_SYNC, SYNC_TRANSPORT_HEADROOM }
DEFAULT_RETRY, // { MAX_ATTEMPTS, INITIAL_DELAY_MS, ... }
RUN_STATUS, // { RUNNING, COMPLETED, FAILED, CANCELLED, PAUSED, WAITING_FOR_CAPACITY, WAITING }
STEP_STATUS, // { COMPLETED, FAILED, WAITING }
ENV_VARS, // { SERVER_URL, SIGNING_KEY, API_KEY, LOG_LEVEL }
} from "@ironflow/core";
ConstantValueDescription
DEFAULT_SERVER_URL'http://localhost:9123'Default Ironflow server URL
DEFAULT_WS_URL'ws://localhost:9123/ws'Default WebSocket URL
DEFAULT_RECONNECT.ENABLEDtrueAuto-reconnect enabled by default
DEFAULT_RECONNECT.MAX_ATTEMPTS10Maximum reconnection attempts
DEFAULT_RECONNECT.INITIAL_DELAY_MS1000Initial backoff delay
DEFAULT_RECONNECT.MAX_DELAY_MS30000Maximum backoff delay
DEFAULT_WORKER.MAX_CONCURRENT_JOBS10Default concurrent job limit
DEFAULT_WORKER.HEARTBEAT_INTERVAL_MS30000Worker heartbeat interval
DEFAULT_TIMEOUTS.CLIENT30000Client request timeout
DEFAULT_TIMEOUTS.FUNCTION600000Exported constant. Registration does not apply it: an unset timeout means no per-function budget

Built-in error types with error code classification:

import {
IronflowError,
ConnectionError,
SubscriptionError,
ValidationError,
SchemaValidationError,
SignatureError,
TimeoutError,
StepError,
StepTimeoutError,
FunctionNotFoundError,
RunNotFoundError,
RunWaitTimeoutError,
RunFailedError,
RunCancelledError,
NonRetryableError,
NotConfiguredError,
UnauthenticatedError,
UnauthorizedError,
EnterpriseRequiredError,
ConflictError,
ContendedError,
PreconditionFailedError,
PreconditionRequiredError,
PayloadTooLargeError,
UnsupportedMediaTypeError,
InjectionUnverifiedError,
InvokeError,
InvokeTimeoutError,
AgentInvokeTimeoutError,
MemoryCatchupTimeoutError,
QueueFullError,
isRetryable,
isIronflowError,
toError,
} from "@ironflow/core";
try {
await someOperation();
} catch (error) {
if (error instanceof IronflowError) {
console.log(error.code); // Error code
console.log(error.message); // Error message
console.log(error.retryable); // Whether retryable
if (isRetryable(error)) {
// Retry the operation
}
}
}
Error ClassDescription
IronflowErrorBase error class with code, retryable, details, and status (the HTTP status, when the error came from a response)
ConnectionErrorWebSocket/HTTP connection failures (retryable)
SubscriptionErrorSubscription-related errors; has subscriptionId (retryable)
ValidationErrorInvalid input or schema validation failures; has validationErrors
SchemaValidationErrorZod schema validation failures
SignatureErrorWebhook signature verification failures
TimeoutErrorOperation timeout with timeoutMs property
StepErrorStep execution failures with stepId and stepName
StepTimeoutErrorstep.run() exceeded its timeout; has stepName and timeout (string) properties (retryable)
FunctionNotFoundErrorFunction not found with functionId property
RunNotFoundErrorRun not found with runId property
RunWaitTimeoutErrorinvoke() stopped waiting while the run remained active; has runId, functionId, runStatus, and timeoutMs properties (non-retryable). emitSync() reports the same condition as waitTimedOut on the result instead of throwing
RunFailedErrorThrown by invoke() when the targeted run fails; has runId and output properties. emitSync() reports a failure as status + error per result instead
RunCancelledErrorThrown by invoke() when the targeted run is cancelled; has runId property. emitSync() reports it as status per result instead
NonRetryableErrorPermanent failures that should not be retried
RetryAfterErrorRetryable failure that asks for a delay: new RetryAfterError(message, retryAfterMs). The engine waits retryAfterMs instead of the computed backoff, capped at the retry policy’s maxDelayMs. Has retryAfterMs (code RETRY_AFTER)
NotConfiguredErrorClient not configured (call configure() first)
UnauthenticatedErrorHTTP 401 — missing or invalid API key
UnauthorizedErrorHTTP 403 — API key lacks required permissions
EnterpriseRequiredErrorHTTP 402 — legacy; retained for wire compatibility. Ironflow ships a single build with no Enterprise/Core split (ADR 0015), so the server never returns 402.
ConflictErrorHTTP 409 that is not a lost race — e.g. a second resumeRun() of the same run within about 2 minutes, even after the first finished. It is not queued; retry after about 2 minutes
PreconditionFailedErrorHTTP 412 — an If-Match or If-None-Match precondition failed (file storage)
PreconditionRequiredErrorHTTP 428 — the write needs If-Match with the version you read (policy set)
PayloadTooLargeErrorHTTP 413 — a file is larger than the server, bucket or token cap (file storage)
UnsupportedMediaTypeErrorHTTP 415 — the content type is not allowed by the bucket or the signed URL (file storage)
ContendedErrorHTTP 409 from a lost compare-and-set race (Connect aborted); nothing was applied, so re-read and reissue
InjectionUnverifiedErrorConnect aborted with Ironflow-Error-Reason: injection_unverified; injectStepOutput() wrote the step but could not confirm the run stood still. Unlike ContendedError the write did land — read the step rather than reissuing
InvokeErrorstep.invoke() failed; has functionId, childRunId, and errorCause properties
InvokeTimeoutErrorstep.invoke() timed out; extends InvokeError, adds timeoutMs property
AgentInvokeTimeoutErrorAgent invoke() exceeded its wait deadline
MemoryCatchupTimeoutErrorAgent run timed out catching up its memory stream during resume
QueueFullError@ironflow/browser offline write queue is at maxItems or maxBytes; the write was not stored

Normalizes any thrown value to an Error instance. Useful in catch blocks where the caught value may be a string, number, or plain object rather than an Error.

function toError(error: unknown): Error
try {
await riskyOperation();
} catch (err) {
const error = toError(err); // always an Error instance
console.error(error.message);
}

import {
generateId,
createRunId,
createStepId,
createEventId,
createFunctionId,
} from "@ironflow/core";
// Generate a unique ID (no arguments)
const id = generateId(); // 'm1abc23-x4y5z6'
// Create branded IDs for type safety
const runId = createRunId("run_abc123");
const stepId = createStepId("step_def456");
import { parseDuration } from "@ironflow/core";
parseDuration("30s"); // 30000 (ms)
parseDuration("5m"); // 300000
parseDuration("2h"); // 7200000
parseDuration("1d"); // 86400000
import { createLogger, createNoopLogger } from "@ironflow/core";
// Create a logger with prefix
const logger = createLogger({ prefix: "[myapp]" });
logger.info("Starting...");
logger.debug("Debug info", { data: 123 });
// Create a no-op logger (disables logging)
const noopLogger = createNoopLogger();
import {
calculateBackoff,
sleep,
createDeferred,
safeJsonParse,
isObject,
deepMerge,
isRedacted, // true for the `{ $redacted: true, sha256, redactedAt }` placeholder a redaction leaves behind
createRunVersionFilter, // see RunEvent and createRunVersionFilter above
} from "@ironflow/core";

Utilities for event schema evolution:

import {
createUpcasterRegistry,
defineEvent,
createEventDefinitionRegistry,
} from "@ironflow/core";
import type { UpcasterFn, EventDefinition } from "@ironflow/core";
// Define versioned events
const orderCreatedV1 = defineEvent({ name: "order.created", version: 1 });
const orderCreatedV2 = defineEvent({
name: "order.created",
version: 2,
upcast: (data: any) => ({ ...data, address: null }),
});
// Registry applies upcasters in chain
const registry = createEventDefinitionRegistry();
registry.register(orderCreatedV1);
registry.register(orderCreatedV2);
// Upcast v1 event data to latest version
const upcasted = registry.upcastEvent("order.created", oldData, 1);
// Pass to serve() or createWorker() for automatic upcasting
// See @ironflow/node docs for serve({ eventDefinitions: registry })

PathExports
@ironflow/coreTypes, schemas, errors, constants, utilities and protocol helpers
@ironflow/core/schemasZod schemas and validation helpers only
@ironflow/core/protocolPush and WebSocket protocol types, patterns and the subscribe/resume helpers only
@ironflow/core/genGenerated protobuf and ConnectRPC code; needs the optional @bufbuild/protobuf and @connectrpc/connect packages. @ironflow/core/gen/* imports one generated file

Access generated service definitions and message schemas:

import {
// Service definitions
IronflowService,
PubSubService,
WorkerService,
// Message schemas
RunSchema as ProtoRunSchema,
EventSchema as ProtoEventSchema,
} from "@ironflow/core/gen";