Runtime does not merely update CurrentNodeId. It coordinates instance version, execution tokens, tasks, candidates, FlowState, BusinessStatus, activity, timers, cache, and external callbacks. Business code enters through focused ports or Platform commands.
1. Runtime ports
| Port | Operations |
|---|---|
| IWorkflowTransitionFlow | Start, Complete, Reject, Withdraw, Resubmit |
| IWorkflowTaskHandoffFlow | Transfer, Delegate, AddParticipant |
| IWorkflowMigrationFlow | ResetToNode, StartAtNode, TransferApplicant |
| IWorkflowBatchOperation | Terminate, Cancel, Suspend, Resume, Remind, BatchComplete, Priority |
| IWorkflowQuery | ProgressView |
IRuntimeService is an obsolete compatibility facade.
2. Start
// Actor and tenant come from authenticated server context.Result<IProcessInstance> started = await transitions.StartAsync( "matter-intake-approval", matter.MatterNo, "MatterIntake", currentUserId, currentTenantId, matter.OfficeId, new Dictionary<string, object?> { // Pass routing facts, not the mutable business aggregate. ["disputeAmount"] = matter.DisputeAmount, ["crossBorder"] = matter.IsCrossBorder }, cancellationToken);Start has no IdempotencyKey and does not visibly precheck definitionKey+businessKey. Reconcile after a timeout before retrying.
3. Complete
Completion loads a Pending task, performs Operate permission, loads the pinned definition, and in one transaction advances instance Version, completes the task, handles multi-instance, leaves the node, moves tokens, updates state, and appends activity.
// Runtime rechecks Pending state and Operate permission before writing.Result completed = await transitions.CompleteTaskAsync( taskId, currentUserId, "Conflict check cleared; open the matter", new Dictionary<string, object?> { // These values become the snapshot used by the next gateway. ["conflictCleared"] = true, ["engagementLetterSent"] = false }, cancellationToken);A duplicate may return TaskNotPending or ConcurrencyConflict rather than the original response.
4. Reject, withdraw, and resubmit
Reject accepts an explicit target or resolves RollbackRule, rebuilds relevant execution/task state, and records the transition. Withdraw uses current-applicant semantics — it maps to ProcessInstanceStatus.Cancelled with BusinessStatus = "Withdraw", from which Resubmit reopens the flow. There is no distinct Withdrawn enum value.
IApplicantResolver can resolve current ownership dynamically and falls back to CurrentApplicantId/StarterId. TransferApplicant updates that snapshot.
5. Handoff
Transfer permanently changes responsibility. Delegate is temporary. AddParticipant adds approvers. These task-focused writes advance instance Version inside the transaction so they conflict with other writes on the same instance.
// Validate that the successor is an active user in the same tenant.Result transferred = await handoff.TransferTaskAsync( taskId, currentUserId, successorUserId, "Coverage while lead counsel is in court", cancellationToken);Server enforcement of UserTaskConfig AllowTransfer/AllowDelegate/AllowAddParticipant must be verified; hiding UI controls is insufficient.
6. Control operations
Suspend/Resume, Terminate/Cancel, Reset, and Priority require Manage permission and use optimistic concurrency. BatchComplete is a proxy administrative operation: it atomically marks pending tasks completed and advances versions but intentionally does not move tokens. It is not a batch approval endpoint.
Remind dispatches TaskRemind to pending assignees; notification failures fail the command together with the transition (see section 8), so reminders can no longer silently vanish.
7. Multi-instance and gateways
Parallel multi-instance creates work for each participant; Sequential creates it one at a time. CompletionCondition determines closure. Parallel gateway join scopes by parent execution. Test simultaneous final votes, sibling cancellation, variable merging, nested joins, and inclusive zero-match behavior.
8. Transaction and side effects
IPersistenceStore transaction covers workflow tables and maps concurrency exceptions. Notifications and listeners now ride the failure chain: the platform INotificationChannel writes per-recipient inbox entries plus CAP Outbox rows and throws when any recipient fails; NotificationDispatcher logs and rethrows; EventDispatcher propagates listener exceptions upward — failing and rolling back the whole transition. Approval success and “notification facts persisted” are now the same statement.
IBusinessIntegrationCallback is awaited directly and is not an Outbox. Placement varies by operation and can leave committed workflow state with a failed request or callback. Business synchronization needs idempotency, replay, and reconciliation.
9. Timers
Timer records are created when timed nodes run. JobHost scans Pending. Current order:
var fired = job with { Status = "Fired" };await store.SaveTimerJobAsync(fired, cancellationToken);
// A crash or action failure here does not restore Pending.await DispatchJobAsync(job, cancellationToken);Scanning has no atomic claim either. Add compare-and-swap/lease, attempt, retry time, last error, DLQ, and recovery runbook.
10. Error handling
Distinguish NotFound, Forbidden, InvalidState, TaskNotPending, ConcurrencyConflict, and infrastructure failure. Do not blindly retry all 409 responses; a conflict normally requires a fresh read and user confirmation.
11. Test matrix
- duplicate business key and disconnected start client;
- both ORM transaction rollback;
- complete/reject/terminate races;
- assignee, candidate, applicant, and Manage permissions;
- notification and callback failure state;
- duplicate scan, crash, unknown timer, and failed escalation;
- parallel and multi-instance convergence;
- cross-node cache invalidation.