Engine, approvals & SLA
The runtime lives in Genie.Engine/Features/Workflow. IWorkflowEngine (implemented by
WorkflowEngine) orchestrates the lifecycle; focused collaborators handle approvals
(WorkflowApprovalResolver), guards (WorkflowExpressionEvaluator), and background SLA checks
(WorkflowSlaMonitor + its hosted service). This page describes how a definition actually runs. For
the XML that drives it, see Authoring a workflow.
Instance lifecycle
Section titled “Instance lifecycle”StartAsync(workflowName, entityType, entityId, contextData):
- Loads the active definition by
Name, parses its XML, and finds theStartnode. - Pins the version — records the definition’s current
VersionHashon the new instance so later edits never change how this run behaves (minting a first version lazily for pre-versioning definitions). See Versioning & company scoping. - Serialises
contextDatato the instance’sContextData(JSON) — every@variablethe workflow reads later comes from here. - Creates the
WorkflowInstance(Status = "Active") and its initialWorkflowInstanceState, then auto-advances fromStartthrough its single outgoing transition.
From there the engine calls AdvanceAsync for each edge it follows. AdvanceAsync:
- closes the current state (
Completed,ExitedAtstamped) and writes aWorkflowTransitionLogrow, - moves
CurrentNodeKeyto the target and opens a new state, - resolves node assignment (from node attributes, else inherited from the instance),
- applies the transition’s sticky
FlowStatuswhen non-empty, - if the target is
End, sets the instanceCompletedand clears assignment, - then auto-advances again when the target is a
DecisionorActionnode.
Approval and Standard nodes pause — the instance waits for an external TransitionAsync or
SubmitApprovalAsync call.
TransitionAsync(instanceId, transitionLabel, …) finds the matching outgoing transition (by Label,
falling back to To), evaluates its Expression guard, optionally merges caller data into the
context, and advances. CancelAsync sets the instance Cancelled.
Approvals
Section titled “Approvals”When the engine enters an Approval node it creates one pending WorkflowApproval record per
approver rule:
Role— one record per role; any member can claim and decide it. The instance is also assigned to that role (looked up in[Identity].[Roles]) so it appears in the role’s queue.User— one record for a specific user id.FieldValue— resolves a user id from a@ContextVar.
Approvers act through the built-in Pending Approvals list and the WF_ApprovalForm (the Approval
node defaults its activity to that form) — no custom form required. A decision comes in through
SubmitApprovalAsync(instanceId, nodeKey, userId, decision, comment).
Gate policies (<Type>)
Section titled “Gate policies (<Type>)”After each decision, CheckApprovalCompletionAsync evaluates the node’s ApprovalConfig.Type against
the pending / approved / rejected records created since the node was last entered:
Type |
Approves when… | Rejects when… |
|---|---|---|
Any |
the first approval arrives (remaining pending records are marked Ignored). |
the first rejection arrives. |
Sequential (default) |
all records are approved and none pending. | any rejection arrives (immediately). |
Parallel |
all have decided and none rejected. | all have decided and at least one rejected. |
When the gate is satisfied the engine follows OnApprove / OnReject — matching those values against
the transition Labels leaving the node — and calls AdvanceAsync.
Action handlers
Section titled “Action handlers”Action nodes execute by ActionType (case-insensitive), then auto-advance:
ExecuteSql— the<Payload>SQL runs viaExecuteSqlRawAsync, parameterised with@RecordIdand@EntityId(both the entity id),@InstanceId,@FlowStatus, the session parameters, and every context variable (@ContextVar). Context keys that aren’t valid SQL parameter names (e.g. containing spaces) are skipped. Literal braces in the SQL are escaped internally.AppNotification— parses<To>/<Message>, renders the message with Handlebars over the context, resolves recipients to usernames, and insertsNotificationStorerows (Type = App) delivered over SignalR/web-push.SendEmail— parses<To>/<CC>/<Subject>/<MailBody>, renders Subject and Body with Handlebars, resolves recipient emails, and queues aNotificationStorerow (Type = Email) for the background email worker. Delivery requires SMTP configured underNotifications:Email.
Recipient tokens (Role:Name, @ContextVar, literal email) are described in
Authoring — Action node. See also
Notifications for delivery workers and channels.
IWorkflowActionHandler (ActionType + ExecuteAsync) is the pluggable seam for adding further
action types.
Expression evaluator
Section titled “Expression evaluator”WorkflowExpressionEvaluator guards transitions (and drives Decision branching). It is a small,
eval-free interpreter — not a SQL or C# engine:
- Operators:
==,!=,>,<,>=,<=,IS NULL,IS NOT NULL. - Boolean composition:
AND,OR(split outside single-quoted literals). - Literals:
true/false; single/double-quoted strings; numbers. - Operands:
@Varor bareVar(both resolved against the context, with and without the@). - Typing: comparisons parse both sides as decimals and compare numerically; equality compares string forms case-insensitively.
- Fail-safe: an empty expression is
true; an expression it can’t parse isfalse.
@FlowStatus is injected into the context for evaluation, so guards can branch on the business status.
SLA monitor
Section titled “SLA monitor”WorkflowSlaMonitorHostedService is a BackgroundService that runs every 1 minute and calls
IWorkflowSlaMonitor.CheckAndEscalateAsync.
The designer API — /api/v1/genie/workflow
Section titled “The designer API — /api/v1/genie/workflow”WorkflowDesignerController ([Authorize], base route /api/v1/genie/workflow) backs the visual
designer and the runtime hooks. At a high level:
Definitions
GET definitions— list active definitions (company-scoped).GET definitions/{key}— a definition by id, name, or slug (includes its XML +VersionHash).POST definitions/PUT definitions/{id}— create / update metadata.POST definitions/{definitionId}/save— save XML (validates a Start node exists; versions the save).
Version history
GET definitions/{definitionId}/versions— every saved version (hash · time · notes ·IsCurrent).GET definitions/{definitionId}/versions/{versionHash}— a single version’s XML.
Instances
POST instances/start,instances/{id}/transition,instances/{id}/approve,instances/{id}/cancel.GET instances/{id},instances/{id}/audit,instances/{id}/transitions.GET instances/{id}/preview— the graph the instance actually runs (its pinned XML), visited nodes, traversed paths, per-node context snapshots, and available transitions — for the designer’s instance overlay.POST instances/retry— re-attempt a workflow start for a wizard form response that failed to start (rehydrating context from the storedResponseData).
The React designer (components/workflow/GenieWorkflowDesigner.tsx) renders the node/transition graph,
edits action payloads structurally, and overlays live instance state via the preview endpoint.