@elsium-ai/workflows
Multi-step workflow pipelines and DAG execution for ElsiumAI.
Install
npm install @elsium-ai/workflows @elsium-ai/core
What’s Inside
| Category | Export | Kind |
|---|---|---|
| Types | StepConfig |
interface |
StepContext |
interface | |
StepResult |
interface | |
StepStatus |
type alias | |
RetryConfig |
interface | |
WorkflowConfig |
interface | |
WorkflowResult |
interface | |
WorkflowStatus |
type alias | |
WorkflowRunOptions |
interface | |
DagWorkflowConfig |
interface | |
DagStepConfig |
interface | |
ResumableWorkflowConfig |
interface | |
ResumableWorkflowRunOptions |
interface | |
ResumableWorkflow |
interface | |
CheckpointStore |
interface | |
WorkflowCheckpoint |
interface | |
IdempotentStepConfig |
interface | |
| Steps | step |
function |
executeStep |
function | |
| Workflow | defineWorkflow |
function |
defineParallelWorkflow |
function | |
defineBranchWorkflow |
function | |
Workflow |
interface | |
ParallelWorkflowConfig |
interface | |
BranchConfig |
interface |
Types
StepStatus
Union type representing the possible states of a step during execution.
type StepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
StepConfig<TInput, TOutput>
Configuration object that defines a single step’s behavior, including its handler, optional input validation, retry policy, conditional execution, fallback logic, and timeout.
interface StepConfig<TInput = unknown, TOutput = unknown> {
name: string
input?: z.ZodType<TInput>
handler: (input: TInput, context: StepContext) => Promise<TOutput>
retry?: RetryConfig
condition?: (input: TInput, context: StepContext) => boolean
fallback?: (error: Error, input: TInput) => Promise<TOutput>
timeoutMs?: number
}
| Field | Type | Description |
|---|---|---|
name | string | Unique identifier for the step within a workflow. |
input | z.ZodType<TInput> | Optional Zod schema used to validate the step’s input before the handler runs. |
handler | (input: TInput, context: StepContext) => Promise<TOutput> | Async function that performs the step’s work. |
retry | RetryConfig | Optional retry policy applied when the handler throws. |
condition | (input: TInput, context: StepContext) => boolean | Optional guard; when it returns false the step is skipped. |
fallback | (error: Error, input: TInput) => Promise<TOutput> | Optional async function invoked when all retries are exhausted. |
timeoutMs | number | Optional per-step timeout in milliseconds. |
StepContext
Runtime context passed to every step handler and condition function.
interface StepContext {
workflowName: string
stepIndex: number
previousOutputs: Record<string, unknown>
signal?: AbortSignal
}
| Field | Type | Description |
|---|---|---|
workflowName | string | Name of the workflow that owns this step. |
stepIndex | number | Zero-based position of the step within the workflow. |
previousOutputs | Record<string, unknown> | Map of step name to output for all previously completed steps. |
signal | AbortSignal | Optional abort signal forwarded from WorkflowRunOptions. |
StepResult<T>
Outcome returned after a step finishes (or is skipped/fails).
interface StepResult<T = unknown> {
name: string
status: StepStatus
data?: T
error?: string
durationMs: number
retryCount: number
}
| Field | Type | Description |
|---|---|---|
name | string | Name of the step that produced this result. |
status | StepStatus | Final status of the step. |
data | T | Output data, present when status is 'completed'. |
error | string | Error message, present when status is 'failed'. |
durationMs | number | Wall-clock time spent on the step in milliseconds. |
retryCount | number | Number of retries that were attempted before the final outcome. |
RetryConfig
Per-step retry policy with exponential backoff and jitter.
interface RetryConfig {
maxRetries: number
baseDelayMs?: number
maxDelayMs?: number
shouldRetry?: (error: Error) => boolean
}
| Field | Type | Default | Description |
|---|---|---|---|
maxRetries | number | — | Maximum number of retry attempts. |
baseDelayMs | number | 1000 | Base delay for exponential backoff in milliseconds. |
maxDelayMs | number | 30000 | Upper bound for the computed delay in milliseconds. |
shouldRetry | (error: Error) => boolean | — | Optional predicate; when omitted, all errors except non-retryable ElsiumError instances are retried. |
Backoff formula: min(baseDelayMs * 2^(attempt-1), maxDelayMs) * random(0.5, 1.0).
WorkflowStatus
Union type representing the possible states of a workflow.
type WorkflowStatus = 'pending' | 'running' | 'completed' | 'failed' | 'paused'
WorkflowConfig
Configuration for a sequential workflow created via defineWorkflow.
interface WorkflowConfig {
name: string
steps: StepConfig[]
onStepComplete?: (result: StepResult) => void | Promise<void>
onStepError?: (error: Error, stepName: string) => void | Promise<void>
onComplete?: (result: WorkflowResult) => void | Promise<void>
}
| Field | Type | Description |
|---|---|---|
name | string | Identifier for the workflow. |
steps | StepConfig[] | Ordered list of steps to execute sequentially. |
onStepComplete | (result: StepResult) => void | Promise<void> | Optional callback fired after each step completes. |
onStepError | (error: Error, stepName: string) => void | Promise<void> | Optional callback fired when a step fails. |
onComplete | (result: WorkflowResult) => void | Promise<void> | Optional callback fired when the workflow finishes (success or failure). |
WorkflowResult
Final output returned by workflow.run().
interface WorkflowResult {
name: string
status: WorkflowStatus
steps: StepResult[]
totalDurationMs: number
outputs: Record<string, unknown>
}
| Field | Type | Description |
|---|---|---|
name | string | Name of the workflow. |
status | WorkflowStatus | Overall status of the workflow. |
steps | StepResult[] | Results for each step in execution order. |
totalDurationMs | number | Total wall-clock time for the entire workflow in milliseconds. |
outputs | Record<string, unknown> | Map of step name to output for every completed step. |
WorkflowRunOptions
Options passed to workflow.run().
interface WorkflowRunOptions {
signal?: AbortSignal
}
| Field | Type | Description |
|---|---|---|
signal | AbortSignal | Optional abort signal; forwarded to each step’s StepContext. |
Steps
step
Factory function that creates a StepConfig by combining a name with the rest of the configuration, providing a concise shorthand for inline step definitions.
function step<TInput, TOutput>(
name: string,
config: Omit<StepConfig<TInput, TOutput>, 'name'>,
): StepConfig<TInput, TOutput>
| Parameter | Type | Description |
|---|---|---|
name | string | Unique name for the step. |
config | Omit<StepConfig<TInput, TOutput>, 'name'> | Step configuration without the name field. |
Returns: StepConfig<TInput, TOutput>
import { step } from '@elsium-ai/workflows'
import { z } from 'zod'
const fetchPage = step('fetch-page', {
input: z.object({ url: z.string().url() }),
handler: async (input) => {
const res = await fetch(input.url)
return res.text()
},
retry: { maxRetries: 3, baseDelayMs: 500 },
timeoutMs: 10_000,
})
executeStep
Runs a single step to completion, handling input validation, condition checks, retries with exponential backoff, timeout enforcement, and fallback execution.
function executeStep<TInput, TOutput>(
stepConfig: StepConfig<TInput, TOutput>,
rawInput: unknown,
context: StepContext,
): Promise<StepResult<TOutput>>
| Parameter | Type | Description |
|---|---|---|
stepConfig | StepConfig<TInput, TOutput> | The step definition to execute. |
rawInput | unknown | Raw input value; validated against stepConfig.input if a schema is provided. |
context | StepContext | Runtime context for the step. |
Returns: Promise<StepResult<TOutput>>
The execution order is:
- Validate
rawInputagainst the Zod schema (if provided). Return'failed'on validation error. - Evaluate the
conditionguard (if provided). Return'skipped'whenfalse. - Run the
handler, retrying on failure up toretry.maxRetriestimes with backoff. - On final failure, invoke
fallback(if provided). If the fallback also fails, return'failed'.
import { step, executeStep } from '@elsium-ai/workflows'
import type { StepContext } from '@elsium-ai/workflows'
const myStep = step('greet', {
handler: async (input: { name: string }) => `Hello, ${input.name}!`,
})
const context: StepContext = {
workflowName: 'demo',
stepIndex: 0,
previousOutputs: {},
}
const result = await executeStep(myStep, { name: 'World' }, context)
console.log(result.data) // "Hello, World!"
Workflow
Workflow
Interface implemented by all workflow variants (sequential, parallel, and branch).
interface Workflow {
readonly name: string
run(input: unknown, options?: WorkflowRunOptions): Promise<WorkflowResult>
}
| Member | Type | Description |
|---|---|---|
name | string (readonly) | The workflow identifier. |
run | (input: unknown, options?: WorkflowRunOptions) => Promise<WorkflowResult> | Executes the workflow with the given input and returns the aggregated result. |
defineWorkflow
Creates a sequential workflow that executes steps one after another, piping each step’s output as the next step’s input.
function defineWorkflow(config: WorkflowConfig): Workflow
| Parameter | Type | Description |
|---|---|---|
config | WorkflowConfig | Workflow configuration including steps and lifecycle callbacks. |
Returns: Workflow
When a step completes, its output is stored in outputs under the step’s name and becomes the input for the next step. If any step fails, the workflow short-circuits and returns with status: 'failed'.
import { defineWorkflow, step } from '@elsium-ai/workflows'
const pipeline = defineWorkflow({
name: 'etl-pipeline',
steps: [
step('extract', {
handler: async (input: { source: string }) => {
return await extractData(input.source)
},
}),
step('transform', {
handler: async (rawData: RawData) => {
return transformData(rawData)
},
}),
step('load', {
handler: async (transformed: TransformedData) => {
await loadData(transformed)
return { loaded: true }
},
}),
],
onStepComplete: (result) => {
console.log(`Step "${result.name}" finished in ${result.durationMs}ms`)
},
onComplete: (result) => {
console.log(`Workflow "${result.name}" ${result.status}`)
},
})
const result = await pipeline.run({ source: 'database' })
ParallelWorkflowConfig
Configuration for a parallel workflow created via defineParallelWorkflow.
interface ParallelWorkflowConfig {
name: string
steps: StepConfig[]
onComplete?: (result: WorkflowResult) => void | Promise<void>
}
| Field | Type | Description |
|---|---|---|
name | string | Identifier for the parallel workflow. |
steps | StepConfig[] | Steps to execute concurrently; all receive the same input. |
onComplete | (result: WorkflowResult) => void | Promise<void> | Optional callback fired when all steps have settled. |
defineParallelWorkflow
Creates a parallel workflow that executes all steps concurrently using Promise.all, where every step receives the same input.
function defineParallelWorkflow(config: ParallelWorkflowConfig): Workflow
| Parameter | Type | Description |
|---|---|---|
config | ParallelWorkflowConfig | Parallel workflow configuration. |
Returns: Workflow
Each step’s output is stored in outputs under its name. The workflow status is 'failed' if any step fails, 'completed' otherwise.
import { defineParallelWorkflow, step } from '@elsium-ai/workflows'
const enrichment = defineParallelWorkflow({
name: 'enrich-profile',
steps: [
step('fetch-social', {
handler: async (input: { userId: string }) => {
return await fetchSocialProfile(input.userId)
},
}),
step('fetch-activity', {
handler: async (input: { userId: string }) => {
return await fetchActivityLog(input.userId)
},
}),
step('fetch-preferences', {
handler: async (input: { userId: string }) => {
return await fetchPreferences(input.userId)
},
}),
],
})
const result = await enrichment.run({ userId: 'u_123' })
// result.outputs['fetch-social'], result.outputs['fetch-activity'], etc.
BranchConfig
Defines a single branch in a branching workflow, pairing a condition with the workflow to execute when the condition is met.
interface BranchConfig {
condition: (input: unknown) => boolean
workflow: Workflow
}
| Field | Type | Description |
|---|---|---|
condition | (input: unknown) => boolean | Predicate evaluated against the workflow input. |
workflow | Workflow | Workflow to run when condition returns true. |
defineBranchWorkflow
Creates a branching workflow that evaluates conditions in order and delegates to the first matching branch’s workflow, with an optional fallback.
function defineBranchWorkflow(
name: string,
branches: BranchConfig[],
fallback?: Workflow,
): Workflow
| Parameter | Type | Description |
|---|---|---|
name | string | Identifier for the branch workflow. |
branches | BranchConfig[] | Ordered list of condition/workflow pairs; the first match wins. |
fallback | Workflow | Optional workflow to run when no branch condition matches. |
Returns: Workflow
If no branch matches and no fallback is provided, the workflow returns immediately with status: 'completed', an empty steps array, and empty outputs.
import { defineBranchWorkflow, defineWorkflow, step } from '@elsium-ai/workflows'
const textWorkflow = defineWorkflow({
name: 'process-text',
steps: [
step('analyze-text', {
handler: async (input: { content: string }) => {
return await analyzeText(input.content)
},
}),
],
})
const imageWorkflow = defineWorkflow({
name: 'process-image',
steps: [
step('analyze-image', {
handler: async (input: { content: string }) => {
return await analyzeImage(input.content)
},
}),
],
})
const router = defineBranchWorkflow(
'content-router',
[
{ condition: (input: any) => input.type === 'text', workflow: textWorkflow },
{ condition: (input: any) => input.type === 'image', workflow: imageWorkflow },
],
)
const result = await router.run({ type: 'text', content: 'Hello world' })
Sequential Workflow
| Export | Signature | Description |
|---|---|---|
defineWorkflow |
defineWorkflow(config: WorkflowConfig): Workflow |
Define a linear step-by-step workflow |
Steps execute in order. Each step receives the previous step’s output as input.
import { step, defineWorkflow } from '@elsium-ai/workflows'
const pipeline = defineWorkflow({
name: 'data-pipeline',
steps: [
step('fetch', {
handler: async (input: { url: string }) => {
const res = await fetch(input.url)
return res.json()
},
}),
step('transform', {
handler: async (data: unknown) => {
return processData(data)
},
}),
step('save', {
handler: async (processed: unknown) => {
await db.insert(processed)
return { saved: true }
},
}),
],
onStepComplete: (result) => {
console.log(`Step ${result.name}: ${result.status} (${result.durationMs}ms)`)
},
onStepError: (error, stepName) => {
console.error(`Step ${stepName} failed:`, error.message)
},
})
const result = await pipeline.run({ url: 'https://api.example.com/data' })
Parallel Workflow
| Export | Signature | Description |
|---|---|---|
defineParallelWorkflow |
defineParallelWorkflow(config: ParallelWorkflowConfig): Workflow |
Run all steps concurrently |
All steps receive the same initial input and execute in parallel.
import { step, defineParallelWorkflow } from '@elsium-ai/workflows'
const fanOut = defineParallelWorkflow({
name: 'multi-search',
steps: [
step('search-web', {
handler: async (query: { text: string }) => searchWeb(query.text),
}),
step('search-db', {
handler: async (query: { text: string }) => searchDB(query.text),
}),
step('search-cache', {
handler: async (query: { text: string }) => searchCache(query.text),
}),
],
onComplete: (result) => {
console.log(`All searches completed in ${result.totalDurationMs}ms`)
},
})
const result = await fanOut.run({ text: 'ElsiumAI documentation' })
// result.outputs contains each step's output keyed by step name
Branch Workflow
| Export | Signature | Description |
|---|---|---|
defineBranchWorkflow |
defineBranchWorkflow(name, branches, fallback?): Workflow |
Conditional branching based on input |
Routes input to the first branch whose condition returns true. An optional fallback workflow handles unmatched inputs.
import { defineWorkflow, defineBranchWorkflow } from '@elsium-ai/workflows'
const textPipeline = defineWorkflow({ name: 'text', steps: [/* ... */] })
const imagePipeline = defineWorkflow({ name: 'image', steps: [/* ... */] })
const defaultPipeline = defineWorkflow({ name: 'default', steps: [/* ... */] })
const router = defineBranchWorkflow('content-router', [
{
condition: (input: unknown) => (input as any).type === 'text',
workflow: textPipeline,
},
{
condition: (input: unknown) => (input as any).type === 'image',
workflow: imagePipeline,
},
], defaultPipeline)
const result = await router.run({ type: 'text', data: '...' })
Step Configuration Details
Retry
Automatic retry with exponential backoff.
step('unreliable-api', {
handler: async (input) => callExternalAPI(input),
retry: {
maxRetries: 3,
baseDelayMs: 500,
maxDelayMs: 5000,
shouldRetry: (error) => error.message.includes('timeout'),
},
})
Conditional Execution
Skip a step based on input or prior context.
step('optional-enrichment', {
condition: (input, ctx) => input.needsEnrichment === true,
handler: async (input) => enrichData(input),
})
Fallback
Provide a fallback value when a step fails.
step('fetch-with-fallback', {
handler: async (input) => fetchFromPrimary(input),
fallback: async (error, input) => {
return { data: null, source: 'fallback' }
},
})
Input Validation
Validate step input with a Zod schema.
import { z } from 'zod'
step('validated-step', {
input: z.object({ url: z.string().url(), limit: z.number().positive() }),
handler: async (input) => {
// input is typed as { url: string, limit: number }
return fetch(input.url)
},
})
DAG Workflow
| Export | Signature | Description |
|---|---|---|
defineDagWorkflow |
defineDagWorkflow(config: DagWorkflowConfig): Workflow |
Run steps as a dependency graph |
Steps declare their dependencies via dependsOn. The workflow computes a topological order and executes independent steps concurrently in waves. A step receives the output of its first declared dependency as input (falling back to the workflow input when it has none). Execution stops after the first wave that contains a failed step. Unknown dependencies and cycles throw at run time.
import { step, defineDagWorkflow } from '@elsium-ai/workflows'
const build = defineDagWorkflow({
name: 'build-pipeline',
steps: [
step('compile', { handler: async (src: { files: string[] }) => compile(src) }),
{ ...step('test', { handler: async (artifact) => runTests(artifact) }), dependsOn: ['compile'] },
{ ...step('lint', { handler: async (artifact) => runLint(artifact) }), dependsOn: ['compile'] },
{ ...step('package', { handler: async (artifact) => bundle(artifact) }), dependsOn: ['test', 'lint'] },
],
onStepComplete: (result) => console.log(`${result.name}: ${result.status}`),
})
const result = await build.run({ files: ['a.ts', 'b.ts'] })
// 'compile' runs first; 'test' and 'lint' run in parallel; 'package' runs last.
DagWorkflowConfig
interface DagWorkflowConfig {
name: string
steps: DagStepConfig[]
onStepComplete?: (result: StepResult) => void | Promise<void>
onStepError?: (error: Error, stepName: string) => void | Promise<void>
onComplete?: (result: WorkflowResult) => void | Promise<void>
}
DagStepConfig
Extends StepConfig with a dependency list.
interface DagStepConfig<TInput, TOutput> extends StepConfig<TInput, TOutput> {
dependsOn?: string[] // names of steps that must complete first
}
Resumable Workflows
| Export | Signature | Description |
|---|---|---|
defineResumableWorkflow |
defineResumableWorkflow(config: ResumableWorkflowConfig): ResumableWorkflow |
Sequential workflow that checkpoints progress and can resume after a failure |
createInMemoryCheckpointStore |
createInMemoryCheckpointStore(): CheckpointStore |
In-memory checkpoint store (reference adapter) |
A resumable workflow saves a WorkflowCheckpoint before each step. If a step fails, the checkpoint is persisted with status: 'failed' and the run returns. Calling resume(workflowId) reloads the checkpoint and continues from the step that failed; resuming an already-completed workflow returns its stored result. Provide your own CheckpointStore implementation for durable persistence — the package ships only the in-memory adapter.
import { step, defineResumableWorkflow, createInMemoryCheckpointStore } from '@elsium-ai/workflows'
const store = createInMemoryCheckpointStore()
const order = defineResumableWorkflow({
name: 'process-order',
checkpointStore: store,
steps: [
step('reserve', { handler: async (o: { id: string }) => reserveStock(o) }),
step('charge', { handler: async (o) => chargeCard(o) }),
step('ship', { handler: async (o) => createShipment(o) }),
],
})
const first = await order.run({ id: 'ord-42' }, { workflowId: 'ord-42' })
if (first.status === 'failed') {
// ...fix the underlying issue, then continue from the failed step
const final = await order.resume('ord-42')
}
const checkpoint = await order.getCheckpoint('ord-42')
const all = await order.listCheckpoints()
ResumableWorkflowConfig
Extends WorkflowConfig with a checkpoint store.
interface ResumableWorkflowConfig extends WorkflowConfig {
checkpointStore: CheckpointStore
}
ResumableWorkflowRunOptions
Extends WorkflowRunOptions. Pass a workflowId to use a stable identifier (so the run can be resumed by that id); one is generated when omitted.
interface ResumableWorkflowRunOptions extends WorkflowRunOptions {
workflowId?: string // defaults to a generated id
}
ResumableWorkflow
interface ResumableWorkflow {
readonly name: string
run(input: unknown, options?: ResumableWorkflowRunOptions): Promise<WorkflowResult>
resume(workflowId: string, options?: WorkflowRunOptions): Promise<WorkflowResult>
getCheckpoint(workflowId: string): Promise<WorkflowCheckpoint | null>
listCheckpoints(): Promise<WorkflowCheckpoint[]>
}
CheckpointStore
The port a backend must implement to persist checkpoints.
interface CheckpointStore {
save(checkpoint: WorkflowCheckpoint): Promise<void>
load(workflowId: string): Promise<WorkflowCheckpoint | null>
delete(workflowId: string): Promise<void>
list(workflowName?: string): Promise<WorkflowCheckpoint[]>
}
WorkflowCheckpoint
interface WorkflowCheckpoint {
workflowId: string
workflowName: string
status: 'running' | 'completed' | 'failed' | 'paused'
input: unknown
currentStepIndex: number
stepResults: StepResult[]
outputs: Record<string, unknown>
createdAt: number
updatedAt: number
}
Idempotent Steps
Step-level deduplication for side-effectful workflows (POST to an external API, DB writes, sending email). When a workflow is resumed after a crash, an idempotent step that already ran is served from the store instead of re-executing. The package ships only the in-memory adapter; implement IdempotentCheckpointStore against your backend for durability.
| Export | Signature | Description |
|---|---|---|
createInMemoryIdempotentCheckpointStore |
createInMemoryIdempotentCheckpointStore(): IdempotentCheckpointStore |
In-memory store extending CheckpointStore with per-step records |
executeIdempotentStep |
executeIdempotentStep<TInput, TOutput>(args): Promise<StepResult<TOutput>> |
Run a step, returning the cached result if one exists for its idempotency key |
resolveIdempotencyKey |
resolveIdempotencyKey<TInput>(step, input): Promise<string | null> |
Compute a step’s idempotency key, or null when the step is not idempotent |
defaultIdempotencyKey |
defaultIdempotencyKey(input): Promise<string> |
Default key: a stable SHA-256 over the input JSON |
A step opts in by setting idempotent: true (see IdempotentStepConfig). executeIdempotentStep resolves the key; if the step is not idempotent it falls through to a normal executeStep. Otherwise it checks the store for an existing record under (workflowId, stepName, idempotencyKey) and returns it as a StepResult (with durationMs: 0) on a hit, or runs the step and records the outcome on a miss.
import {
step,
executeIdempotentStep,
createInMemoryIdempotentCheckpointStore,
} from '@elsium-ai/workflows'
const store = createInMemoryIdempotentCheckpointStore()
const charge: IdempotentStepConfig<{ orderId: string }, { receiptId: string }> = {
...step('charge', { handler: async (o) => chargeCard(o) }),
idempotent: true,
idempotencyKey: (o) => o.orderId, // optional; defaults to a hash of the input
}
const result = await executeIdempotentStep({
workflowId: 'ord-42',
step: charge,
input: { orderId: 'ord-42' },
context: { workflowName: 'process-order', stepIndex: 1, previousOutputs: {} },
store,
})
// A second call with the same input returns the recorded result without re-charging.
IdempotentStepConfig
Extends StepConfig with idempotency opt-in.
interface IdempotentStepConfig<TInput, TOutput> extends StepConfig<TInput, TOutput> {
readonly idempotent?: boolean
readonly idempotencyKey?: (input: TInput) => string // defaults to defaultIdempotencyKey
}
Part of ElsiumAI
This package is the workflow orchestration layer of the ElsiumAI framework. See the Getting Started guide or the Fundamentals for comprehensive coverage of every feature.