Core Concepts
This page explains the fundamental concepts of Omega Flow workflows.
What is a Workflow?
A workflow is a directed graph that defines a sequence of steps executed in response to events. Each workflow consists of:
- Nodes - Individual steps (trigger, action, condition, etc.)
- Edges - Connections that define the flow between nodes
- Options - Configuration like execution frequency
interface Workflow {
id: string;
name: string;
flow: {
nodes: Node[];
edges: Edge[];
};
options: WorkflowOptions;
}Nodes
Nodes are the building blocks of workflows. A node in a saved workflow is just a small JSON object — an id, a type string, a data payload, and a position used by the visual editor to place the node on the canvas:
{
id: "trigger-1",
type: "Trigger",
data: { params: { event: "user.signup" } },
position: { x: 250, y: 0 }
}position is purely a visual concern — the engine ignores it. The editor reads and writes it as you drag nodes around.
The type string is what binds that object to runtime behaviour. Omega Flow looks up type in two registries — one in the engine, one in the editor — and a complete node implementation has an entry in both.
NodeModel — execution side (engine)
A NodeModel is a class in @omega-flow/engine that implements how a node behaves when an event arrives. Every node type subclasses NodeModel and implements two methods:
acceptEvent(event)— Returnstruewhen the node accepts the event and processing is complete; the engine then callsnextNode()to move forward. Returnsfalsewhen the node does not accept this event — the workflow stays on the current node and waits for another.nextNode(event)— called only afteracceptEventreturnedtrue. Returns the nextNodeModel, ornullto end the workflow.
The engine ships built-in models for Trigger, Action, Condition, Wait, TriggerOrTimeout, and Exit, exported as the defaultNodeModels map. You pass that map (optionally extended with your own) to WorkflowManager, which uses it to instantiate nodes when running a workflow:
import { WorkflowManager, defaultNodeModels } from "@omega-flow/engine";
import HttpRequestNode from "./nodes/HttpRequestNode";
const manager = new WorkflowManager({
// ...stores, memory, scheduler...
nodeModels: { ...defaultNodeModels, HttpRequest: HttpRequestNode },
});See Custom Nodes (Engine) for the full guide.
NodeTypeDefinition — visual side (editor)
A NodeTypeDefinition is the editor-side counterpart. It declares how the node looks and how it is configured:
interface NodeTypeDefinition {
type: string; // matches the engine NodeModel
label: string; // shown in the NodesPanel
description?: string;
Icon?: ComponentType<{ size?: number }>;
defaultData: Record<string, unknown>; // initial data on drop
ViewComponent: ComponentType<NodeViewProps>; // canvas rendering + handles
DetailComponent: ComponentType<NodeDetailProps>; // properties panel form
}@omega-flow/editor ships definitions for the same six built-in types as defaultNodeTypes. You pass them to WorkflowEditor, optionally combined with your own — mergeNodeTypes dedupes by type, which also lets you override a built-in:
import {
WorkflowEditor,
defaultNodeTypes,
mergeNodeTypes,
} from "@omega-flow/editor";
<WorkflowEditor nodeTypes={mergeNodeTypes(defaultNodeTypes, [sendEmailNodeType])}>
{/* ... */}
</WorkflowEditor>See Custom Nodes (Editor) for the full guide.
useNodeRegistry — bridging to ReactFlow
The useNodeRegistry() hook exposes the registered NodeTypeDefinitions and returns reactFlowNodeTypes — the { [type]: ViewComponent } map ReactFlow expects:
const { reactFlowNodeTypes } = useNodeRegistry();
return <ReactFlow nodeTypes={reactFlowNodeTypes} /* ... */ />;Extending Omega Flow
Adding a new node type means creating both halves and giving them the same type string:
| Side | What you create | Where it goes |
|---|---|---|
| Engine | A NodeModel subclass | Pass via nodeModels to WorkflowManager |
| Editor | A NodeTypeDefinition (with View + Detail components) | Pass via nodeTypes to WorkflowEditor |
The engine half drives execution; the editor half drives the visual representation. Either can be used independently — a server-only setup needs no NodeTypeDefinition, and a read-only viewer can render workflows without ever instantiating a NodeModel — but a fully editable, runnable node needs both.
Built-in node types
The engine and editor both ship the following types out of the box:
| Type | Purpose |
|---|---|
Trigger | Entry point — accepts events of a specific event type. |
Action | Pass-through step — record an action and continue. |
Condition | Branches on rules in the shared Conditions format (top-level OR between groups; each group is all or any). Has true / false outputs. |
Wait | Pauses for a fixed duration (ms). |
TriggerOrTimeout | Continues on a matching event or after a timeout, whichever comes first. |
Exit | Terminates the workflow. |
Execution semantics are covered in Custom Nodes (Engine). Handle layouts are covered in the next subsection.
Handles
Handles are the connection points on a node where edges attach. Every node declares its own handles:
- Target handles — inputs, rendered on the top of the node
- Source handles — outputs, rendered on the bottom of the node
Each handle has an id. Edges reference these ids via sourceHandle and targetHandle — see Edges below.
Built-in node handles
| Node | Target handles (inputs) | Source handles (outputs) |
|---|---|---|
Trigger | — (entry point) | output |
Action | input | output |
Condition | input | true, false |
Wait | input | output |
TriggerOrTimeout | input | trigger, timeout |
Exit | input | — (terminates) |
A few notes on the built-ins:
Triggerhas no target handle — it is always the starting point of a branch.Exithas no source handle — it terminates the workflow.Conditionhas two source handles. Edges leaving it must specifysourceHandle: "true"orsourceHandle: "false".TriggerOrTimeouthas two source handles. Edges leaving it must specifysourceHandle: "trigger"(taken when the matching event arrives) orsourceHandle: "timeout"(taken when the duration elapses first).
Defining handles on a custom node
Handles are declared in the editor's ViewComponent, not on the NodeModel or the NodeTypeDefinition itself. Pass sourceHandles and targetHandles to BaseNodeView:
<BaseNodeView
// ...other props
sourceHandles={[{ id: "yes", label: "Yes" }, { id: "no", label: "No" }]}
targetHandles={[{ id: "input", label: "In" }]}
/>The engine then routes execution to those handles from nextNode() — e.g. this.getTargetNodeFromSourceHandle("yes"). See Custom Nodes (Editor) → Step 3 for the full pattern.
Edges
Edges connect nodes and define the flow of execution.
{
id: "edge-1",
source: "trigger-1", // Source node ID
target: "action-1", // Target node ID
sourceHandle: "output", // Optional: source handle id on the source node
targetHandle: "input" // Optional: target handle id on the target node
}When an edge leaves a specific output (e.g. the true branch of a Condition), set sourceHandle on the edge. Likewise, set targetHandle when the destination node has more than one input. For nodes with a single input/output, the handle id can be omitted on the edge.
Handles themselves — and the layouts of the built-in nodes — are documented in Nodes → Handles above.
Events
Events drive workflow execution. An event is a discrete occurrence in the system.
interface Event {
id: string; // Unique identifier
time: number; // Unix timestamp (ms)
type: string; // Event type (e.g., "user.signup")
data?: any; // Optional payload
}Event Flow
- Event arrives at Workflow Manager
- Manager runs the
eventExtractorto derive[domain, subjectId]from the event - Manager loads workflows for that domain and active contexts for that Subject
- For each active instance, the event is passed to the current node's
acceptEventmethod - If a workflow has no active instance for the Subject, the Manager checks
WorkflowOptions.frequencyto decide whether to start a new one - If accepted, the workflow processes and moves to the next node
- Process repeats until each workflow reaches a waiting state or completes
Tenants and Subjects
Routing is entirely driven by the eventExtractor function you pass to WorkflowManager. It maps every incoming event to a [domain, subjectId] tuple — there is no implicit notion of "current user" inside the engine.
// Single-tenant: domain is constant, subject is the user
eventExtractor: (event) => ["default", event.data.userId];
// Multi-tenant: tenant lives on the event payload
eventExtractor: (event) => [event.data.tenantId, event.data.userId];
// Different subject types per event
eventExtractor: (event) => event.type.startsWith("order.")
? ["orders", event.data.orderId]
: ["users", event.data.userId];- Domain scopes which workflow definitions are loaded (the
WorkflowStoreis queried per domain). Use it for tenant isolation. - Subject is the entity the workflow runs for — a user, an order, a device. Each Subject has its own independent workflow Context, so two users running the same workflow never share state.
See Workflow Execution → Event Extraction for more patterns.
Starting a new instance vs. resuming
Whether an event resumes an existing instance or starts a fresh one is governed by WorkflowOptions.frequency:
frequency.type | New instance is started when… |
|---|---|
one_time (default) | The Subject has never matched the trigger before — neither active nor completed instances exist. |
every_rematch | No instance is currently active and at least interval seconds have passed since the most recent completed instance started. |
Two consequences worth calling out:
- An active instance always blocks a new one. Even with
every_rematch, the Manager will not run two instances of the same workflow for the same Subject in parallel — the in-flight one must complete first. one_timeis permanent. Once a Subject has completed (or even started) the workflow, that workflow is closed for them forever. Useevery_rematchif you need re-entry.
See Workflow Options → Frequency below for the full configuration.
Context
Context stores the execution state for a workflow instance.
interface Context {
workflowId: string; // Which workflow
instanceId: string; // Unique instance ID
currentNodeId: string | null; // Current position
nodeState: NodeState; // Node-specific data
history: WorkflowHistoryItem[]; // Execution log
isCompleted?: boolean; // Completion flag
startedAt: number; // Start timestamp
}Multiple Instances
Each Subject (user, order, device, etc.) can have its own workflow instance with separate context. This allows:
- Multiple subjects running the same workflow independently
- Multiple workflow instances per subject (with
every_rematch) - Isolated state for each execution
Workflow Options
Frequency
Controls how often a subject can enter/re-enter a workflow:
interface WorkflowFrequency {
type: "one_time" | "every_rematch";
interval?: number; // seconds
}One Time
{
frequency: {
type: "one_time"
}
}Subject enters only the first time they match trigger conditions. Never enters again.
Use case: Welcome email workflow - send only once per user.
Every Rematch
{
frequency: {
type: "every_rematch",
interval: 86400 // 24 hours
}
}Subject can re-enter when they match trigger conditions again, but:
- Not more than once simultaneously
- Not more often than the specified interval
Use case: Re-engagement workflow - can restart every 7 days if user is inactive.
Workflow Statuses
Workflows progress through these states:
| Status | Description |
|---|---|
idle | Created but not started |
waiting | Running, waiting for events |
processing | Currently handling an event |
transforming | Moving between nodes |
completed | Finished execution |
Example Workflow
Here's a complete example - a user onboarding workflow:
{
id: "user-onboarding",
name: "User Onboarding",
flow: {
nodes: [
{
id: "trigger-1",
type: "Trigger",
data: { params: { event: "user.signup" } },
position: { x: 250, y: 0 }
},
{
id: "action-1",
type: "Action",
data: { action: "send_welcome_email", params: {} },
position: { x: 250, y: 100 }
},
{
id: "wait-1",
type: "Wait",
data: { params: { duration: 86400000 } }, // 24h
position: { x: 250, y: 200 }
},
{
id: "condition-1",
type: "Condition",
data: {
conditions: {
all: [{
fact: "user.completed_profile",
operator: "equal",
value: true
}]
}
},
position: { x: 250, y: 300 }
},
{
id: "action-2",
type: "Action",
data: { action: "send_reminder_email", params: {} },
position: { x: 100, y: 400 }
},
{
id: "exit-1",
type: "Exit",
data: {},
position: { x: 400, y: 400 }
}
],
edges: [
{ id: "e1", source: "trigger-1", target: "action-1" },
{ id: "e2", source: "action-1", target: "wait-1" },
{ id: "e3", source: "wait-1", target: "condition-1" },
{ id: "e4", source: "condition-1", sourceHandle: "false", target: "action-2" },
{ id: "e5", source: "condition-1", sourceHandle: "true", target: "exit-1" },
{ id: "e6", source: "action-2", target: "exit-1" }
]
},
options: {
frequency: {
type: "one_time"
}
}
}This workflow:
- Triggers on
user.signupevent - Sends a welcome email
- Waits 24 hours
- Checks if user completed their profile
- If not, sends a reminder email
- Exits
Architecture Overview
Workflow Manager
- Manages multiple workflows
- Loads workflow definitions from Store
- Loads/saves execution contexts from Memory
- Routes events to appropriate workflow instances
- Schedules future events via Scheduler
Workflow Engine
- Executes individual workflow instances
- Processes events through nodes
- Manages workflow state transitions
- Validates data using schemas
Next Steps
- Learn how to execute workflows with the engine
- Deploy with AWS Storage & Scheduler (DynamoDB + EventBridge)
- Learn how to set up the editor
- Create custom nodes for the engine and editor
- Explore the API reference