Type Registries
Type registries let persisted workflow definitions refer to executable logic by schemaType.
Why They Exist
Stored workflow definitions are data:
{
"type": "serviceTask",
"config": {
"schemaType": "task.email.send",
"storeAs": "email"
}
}At runtime, the engine resolves task.email.send to a TaskExecutor through descriptors.
Descriptor Setup
final descriptor = WorkflowDescriptor(
title: 'Notification executors',
tasks: [SendEmailExecutor.typeDescriptor],
userTasks: [ApprovalUserTaskExecutor.typeDescriptor],
conditions: [RequiresManagerCondition.typeDescriptor],
);
final context = RegistryTypeResolver(
descriptors: [
DefaultWorkflowDescriptor(),
descriptor,
],
);The resolver creates and initializes a WorkflowRegistry lazily from the descriptor list.
Task TypeDescriptor
class SendEmailExecutor extends TaskExecutor {
static const type = 'task.email.send';
static final typeDescriptor = TypeDescriptor<TaskExecutor>(
schemaType: type,
fromJson: (_) => SendEmailExecutor(),
title: 'Send Email',
);
@override
String get schemaType => type;
@override
String get name => 'Send Email';
@override
Future<TaskResult> execute(ExecutionContext context) async {
final to = context.getRequired<String>('recipient');
await emailService.send(to: to);
return TaskSuccess([
SetOutputEffect(output: {'sent': true}),
]);
}
}User Task TypeDescriptor
class ApprovalUserTaskExecutor extends UserTaskExecutor {
static const type = 'userTask.approval';
static final typeDescriptor = TypeDescriptor<UserTaskExecutor>(
schemaType: type,
fromJson: (_) => ApprovalUserTaskExecutor(),
title: 'Approval',
);
@override
String get schemaType => type;
@override
String get name => 'Approval';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
return WaitForUserTaskResult(
signalName: context.signalName ?? 'approval_decision',
config: UserTaskConfiguration(
title: 'Approve request',
schemaType: schemaType,
assignment: TaskAssignment.role('approvers'),
input: context.accumulated,
),
);
}
}Condition TypeDescriptor
class RequiresManagerCondition extends ConditionExecutor {
static const type = 'condition.requires_manager';
static final typeDescriptor = TypeDescriptor<ConditionExecutor>(
schemaType: type,
fromJson: (_) => const RequiresManagerCondition(),
title: 'Requires Manager',
);
const RequiresManagerCondition();
@override
String get schemaType => type;
@override
Future<bool> execute(ExecutionContext context) async {
final amount = context.getAny<num>('amount') ?? 0;
return amount > 5000;
}
@override
Map<String, dynamic> toJson() => {'schemaType': schemaType};
}What Gets Resolved
| Runtime piece | Resolution rule |
|---|---|
NodeConfiguration | Resolved by node type, such as serviceTask -> ServiceTaskNodeConfiguration. |
TaskExecutor | Resolved by task config schemaType. |
UserTaskExecutor | Resolved by user-task config schemaType, with default user-task fallback. |
ConditionExecutor | Resolved from condition JSON schemaType. |
NodeProcessor | Resolved by NodeType from the default node processor registry. |
| Lifecycle hooks | Resolved by workflow code through descriptors. |
Loading With A Resolver
Use engine loading APIs so the resolver scope is applied:
final workflow = engine.loadWorkflowDefinition(definition);or:
final workflow = engine.loadWorkflowJSON(json);Avoid direct Workflow.fromJson(json) unless you wrap it in context.run(...).
Simulation Resolver
SimulationDeserializationContext keeps node configs typed but substitutes passthrough task/user-task behavior:
final engine = WorkflowEngine(
context: SimulationDeserializationContext(),
storage: InMemoryStorage(),
executionMode: ExecutionMode.simulation,
);This is the path used by the editor and tests when workflows should be explored without running production side effects.
Template Descriptors
Templates contribute descriptors just like apps:
final template = ApprovalTemplate(domains: [approvalDomain]);
final context = RegistryTypeResolver(
descriptors: [
DefaultWorkflowDescriptor(),
template.buildDescriptor(),
],
);For the canonical approval workflow, the descriptor registers approval loop executors, outcome dispatchers, approval user-task executors, and revision user-task executors.
Duplicate Schema Types
Descriptors are processed in order. Avoid duplicate schema types unless overriding is deliberate and tested. Put DefaultWorkflowDescriptor() first so app descriptors can override built-ins when needed.