Loading & Registration
Workflow execution has two separate registration concerns:
- Register executable types through
WorkflowDescriptors. - Load workflow definitions into the engine's in-memory execution cache.
Storage persists definitions and instances, but storage does not resolve types or execute anything.
Boot Sequence
1. Register Executable Types
Executors are registered through descriptors:
final descriptor = WorkflowDescriptor(
title: 'Document workflow executors',
tasks: [
ValidateDocumentExecutor.typeDescriptor,
ApplyApprovalOutcomeExecutor.typeDescriptor,
],
userTasks: [
ApprovalUserTaskExecutor.typeDescriptor,
],
conditions: [
RequiresSecondLevelCondition.typeDescriptor,
],
);
final context = RegistryTypeResolver(
descriptors: [
DefaultWorkflowDescriptor(),
descriptor,
],
);DefaultWorkflowDescriptor() provides built-in node processors and built-in condition support. Custom descriptors provide app or template executors.
2. Create Storage
For tests and local runs:
final storage = InMemoryStorage();For Supabase:
final storage = SupabaseStorage(
client: supabaseClient,
config: const SupabaseStorageConfig(schema: 'elog'),
);Storage exposes four repositories:
workflows:WorkflowDefinitiontemplates.instances: running and completedWorkflowInstances.userTaskInstances: inbox tasks.events: append-only workflow events.
3. Create and Initialize the Engine
final engine = WorkflowEngine(
context: context,
storage: storage,
executionMode: ExecutionMode.production,
);
await engine.initialize();initialize() initializes the storage adapter and, by default, recovers stale running workflows.
4. Load Workflow Definitions
Code-built workflow
final workflow = WorkflowBuilder('document-review', 'Document Review')
.start('start')
.task('validate', executor: ValidateDocumentExecutor())
.end('done')
.build();
engine.registerWorkflow(workflow);registerWorkflow() caches the workflow in memory. It does not persist it.
Stored workflow definition
final definition = await engine.storage.workflows.getByCode(
'approval',
version: 4,
);
if (definition != null) {
engine.loadWorkflowDefinition(definition);
}loadWorkflowDefinition() converts the data model into an executable Workflow using the engine resolver, validates the graph, resolves node processors, and registers the workflow in memory.
JSON workflow definition
final workflow = engine.loadWorkflowJSON(json);loadWorkflowJSON() parses the JSON into WorkflowDefinition, then uses the same load path as storage.
5. Start Instances
final instance = await engine.startWorkflow(
workflowCode: 'approval',
version: 4,
input: approvalInput,
correlationId: entityId,
userId: currentUserId,
);startWorkflow() can use either:
workflowCode, with optionalversion.workflowId, the exact stored definition ID.
The engine checks the in-memory cache first. If the workflow is not cached, it loads the definition from storage, resolves it, caches it, and starts the instance.
Templates
cdx_workflow_templates uses the same two phases:
- Register the template descriptor in the resolver.
- Load the canonical definition from storage.
final template = ApprovalTemplate(domains: [myApprovalDomain]);
final context = RegistryTypeResolver(
descriptors: [
DefaultWorkflowDescriptor(),
template.buildDescriptor(),
],
);
final engine = WorkflowEngine(
context: context,
storage: storage,
executionMode: ExecutionMode.production,
);
await engine.initialize();
final definition = await engine.storage.workflows.getByCode(
template.workflowCode,
);
if (definition == null) {
throw StateError('Seed the template workflow definition before boot.');
}
engine.loadWorkflowDefinition(definition);template.buildDefinition() is for generating the canonical JSON to seed storage, often through a migration. Runtime boot should read from storage so every service instance executes the same persisted version.
Simulation and Editor Loading
The editor uses environments:
WorkflowEditor(
environments: [
SimulationEnvironment(storageAdapter: InMemoryStorage()),
DatabaseEnvironment(
id: 'elog',
name: 'ELog',
storageAdapter: storage,
descriptors: [template.buildDescriptor()],
),
],
)EditorContext initializes storage, creates an in-process engine, wraps it in InProcessTransport, and exposes WorkflowProtocolClient. It loads the workflow library from storage.workflows.list(activeOnly: true).
Registration vs Persistence
| Operation | What it does |
|---|---|
registerWorkflow(workflow) | Cache a code-built executable workflow in memory. |
saveWorkflow(workflow) | Persist a workflow as a WorkflowDefinition through storage. |
loadWorkflowDefinition(definition) | Convert stored data into an executable workflow and cache it. |
loadWorkflowJSON(json) | Parse JSON to WorkflowDefinition, then load it. |
startWorkflow(...) | Create a new instance, loading from storage if needed. |
Common Failure Modes
| Symptom | Cause | Fix |
|---|---|---|
Unknown TaskExecutor type | Missing TypeDescriptor<TaskExecutor> for the node schemaType. | Add the descriptor to RegistryTypeResolver. |
Unknown ConditionExecutor type | Condition schema type was not registered. | Register the condition descriptor or use a built-in expression condition. |
Cannot create instance: workflow has no ID | Workflow did not have an ID. | Use WorkflowBuilder or a stored WorkflowDefinition with an ID. |
| Template definition missing | ApprovalTemplate descriptor was registered but the approval definition was not seeded. | Seed template.buildDefinition() into storage.workflows or run the DB migration. |
| Editor task runs real code unexpectedly | Production engine used in an authoring surface. | Use SimulationEnvironment or editor DatabaseEnvironment, both of which run in simulation mode. |