Skip to content

Nodes and Edges ​

This section covers the core types and classes for defining the logic of your workflow tasks and the connections between them.

NodeDefinition Interface ​

This is the serializable representation of a node within a WorkflowBlueprint.

typescript
interface NodeDefinition {
	id: string
	uses: string // Key that resolves to an implementation in a registry.
	params?: Record<string, any>
	inputs?: string | Record<string, string>
	config?: NodeConfig
}

Built-in Node Types ​

Flowcraft provides several built-in node types for common patterns. Each is available as a string key for raw blueprints, and as an exported class for use with the builder API (import from 'flowcraft'):

ClassString KeyDescription
SubflowNodesubflowExecutes a nested workflow.
WaitNodewaitPauses workflow execution for external input (human-in-the-loop).
SleepNodesleepPauses execution for a specified duration.
WebhookNodewebhookListens for an external HTTP request to resume execution.
BatchScatterNodebatch-scatterSplits an array for parallel processing.
BatchGatherNodebatch-gatherCollects results from parallel workers.
(function)loop-controllerManages iterative loops (created automatically by .loop()).

Example using the builder API:

typescript
import { createFlow, SubflowNode } from 'flowcraft'

const flow = createFlow('my-flow')
	.node('start', startNode)
	.node('run-subflow', SubflowNode, {
		params: {
			blueprintId: 'child-flow',
			inputs: { data: 'startData' },
		},
	})
	.edge('start', 'run-subflow')

EdgeDefinition Interface ​

Defines the connection and data flow between two nodes.

typescript
interface EdgeDefinition {
	source: string
	target: string
	action?: string // An 'action' from the source node that triggers this edge.
	condition?: string // A condition that must be met for this edge to be taken.
	transform?: string // A string expression to transform the data before passing it to the target node.
}

NodeConfig Interface ​

Configuration for a node's resiliency and execution behavior.

typescript
interface NodeConfig {
	maxRetries?: number // Number of retries on failure (default: 0)
	retryDelay?: number // Delay in ms between retries (default: 1000)
	timeout?: number // Timeout in ms for the node execution
	fallback?: string // ID of a fallback node to run on failure
	joinStrategy?: 'all' | 'any' // For nodes with multiple inputs: wait for all or any
}

Example usage:

typescript
.node('flaky-api', async ({ input }) => {
  // Some API call that might fail
  return { output: await callExternalAPI(input) }
}, {
  config: {
    maxRetries: 3,
    retryDelay: 2000,
    timeout: 5000,
    fallback: 'fallback-node',
    joinStrategy: 'any'
  }
})

NodeResult Interface ​

The required return type for any node implementation.

typescript
interface NodeResult<TOutput = any, TAction extends string = string> {
	output?: TOutput
	action?: TAction // For conditional branching.
	error?: { message: string; [key: string]: any }
	dynamicNodes?: NodeDefinition[] // For dynamically scheduling new nodes.
	_fallbackExecuted?: boolean // Internal flag: Indicates that this result came from a fallback execution.
}

NodeContext Interface ​

The context object passed to every node's execution logic.

typescript
interface NodeContext<
	TContext extends Record<string, any> = Record<string, any>,
	TDependencies extends RuntimeDependencies = RuntimeDependencies,
	TInput = any,
> {
	/** The async-only interface for interacting with the workflow's state. */
	context: IAsyncContext<TContext>
	/** The primary input data for this node, typically from its predecessor. */
	input?: TInput
	/** Static parameters defined in the blueprint. */
	params: Record<string, any>
	/** Shared, runtime-level dependencies (e.g., database clients, loggers). */
	dependencies: TDependencies & {
		runtime: ExecutionContext<TContext, TDependencies>
		workflowState: WorkflowState<TContext>
	}
	/** A signal to gracefully cancel long-running node operations. */
	signal?: AbortSignal
}

NodeFunction Type ​

A simple, function-based node implementation.

typescript
type NodeFunction<
	TContext = Record<string, any>,
	TDependencies = RuntimeDependencies,
	TInput = any,
	TOutput = any,
	TAction extends string = string,
> = (context: NodeContext<TContext, TDependencies, TInput>) => Promise<NodeResult<TOutput, TAction>>

NodeClass Type ​

Represents a constructor for any concrete class that extends the abstract BaseNode.

typescript
type NodeClass<
	TContext = Record<string, any>,
	TDependencies = RuntimeDependencies,
	TInput = any,
	TOutput = any,
	TAction extends string = string,
> = new (
	params?: Record<string, any>,
	nodeId?: string,
) => BaseNode<TContext, TDependencies, TInput, TOutput, TAction>

isNodeClass Function ​

A type guard to reliably distinguish a NodeClass from a NodeFunction.

typescript
function isNodeClass(impl: any): impl is NodeClass {
	return typeof impl === 'function' && !!impl.prototype?.exec
}

This is useful when you need to check if a node implementation is a class-based one, for example, when registering nodes dynamically.

BaseNode Abstract Class ​

A structured, class-based node for complex logic with a safe, granular lifecycle.

typescript
abstract class BaseNode<
	TContext = Record<string, any>,
	TDependencies = RuntimeDependencies,
	TInput = any,
	TOutput = any,
	TAction extends string = string,
> {
	constructor(params: Record<string, any>) {
		// Initialize with params
	}

	async prep(context: NodeContext<TContext, TDependencies, TInput>): Promise<any> {
		// Prepare data
		return await context.context.get('someData')
	}

	abstract async exec(
		prepResult: any,
		context: NodeContext<TContext, TDependencies, TInput>,
	): Promise<Omit<NodeResult<TOutput, TAction>, 'error'>>

	async post(
		execResult: Omit<NodeResult<TOutput, TAction>, 'error'>,
		context: NodeContext<TContext, TDependencies, TInput>,
	): Promise<NodeResult<TOutput, TAction>> {
		// Process result
		await context.context.set('result', execResult.output)
		return execResult
	}

	async fallback(
		error: Error,
		context: NodeContext<TContext, TDependencies, TInput>,
	): Promise<Omit<NodeResult<TOutput, TAction>, 'error'>> {
		// Fallback logic
		return { output: 'Fallback result' }
	}

	async recover(
		error: Error,
		context: NodeContext<TContext, TDependencies, TInput>,
	): Promise<void> {
		// Cleanup
	}
}

Example implementation:

typescript
class MyNode extends BaseNode {
	async prep(context) {
		return await context.context.get('userId')
	}

	async exec(userId, context) {
		const user = await fetchUser(userId)
		return { output: user, action: 'success' }
	}

	async post(execResult, context) {
		await context.context.set('processedUser', execResult.output)
		return execResult
	}
}

Released under the MIT License