Skip to content

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.

StartAsync(workflowName, entityType, entityId, contextData):

  1. Loads the active definition by Name, parses its XML, and finds the Start node.
  2. Pins the version — records the definition’s current VersionHash on 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.
  3. Serialises contextData to the instance’s ContextData (JSON) — every @variable the workflow reads later comes from here.
  4. Creates the WorkflowInstance (Status = "Active") and its initial WorkflowInstanceState, then auto-advances from Start through its single outgoing transition.

From there the engine calls AdvanceAsync for each edge it follows. AdvanceAsync:

  • closes the current state (Completed, ExitedAt stamped) and writes a WorkflowTransitionLog row,
  • moves CurrentNodeKey to the target and opens a new state,
  • resolves node assignment (from node attributes, else inherited from the instance),
  • applies the transition’s sticky FlowStatus when non-empty,
  • if the target is End, sets the instance Completed and clears assignment,
  • then auto-advances again when the target is a Decision or Action node.

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.

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).

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 nodes execute by ActionType (case-insensitive), then auto-advance:

  • ExecuteSql — the <Payload> SQL runs via ExecuteSqlRawAsync, parameterised with @RecordId and @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 inserts NotificationStore rows (Type = App) delivered over SignalR/web-push.
  • SendEmail — parses <To>/<CC>/<Subject>/<MailBody>, renders Subject and Body with Handlebars, resolves recipient emails, and queues a NotificationStore row (Type = Email) for the background email worker. Delivery requires SMTP configured under Notifications: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.

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: @Var or bare Var (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 is false.

@FlowStatus is injected into the context for evaluation, so guards can branch on the business status.

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 stored ResponseData).

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.