Skip to content

Loading & Registration

Workflow execution has two separate registration concerns:

  1. Register executable types through WorkflowDescriptors.
  2. 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:

dart
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:

dart
final storage = InMemoryStorage();

For Supabase:

dart
final storage = SupabaseStorage(
  client: supabaseClient,
  config: const SupabaseStorageConfig(schema: 'elog'),
);

Storage exposes four repositories:

  • workflows: WorkflowDefinition templates.
  • instances: running and completed WorkflowInstances.
  • userTaskInstances: inbox tasks.
  • events: append-only workflow events.

3. Create and Initialize the Engine

dart
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

dart
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

dart
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

dart
final workflow = engine.loadWorkflowJSON(json);

loadWorkflowJSON() parses the JSON into WorkflowDefinition, then uses the same load path as storage.

5. Start Instances

dart
final instance = await engine.startWorkflow(
  workflowCode: 'approval',
  version: 4,
  input: approvalInput,
  correlationId: entityId,
  userId: currentUserId,
);

startWorkflow() can use either:

  • workflowCode, with optional version.
  • 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:

  1. Register the template descriptor in the resolver.
  2. Load the canonical definition from storage.
dart
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:

dart
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

OperationWhat 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

SymptomCauseFix
Unknown TaskExecutor typeMissing TypeDescriptor<TaskExecutor> for the node schemaType.Add the descriptor to RegistryTypeResolver.
Unknown ConditionExecutor typeCondition schema type was not registered.Register the condition descriptor or use a built-in expression condition.
Cannot create instance: workflow has no IDWorkflow did not have an ID.Use WorkflowBuilder or a stored WorkflowDefinition with an ID.
Template definition missingApprovalTemplate 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 unexpectedlyProduction engine used in an authoring surface.Use SimulationEnvironment or editor DatabaseEnvironment, both of which run in simulation mode.

See Also