Skip to content

@ironflow/browser

Browser client for Ironflow. Provides real-time subscriptions, workflow triggers, and event emission for web applications.

The package is public on npm and installs without authentication. For building from source, see the Local Development guide.

Terminal window
npm install @ironflow/browser
import { ironflow } from '@ironflow/browser';
// Configure once at app startup
ironflow.configure({
serverUrl: 'http://localhost:9123',
});
// Subscribe to events
const sub = await ironflow.subscribe('events:order.*', {
onEvent: (event) => console.log('Order:', event),
});
// Invoke a function
const run = await ironflow.invoke('process-order', {
data: { orderId: '123' },
});
// Emit events
await ironflow.emit('order.approved', { orderId: '123' }, { version: 1 });
// Cleanup
sub.unsubscribe();

Configure the singleton client. Call once at app startup.

ironflow.configure({
// Server URL (required)
serverUrl: 'https://ironflow.example.com',
// Transport: 'connectrpc' (default) or 'websocket'
transport: 'connectrpc',
// Authentication (optional for local development)
// Use the environment API key from the Ironflow dashboard
auth: {
apiKey: 'your-environment-api-key',
// or
token: 'your-jwt-token',
},
// Reconnection settings
reconnect: {
enabled: true, // Enable auto-reconnect (default: true)
maxAttempts: 10, // Max reconnection attempts (default: 10)
backoff: {
initial: 1000, // Initial delay in ms (default: 1000)
max: 30000, // Max delay in ms (default: 30000)
multiplier: 2, // Backoff multiplier (default: 2)
},
},
// Tab visibility handling
visibility: {
pauseOnHidden: true, // Pause subscriptions when tab hidden (default: true)
reconnectOnVisible: true, // Reconnect when tab visible (default: true)
},
// Custom logger (optional)
logger: console, // or false to disable logging
// Request timeout in ms (optional)
timeout: 30000,
});

Manually connect to the server.

await ironflow.connect();

Disconnect from the server.

ironflow.disconnect();

Listen for connection state changes.

const unsubscribe = ironflow.onConnectionChange((state) => {
// state: 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
console.log('Connection state:', state);
});
// Stop listening
unsubscribe();

Emit an event.

// Basic emit
await ironflow.emit('order.approved', {
orderId: '123',
approvedBy: 'user@example.com',
});
// With options
await ironflow.emit('order.approved', { orderId: '123' }, {
version: 2, // Event schema version (default: 1)
idempotencyKey: 'unique-key', // Deduplication key
});

Emit an event and wait for the triggered run to complete. Calls TriggerSync, which blocks until the run finishes or the timeout elapses.

Throws RunFailedError if the run fails, RunCancelledError if it is cancelled.

const result = await ironflow.emitSync('order.placed', { orderId: '123' });
console.log(result.output); // Run output
console.log(result.runId); // Run ID
console.log(result.functionId); // Function that was triggered
console.log(result.status); // 'completed'
console.log(result.durationMs); // Wall-clock duration
// With custom timeout (default: 30 000 ms)
const resultWithTimeout = await ironflow.emitSync('order.placed', { orderId: '123' }, {
timeout: 60000,
});

Invoke a function by function ID.

// Basic invoke
const run = await ironflow.invoke('process-order', {
data: { orderId: '123' },
});
console.log(run.runIds); // IDs of created runs
console.log(run.eventId); // ID of the stored event
// Type-safe invoke (TInput — the input payload)
interface Input { orderId: string }
const run = await ironflow.invoke<Input>('process-order', {
data: { orderId: '123' },
});

Get run status.

const run = await ironflow.getRun('run_abc123');
console.log(run.status); // Run status
console.log(run.attempt); // Current attempt number
console.log(run.output); // Output (if completed)
console.log(run.error); // Error (if failed)
console.log(run.startedAt); // Start time (if started)
console.log(run.endedAt); // End time (if finished)

List runs with filters.

const runs = await ironflow.listRuns({
functionId: 'process-order', // Filter by function
status: 'running', // Filter by status
limit: 50, // Max results
cursor: 'abc123', // Pagination cursor
});
console.log(runs.runs); // Array of runs
console.log(runs.nextCursor); // Next page cursor

Cancel a running workflow.

await ironflow.cancelRun('run_abc123');

Pause running workflows, inspect step outputs, inject modifications, and resume:

// Pause at next step boundary
await ironflow.pauseRun("run_abc123");
// Get paused state with completed steps
const state = await ironflow.getPausedState("run_abc123");
for (const step of state.steps) {
console.log(step.name, step.output, step.injected);
}
// Inject modified output
const result = await ironflow.injectStepOutput(
"run_abc123",
"step_xyz",
{ corrected: true },
"Fix calculation"
);
// Resume with injected data
await ironflow.resumeRun("run_abc123");

Inspect historical run state at any timestamp. Requires recording: true on the function.

// Get run state at a specific point in time
const snapshot = await ironflow.getRunStateAt("run_abc123", "2024-01-15T10:30:00Z");
console.log("Status at that time:", snapshot.status);
for (const step of snapshot.steps) {
console.log(step.name, step.status, step.patched);
}
// Get full timeline of audit events for a run
const events = await ironflow.getRunTimeline("run_abc123");
for (const evt of events) {
console.log(evt.timestamp, evt.eventType, evt.summary, evt.significant);
}
// Get a specific step's output at a point in time
const stepOutput = await ironflow.getStepOutputAt(
"run_abc123", "step_xyz", "2024-01-15T10:30:00Z"
);
console.log(stepOutput.output, stepOutput.patched, stepOutput.injected);

Get the current state of a projection.

const result = await ironflow.getProjection<OrderStats>('order-stats');
console.log(result.state); // Typed projection state
console.log(result.name); // Projection name
console.log(result.partition); // Partition key (or '__global__')
console.log(result.version); // Event sequence version
console.log(result.lastEventId); // ID of the last processed event
console.log(result.lastEventTime); // Timestamp of the last processed event (Date)
console.log(result.mode); // 'managed' | 'external'
// With partition
const resultWithPartition = await ironflow.getProjection('order-stats', {
partition: 'customer-123',
});

List all registered projections with their operational status.

const projections = await ironflow.listProjections();
for (const p of projections) {
console.log(p.name, p.status, p.mode, p.lag);
}

Get the operational status of a single projection.

const status = await ironflow.getProjectionStatus('order-stats');
console.log(status.status); // 'active' | 'rebuilding' | 'paused' | 'error'
console.log(status.mode); // 'managed' | 'external'
console.log(status.lastEventSeq); // Last processed event sequence number
console.log(status.lag); // Number of unprocessed events
console.log(status.errorMessage); // Error message (if status === 'error')
console.log(status.updatedAt); // Timestamp of last status update (Date)

Trigger a full rebuild of a projection from the beginning of the event stream.

const result = await ironflow.rebuildProjection('order-stats');
console.log(result.status); // e.g. 'rebuilding'
// With options
await ironflow.rebuildProjection('order-stats', {
partition: 'customer-123', // Rebuild a specific partition only
fromEventId: 'evt-abc', // Start replay from this event ID
dryRun: true, // Validate without making changes
});

Subscribe to real-time state updates for a projection.

const sub = await ironflow.subscribeToProjection<OrderStats>('order-stats', {
onUpdate: (state, event) => {
console.log('New state:', state);
console.log('Triggered by event:', event.id, event.name);
},
onError: (error) => console.error(error),
});
// With partition and replay
const subWithPartition = await ironflow.subscribeToProjection('order-stats', {
onUpdate: (state) => console.log(state),
}, {
partition: 'customer-123',
replay: 1, // Replay the latest update on connect
});
// Cleanup
sub.unsubscribe();

Query a SQL-backed projection table with optional filtering, ordering, and pagination.

const result = await ironflow.querySQLProjection('board', {
where: "status = 'OPEN'",
orderBy: 'title ASC',
limit: 50,
offset: 0,
});
console.log(result.columns); // Column names
console.log(result.rows); // Array of string[] rows
console.log(result.totalCount); // Total matching rows (before limit)

Subscribe to events matching a pattern.

// Basic subscription
const sub = await ironflow.subscribe('events:order.*', {
onEvent: (event) => console.log(event),
onError: (error) => console.error(error),
onStateChange: (state) => console.log(state),
});
// Type-safe subscription
interface OrderEvent {
orderId: string;
amount: number;
}
const sub = await ironflow.subscribe<OrderEvent>('events:order.*', {
onEvent: (event) => {
// event.data is typed as OrderEvent
console.log(event.data.orderId);
},
});
// With options
const sub = await ironflow.subscribe('events:*', {
onEvent: (e) => console.log(e),
replay: 10, // Replay last N events on connect
trackState: true, // Enable .lastEvent access
filter: 'event.data.amount > 100', // CEL filter expression
backpressure: 'buffer', // 'buffer' (default) | 'drop' | 'block'
});
// Access last event (when trackState is true)
console.log(sub.lastEvent);
// Unsubscribe
sub.unsubscribe();
PatternDescription
events:order.*All order events
events:order.createdSpecific event
events:*All events
system.run.*All run updates
system.run.{runId}.*Specific run updates
system.run.{runId}.step.{stepId}Specific step updates
const sub = await ironflow.subscribe(
['system.run.*', 'events:order.*', 'events:payment.*'],
{
onEvent: (event) => console.log(event),
}
);
import type { AckableSubscription } from '@ironflow/core';
const sub = await ironflow.subscribe('events:*', {
onEvent: async (event) => {
if (!event.eventId) return;
try {
await processEvent(event);
await sub.ack(event.eventId); // Acknowledge
} catch {
await sub.nak(event.eventId); // Negative ack - redeliver
}
},
ackMode: 'manual',
}) as AckableSubscription;

Manage multiple subscriptions together.

const group = ironflow.subscriptionGroup();
group.add('system.run.abc123.*', {
onEvent: (e) => console.log('Run:', e),
});
group.add('events:payment.*', {
onEvent: (e) => console.log('Payment:', e),
});
// Unsubscribe all at once
group.unsubscribeAll();

Get a KV client for bucket management and key operations:

const kv = ironflow.kv();
const info = await kv.createBucket({
name: "sessions",
description: "User session data",
ttlSeconds: 3600,
maxValueSize: 65536,
history: 3,
});
await kv.deleteBucket("sessions");
const buckets = await kv.listBuckets();
for (const b of buckets) {
console.log(`${b.name}: ${b.values} keys, ${b.bytes} bytes`);
}
const info = await kv.getBucketInfo("sessions");
// info.name, info.values, info.bytes, info.history, info.created_at

Get a bucket handle for key operations:

const bucket = kv.bucket("sessions");
const entry = await bucket.get("user:123");
// entry.key, entry.value, entry.revision, entry.created_at, entry.operation

Unconditional write. Returns the new revision:

const { revision } = await bucket.put("user:123", { name: "Alice" });

Write only if the key does not exist. Throws on conflict (HTTP 412):

const { revision } = await bucket.create("user:123", { name: "Alice" });

Write only if the revision matches. Throws on mismatch (HTTP 412):

const { revision } = await bucket.update("user:123", newValue, entry.revision);

Soft-delete (tombstone):

await bucket.delete("user:123");

Permanently remove key and all history:

await bucket.purge("user:123");

List keys with optional wildcard filter:

const keys = await bucket.listKeys("user.*");
// Pass no argument for all keys

Watch for real-time key changes via WebSocket (browser SDK only):

const watcher = bucket.watch({
onUpdate: (event) => {
console.log(`${event.key} ${event.operation}: rev ${event.revision}`);
},
onError: (err) => console.error(err),
onClose: () => console.log("Watch ended"),
}, { key: "user.*" });
// Stop watching
watcher.stop();

See the KV Store Guide for detailed usage patterns.


Get a config client for environment-scoped configuration:

const config = ironflow.configManager();

Full replacement of a config document:

const { revision } = await config.set("app-settings", {
theme: "dark",
maxRetries: 3,
});
const entry = await config.get("app-settings");
// entry.name, entry.data, entry.revision, entry.updatedAt

Shallow merge — only specified keys are updated:

const { revision } = await config.patch("app-settings", {
maxRetries: 5,
timeout: 30,
});
const configs = await config.list();
for (const entry of configs) {
console.log(`${entry.name}: rev ${entry.revision}`);
}

Delete a config. Idempotent — succeeds silently if the config does not exist.

await config.delete("app-settings");

Watch for real-time config changes via WebSocket:

const watcher = await config.watch("feature-flags", {
onUpdate: (event) => {
// event: ConfigWatchEvent ({ type: "config_update", name, data, revision, updatedAt })
console.log("Config updated:", event.data);
console.log("New revision:", event.revision);
},
onError: (err) => console.error("Watch error:", err),
});
// `watcher` is a Subscription — call unsubscribe() to stop watching.
watcher.unsubscribe();

See the Config Management Guide for detailed usage patterns.


Appends an event to an entity stream.

const result = await ironflow.streams.append("order-123", {
name: "order.item_added",
data: { itemId: "item-789" },
entityType: "order",
}, { expectedVersion: 4 });
// result.entityVersion = 5
// result.eventId = "evt-..."

Reads events from an entity stream.

const { events, totalCount } = await ironflow.streams.read("order-123", {
fromVersion: 1,
limit: 50,
direction: "forward",
});

Returns stream metadata, or null if no events have been written to this stream yet.

const info = await ironflow.streams.getInfo("order-123");
// info.entityId, info.entityType, info.version, info.eventCount
// info is null for streams with no events — safe to pass expectedVersion: 0 to append()

Returns: Promise<StreamInfo | null>

Save a materialized state snapshot at a specific stream version. Snapshots enable efficient rebuilds by avoiding replaying all events from the beginning.

const { snapshotId } = await ironflow.streams.createSnapshot("order-123", {
entityType: "order",
entityVersion: 42, // The stream version this snapshot represents
state: { status: "OPEN", total: 99.99 }, // The materialized state
});
console.log(snapshotId); // Unique snapshot identifier

Retrieve the latest snapshot at or before a given stream version.

const snapshot = await ironflow.streams.getSnapshot("order-123");
// Optionally limit to snapshots before a specific version:
const snapshotBeforeVersion = await ironflow.streams.getSnapshot("order-123", {
beforeVersion: 50,
});
console.log(snapshot.snapshotId); // Snapshot identifier
console.log(snapshot.entityId); // Entity ID
console.log(snapshot.entityType); // Entity type
console.log(snapshot.entityVersion); // Stream version this snapshot represents
console.log(snapshot.state); // The materialized state object
console.log(snapshot.createdAt); // ISO 8601 creation timestamp

Join a consumer group for load-balanced event processing.

const sub = await ironflow.joinConsumerGroup(
'order-processors', // Group name
'events:order.*', // Pattern
{
onEvent: async (event) => {
try {
await processOrder(event);
await sub.ack(event.eventId!);
} catch {
await sub.nak(event.eventId!, 5000); // redeliver after 5s
}
},
}
);

List all registered functions in the current environment.

const functions = await ironflow.listFunctions();
for (const fn of functions) {
console.log(fn.id, fn.name);
}

List connected pull-mode workers.

const workers = await ironflow.listWorkers();
for (const worker of workers) {
console.log(worker.id, worker.status);
}

Get the audit trail for a specific run, including all state transitions and step events.

const result = await ironflow.getAuditTrail('run-abc123');
for (const event of result.events) {
console.log(event.eventType, event.createdAt);
console.log(event.stepId, event.payload);
}
console.log(result.totalCount);
console.log(result.nextCursor); // Pagination cursor
// With filters
const result = await ironflow.getAuditTrail('run-abc123', {
eventType: 'step.completed',
fromTimestamp: '2024-01-01T00:00:00Z',
toTimestamp: '2024-02-01T00:00:00Z',
limit: 100,
cursor: result.nextCursor,
});

Each AuditEvent has:

  • id — Unique event ID
  • runId — Associated run ID
  • functionId — Function that owns the run
  • stepId — Step ID (if step-level event)
  • eventType — Event type string (e.g. 'step.completed', 'run.failed')
  • payload — Event-specific data
  • metadata — Optional string key-value metadata
  • createdAt — ISO 8601 timestamp

Manage event schemas and test upcast transformations. Requires appropriate API key permissions.

Register a new event schema or a new version of an existing one.

const schema = await ironflow.schemas.register({
name: 'order.placed',
version: 2,
schema: {
type: 'object',
properties: {
orderId: { type: 'string' },
totalCents: { type: 'integer' },
},
required: ['orderId', 'totalCents'],
},
});

List all registered event schemas.

const schemas = await ironflow.schemas.list();
for (const s of schemas) {
console.log(s.name, s.version);
}

Get the latest version of an event schema by name.

const schema = await ironflow.schemas.get('order.placed');
console.log(schema.version, schema.schema);

Get a specific version of an event schema.

const schema = await ironflow.schemas.getVersion('order.placed', 1);

Delete a specific version of an event schema.

await ironflow.schemas.delete('order.placed', 1);

Test an upcast transformation from one schema version to another without persisting anything.

const result = await ironflow.schemas.testUpcast({
eventName: 'order.placed',
fromVersion: 1,
toVersion: 2,
data: { orderId: '123', total: 99.99 },
});
console.log(result.output); // Transformed event data

Manage webhook sources and inspect delivery history. Requires appropriate API key permissions.

Register a new webhook source.

const source = await ironflow.webhooks.create({
id: 'stripe', // Unique source identifier
eventPrefix: 'stripe', // Events will be emitted as 'stripe.*'
verifyHeader: 'stripe-signature', // Header containing the signature
verifyAlgorithm: 'hmac-sha256', // Signature algorithm
verifySecret: 'whsec_...', // Webhook signing secret
metadata: { env: 'production' }, // Optional metadata
});

List all registered webhook sources.

const sources = await ironflow.webhooks.listSources();
for (const s of sources) {
console.log(s.id, s.eventPrefix, s.sourceType);
}

Delete a webhook source by ID.

await ironflow.webhooks.deleteSource('stripe');

List webhook delivery records with optional filtering.

const { deliveries, totalCount } = await ironflow.webhooks.listDeliveries({
sourceId: 'stripe', // Filter by source
status: 'failed', // 'pending' | 'delivered' | 'failed'
limit: 50,
offset: 0,
});
for (const d of deliveries) {
console.log(d.id, d.sourceId, d.status, d.error);
}

Manage API keys for the current environment. Requires admin permissions.

const key = await ironflow.apiKeys.create({ name: 'ci-runner' });
console.log(key.key); // The raw API key — shown only once
console.log(key.id); // Key ID for future operations
const keys = await ironflow.apiKeys.list();
for (const k of keys) {
console.log(k.id, k.name, k.createdAt);
}
const key = await ironflow.apiKeys.get('ak_abc123');
await ironflow.apiKeys.delete('ak_abc123');

Rotate an existing key, revoking the old secret and issuing a new one.

const rotated = await ironflow.apiKeys.rotate('ak_abc123');
console.log(rotated.key); // New raw secret — shown only once

Manage organizations. Requires admin permissions. Enterprise feature.

const org = await ironflow.orgs.create({ name: 'Acme Corp' });
const orgs = await ironflow.orgs.list();
const org = await ironflow.orgs.get('org_abc123');
const org = await ironflow.orgs.update('org_abc123', { name: 'Acme Inc' });
await ironflow.orgs.delete('org_abc123');

Manage RBAC roles. Requires admin permissions. Enterprise feature.

const role = await ironflow.roles.create({
name: 'editor',
org_id: 'org_abc123',
});
const roles = await ironflow.roles.list(); // All roles
const orgRoles = await ironflow.roles.list('org_abc123'); // Scoped to org
const role = await ironflow.roles.get('role_abc123');
const role = await ironflow.roles.update('role_abc123', { name: 'reviewer' });
await ironflow.roles.delete('role_abc123');

Attach a policy to a role.

await ironflow.roles.assignPolicy('role_abc123', 'policy_xyz');

Detach a policy from a role.

await ironflow.roles.removePolicy('role_abc123', 'policy_xyz');

Manage authorization policies. Requires admin permissions. Enterprise feature.

const policy = await ironflow.policies.create({
name: 'allow-read',
effect: 'allow',
actions: 'read',
resources: '*',
org_id: 'org_abc123',
});
const policies = await ironflow.policies.list(); // All policies
const orgPolicies = await ironflow.policies.list('org_abc123'); // Scoped to org
const policy = await ironflow.policies.get('policy_abc123');
const policy = await ironflow.policies.update('policy_abc123', {
actions: 'read,write',
});
await ironflow.policies.delete('policy_abc123');

Manage dashboard users. Requires admin permissions. Enterprise feature.

const user = await ironflow.users.create({
email: 'alice@example.com',
password: 'secret',
roles: ['admin'],
});
const users = await ironflow.users.list();
const user = await ironflow.users.get('user_abc123');
const user = await ironflow.users.update('user_abc123', { name: 'Alice' });
await ironflow.users.delete('user_abc123');

List tenants. Enterprise feature.

const tenants = await ironflow.tenants.list();
for (const t of tenants) {
console.log(t.id, t.name);
}

Auto-detect the best transport.

const transport = await ironflow.detectTransport();
// Returns 'connectrpc' | 'websocket'

// Error types originate from @ironflow/core (also re-exported by @ironflow/browser)
import {
IronflowError,
ConnectionError,
SubscriptionError,
isRetryable
} from '@ironflow/core';
ironflow.subscribe('events:*', {
onEvent: (e) => console.log(e),
onError: (error) => {
if (error instanceof ConnectionError) {
console.log('Connection lost, will auto-reconnect');
} else if (error instanceof SubscriptionError) {
console.log('Subscription error:', error.message);
}
if (isRetryable(error)) {
// Error is transient, will be retried
}
},
});
// Global error handler
ironflow.onError((error) => {
reportToSentry(error);
});

Example custom hook for subscriptions:

import { useState, useEffect } from 'react';
import { ironflow, IronflowEvent } from '@ironflow/browser';
function useSubscription<T>(pattern: string) {
const [events, setEvents] = useState<IronflowEvent<T>[]>([]);
useEffect(() => {
const subPromise = ironflow.subscribe<T>(pattern, {
onEvent: (event) => setEvents(prev => [...prev, event]),
});
return () => {
subPromise.then((sub) => sub.unsubscribe());
};
}, [pattern]);
return events;
}
// Usage
function OrderList() {
const orders = useSubscription<OrderData>('events:order.*');
return (
<ul>
{orders.map(order => (
<li key={order.id}>{order.data.orderId}</li>
))}
</ul>
);
}
function useConnectionStatus() {
const [status, setStatus] = useState<ConnectionState>('disconnected');
useEffect(() => {
const unsubscribe = ironflow.onConnectionChange(setStatus);
return unsubscribe;
}, []);
return status;
}

  • Chrome 80+
  • Firefox 75+
  • Safari 13.1+
  • Edge 80+