Process Managers
What is a Process Manager?
A process manager orchestrates a multi-step workflow that spans multiple events over time. Unlike a policy, which reacts to a single event, a process manager tracks state across a sequence of events, coordinating actions until the workflow completes, times out, or fails.
Process Managers in Bounda
Process managers live inside domain/<aggregate>/processes/<process-name>/ as a folder containing a configuration file and individual event handler files.
domain/
order/
processes/
order-lifecycle/
index.ts
on-order-placed.ts
on-payment-received.ts
on-order-fulfilled.ts
on-order-lifecycle-timed-out.ts
Configuration
The index.ts file exports a config function that defines when the process starts, when it completes, and an optional timeout:
import type { ProcessManager } from "./+types/order-lifecycle";
export const config: ProcessManager.ConfigFunction = ({ events }) => ({
startedBy: [events.OrderPlaced],
completedBy: [events.OrderFulfilled],
timeout: "7d",
});
Configuration Options
| Option | Description |
|---|---|
startedBy | Array of event types that create a new process instance |
completedBy | Array of event types that mark the process as complete |
timeout | Duration after which the process is considered timed out (e.g., "7d", "24h") |
Event Handlers
Each event the process reacts to gets its own file named on-<event-name>.ts. The handler receives the event and a commands object for dispatching further actions:
import type { ProcessManager } from "./+types/on-order-placed";
export async function handler({ event, commands }: ProcessManager.HandlerArgs) {
await commands.sendWelcomeEmail({
aggregateId: event.aggregateId,
delay: "1m",
});
}
Handlers can dispatch commands immediately or with a delay using the delay property. See Scheduled Commands for supported delay formats.
Special Event Handlers
Process managers support two special handler files for lifecycle edge cases:
Timeout Handler
When a process exceeds its configured timeout, Bounda triggers the timeout handler:
import type { ProcessManager } from "./+types/on-order-lifecycle-timed-out";
export async function handler({ event, commands }: ProcessManager.HandlerArgs) {
await commands.cancelOrder({
aggregateId: event.aggregateId,
});
}
The file is named on-<process-name>-timed-out.ts.
Failure Handler
When a step in the process fails after exhausting retries, the failure handler is triggered:
import type { ProcessManager } from "./+types/on-order-lifecycle-failed";
export async function handler({ event, commands }: ProcessManager.HandlerArgs) {
await commands.flagForManualReview({
aggregateId: event.aggregateId,
});
}
The file is named on-<process-name>-failed.ts.
Process State
Bounda tracks process manager instances internally. Each instance records:
| Field | Description |
|---|---|
processId | Unique identifier for the process instance |
status | Current status: started, completed, timed_out, failed |
startedAt | Timestamp when the process was initiated |
aggregateId | The aggregate instance this process is associated with |
Dead Letter Queue
Like policies, process managers support DLQ configuration for handlers that fail after retrying:
processes: {
retry: {
retryStrategy: "exponential",
maxRetries: 3,
},
dlq: {
type: "sqlite",
path: "./db/processes-dlq.db",
},
}
Guidelines
- Use process managers when a workflow involves waiting for multiple events across time. For single event reactions, prefer policies.
- Always define a timeout. Processes that never complete consume resources and obscure system state.
- Include both timeout and failure handlers to ensure the system can recover gracefully from edge cases.
- Process managers should coordinate, not compute. Keep business logic in commands and let the process manager orchestrate the sequence.
Related Concepts
- Policies — for single-event reactions without state tracking
- Scheduled Commands — delaying command execution from process handlers
- Commands — process handlers dispatch commands to perform actions