Creating a Workflow
Creating a Workflow
Workflows provide a way to compose multiple operations into a single process.
A workflow consists of:
- A workflow definition
- One or more steps
- Registered step implementations
- Optional event triggers
The Workflow Module executes steps sequentially and passes the workflow context from one step to the next.
Defining a Workflow
A workflow is represented by a WorkflowDefinition.
const orderWorkflow = {
slug: "order-processing",
name: "Order Processing",
version: "1.0.0",
steps: [
{
id: "validate",
type: "validate-order",
},
{
id: "payment",
type: "process-payment",
},
{
id: "confirmation",
type: "send-confirmation",
},
],
};
Each step contains an ID and a type.
The type determines which registered step implementation will execute.
The workflow therefore describes what operations should happen, while the registered step definitions describe how those operations happen.
Registering the Workflow
Register the workflow with the Workflow Module:
workflow.register(
orderWorkflow,
);
The workflow can then be executed using its slug:
const result =
await workflow.run(
"order-processing",
{
orderId: "123",
},
);
The input becomes the initial workflow context.
Creating a Workflow Step
Workflow steps are the primary building blocks of a workflow.
A workflow step is represented by a WorkflowStepDefinition:
workflow.registerStep({
type: "validate-order",
name: "Validate Order",
async execute(
context,
config,
) {
// Validate the order
return context;
},
});
The type must match the type referenced by a workflow step:
{
id: "validate",
type: "validate-order",
}
When the workflow engine reaches this step, it resolves the corresponding definition from the step registry and executes it.
Step Execution
Steps execute sequentially in the order they appear in the workflow definition.
For example:
Workflow
│
▼
Validate Order
│
▼
Process Payment
│
▼
Send Confirmation
The output of one step becomes the context available to the next step.
This allows steps to build upon previous operations without requiring the workflow itself to manage that state.
Step Configuration
Steps can contain configuration:
{
id: "send-confirmation",
type: "send-email",
config: {
template: "order-confirmation",
},
}
The configuration is passed directly to the step:
workflow.registerStep({
type: "send-email",
name: "Send Email",
async execute(
context,
config,
) {
const template =
config.template;
// Use the template
return context;
},
});
This allows one step implementation to support multiple configurations.
For example, the same send-email step could be used by several workflows with different email templates.
Workflow Context
Every workflow receives a context.
{
input,
event,
executionId,
workflowId,
metadata,
}
The initial input is supplied when the workflow is run:
await workflow.run(
"order-processing",
{
orderId: "123",
},
);
The workflow engine creates an execution ID and constructs the initial context:
{
input: {
orderId: "123",
},
executionId: "...",
workflowId:
"order-processing",
metadata: {},
}
The context is then passed through each step.
Passing Data Between Steps
A step can return an updated context.
For example:
workflow.registerStep({
type: "load-order",
name: "Load Order",
async execute(context) {
const order = await loadOrder(
context.input.orderId,
);
return {
...context,
metadata: {
...context.metadata,
order,
},
};
},
});
The next step receives the returned context:
workflow.registerStep({
type: "process-order",
name: "Process Order",
async execute(context) {
const order =
context.metadata.order;
// Process order
return context;
},
});
This creates a simple data pipeline:
Input
│
▼
Step 1
│
▼
Updated Context
│
▼
Step 2
│
▼
Updated Context
│
▼
Step 3
The metadata object can be used to carry data between steps without changing the original workflow input.
Using Jobs Inside a Workflow
Workflow steps receive the Job Module through their context:
interface WorkflowStepContext
extends WorkflowContext {
job: JobModule;
logger: Logger;
}
This allows a step to dispatch background work.
For example:
workflow.registerStep({
type: "send-confirmation",
name: "Send Confirmation",
async execute(context) {
await context.job.dispatch({
name: "send-email",
payload: {
orderId:
context.input.orderId,
},
});
return context;
},
});
The step does not need to specify the queue.
The queue is determined when the job is registered:
job.register(
"send-email",
async (job, context) => {
// Send email
},
{
queue: "comms",
},
);
The resulting flow is:
Workflow Step
│
▼
Job Dispatch
│
▼
send-email
│
▼
comms Queue
│
▼
Job Handler
This keeps workflow orchestration separate from queue routing and background execution.
Event Triggers
Workflows can declare events that should trigger them:
const workflow = {
slug: "order-processing",
name: "Order Processing",
version: "1.0.0",
triggers: [
{
event: "order.created",
},
],
steps: [
// ...
],
};
This provides a natural connection between the Event Module and Workflow Module.
order.created
│
▼
Workflow
│
├── Validate
├── Process
└── Complete
Event-triggered execution allows modules to publish events without needing direct knowledge of the workflows that consume them.
Workflow Results
workflow.run() returns a WorkflowExecutionResult.
A successful execution returns:
{
executionId,
workflowId,
status: "completed",
context,
}
If a step throws an error, the workflow returns:
{
executionId,
workflowId,
status: "failed",
context,
error,
}
The execution ID can be used by an application to correlate the result with other application data or logs.
Designing Steps
Workflow steps should generally represent meaningful operations.
Good step boundaries might be:
Validate Order
Load Customer
Reserve Inventory
Process Payment
Send Confirmation
Avoid making every small function a workflow step.
A step should represent an operation that makes sense as part of the larger process.
Steps should also remain focused on their responsibility. If an operation becomes sufficiently independent or needs background execution, it may be better represented as a job that the step dispatches.
Reusing Steps
Because steps are registered by type, the same implementation can be used by multiple workflows.
For example:
send-email
could be used by:
Order Workflow
Password Reset Workflow
Account Registration Workflow
Invoice Workflow
Each workflow can provide different configuration.
This allows workflows to describe application processes while step implementations provide reusable behavior.
Jobs, Steps, and Workflows Together
These components have distinct responsibilities:
Workflow
│
├── Step
│ │
│ └── Module / Service
│
├── Step
│
└── Step
│
└── Job
│
▼
Queue
The workflow defines the process.
The steps perform or coordinate individual operations.
Jobs handle discrete background work.
Queues provide the infrastructure used to execute those jobs.
Summary
A workflow is built from:
Workflow Definition
│
▼
Workflow Steps
│
▼
Workflow Context
│
└── Jobs → Queues
The Workflow Module handles workflow registration and execution while individual steps provide the actual operations.
Steps can interact directly with modules and services or dispatch jobs when work should be performed asynchronously.
This makes workflows useful for coordinating larger processes without coupling the process itself to a particular infrastructure implementation.
Next
Continue to Jobs & Workflows in Practice to learn how to decide between jobs, steps, and workflows and how to combine them into larger application processes.