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, companyId?):
- 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. - Stamps the tenant — every instance query (the workflow grids, WF Tasks, pending approvals,
and the entity lookup behind
transitionEntityWorkflow) filtersCompanyId = @SessionCompanyId, so the instance and its state rows carry: the explicitcompanyIdargument when supplied (programmatic hosts pick the tenant in code), else the session company for authenticated starts, else the definition’s company for anonymous starts (e.g. a public-API host callingIWorkflowEngine.StartAsyncdirectly). - 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, - runs the bound-record update then the transition’s
<Sql>, and saves — see below, - then auto-advances again when the target is a
DecisionorActionnode.
Authored SQL on a transition
Section titled “Authored SQL on a transition”When the Start node declares a <Binding>
or the edge carries a <Sql>, AdvanceAsync runs them in one transaction with the instance save:
- the bound record’s
UPDATE(skipped when the transition carries no status), - the transition’s
<Sql>— so it can extend or override what the binding just wrote, SaveChangesAsync, then commit.
A failure anywhere in that sequence rolls the whole transition back: the instance keeps its
previous node and FlowStatus, no WorkflowTransitionLog row is written, and the exception surfaces
through the standard error envelope. Two consequences worth knowing when authoring:
- Your SQL runs before the instance row is committed, so read the supplied parameters
(
@EntityId,@FlowStatus,@TransitionLabel,@Comment, context variables) rather than queryingWorkflow.Instancesfor the new state. - A transaction is only opened when there is authored SQL to run and none is already ambient — a caller that already owns one (e.g. a form submit whose SQL fired a workflow callback) keeps its own.
A binding whose UPDATE matches no row is logged as a warning, not an error: the flow’s own state
is still valid, and it almost always means the binding points at the wrong entity, primary key, or
tenant. The tenant predicate uses the instance’s own CompanyId, not the ambient session, so
auto-advance and background work (SLA escalation, hosted services) scope correctly with no session. Both this and Action-node ExecuteSql go through the provider-agnostic ADO path, so authored
SQL runs unchanged on SQL Server and PostgreSQL and literal braces need no escaping.
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 on the same provider-agnostic path as transition<Sql>, 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.AppNotification— parses<To>/<Message>, renders both 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 all four with Handlebars (so{{Email}}-style recipients from context work), then resolves recipient emails and queues aNotificationStorerow (Type = Email) for the background email worker. Delivery requires SMTP configured underGenie: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.
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 MonitoredBackgroundService that runs every 1 minute and
calls IWorkflowSlaMonitor.CheckAndEscalateAsync. It appears on the
hosted-services dashboard as Workflow SLA Monitor, can be paused there,
waits for model migration before its first sweep, and retries a failed sweep after a minute. A host can
supply its own SLA policy by registering an IWorkflowSlaMonitor after AddGenieWorkflow(); the last
registration wins.
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— for programmatic and administrative callers only; the React client has no methods for these, because authored SQL drives workflows through server callbacks instead. The three that act on an existing instance require the caller to be its assignee, a member of its assigned role, a pending approver on it, or holdSystem/Admin; the acting user always comes from the session.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.
The designer (UI)
Section titled “The designer (UI)”Property pane
Section titled “Property pane”The right-hand pane is the same property grid as the report designer’s Properties tab. A chip at
the top names the selection — WORKFLOW for the workflow settings (nothing selected), TRANSITION,
the node’s type (START, APPROVAL, …), or 3 NODES for a multi-selection — and below it one
bordered box lists every property as a two-column row under an uppercase category:
- Workflow settings — Identity (Name, Slug, Label), Classification (Entity Type, Category).
- Transition — Identity (Label, Order), Guard (Expression, Require Comment), Status (Flow Status, Entity Status), SQL.
- Node — Identity (Key, read-only; Label; Type; Description), then per type: Activity and SLA (Standard / Approval), Approval (Mode) with an Approvers list, Assignment, Bound record (Start), or Action with its payload rows, recipient lists and the raw-payload disclosure. A Decision lists its Branch conditions (the target above a full-width guard input); other nodes list their Outgoing transitions — click one to select that transition.
Flags are — (unset) / true / false selects. The gating ones — SLA Enabled and Bind Record —
reveal their dependent rows when true; unset or false removes the <Sla> / <Binding> from the XML,
exactly as before. A description footer under the pane explains the focused row (it starts on the
selection’s first property). Delete node / Delete transition / Delete N nodes sits below the
box. While viewing an old version or an instance preview every editor is disabled and the add, remove
and delete buttons are hidden. Drag the splitter to widen the pane.
Ctrl+Z steps back through your edits; Ctrl+Shift+Z or Ctrl+Y replays them. The toolbar carries the same two buttons, and each tooltip names what it would reverse. A drag is one step, a run of typing in one property is one step, and loading XML (the XML dialog, or an assistant Apply) is one step. Ctrl+Z inside a text box or the XML editor belongs to that editor. A dot on Save marks unsaved changes, and leaving the page with any asks first. Opening an old version from History starts a fresh stack, so an old version can never be undone into the live workflow.
Assistant-assisted authoring
Section titled “Assistant-assisted authoring”When the AI Assistant module is on, attach the workflow from the composer’s + menu (Current workflow) and two tools scoped to this page become available:
workflow_get_definitionreads the saved workflow under your own permissions, together with a condensed reference of the<Workflow>elements and attributes, so the model writes from the contract rather than guessing from what the open workflow happens to use.workflow_apply_changeproposes a complete rewritten<Workflow>document as an Apply card in the chat.
Clicking Apply loads the proposal onto the canvas as one undoable step; Ctrl+Z reverses it. From there it is an ordinary unsaved edit — review it, then Save.
Apply keeps your layout. Nodes that already exist stay exactly where they are on your canvas (position and size), and existing transitions keep their label position and connection sides. A new node is placed beside the node its incoming transition comes from, clear of every other node. The flip side: asking the assistant to move existing nodes or tidy the canvas does nothing, so arrange nodes by hand. The XML dialog’s Load is not adjusted; it loads exactly what you paste.
- The assistant never writes. Apply fills the canvas; saving stays your action. Each tool
re-checks
Updateon theWf_DefinitionsViewresource itself. - A proposal is validated before you see it. It runs the same checks Save does (it parses, has a Start node, and its binding validates) plus a few Save cannot catch: exactly one Start node, unique node keys, transitions between existing nodes,
OnApprove/OnRejectnaming a transition that leaves that node, and an unchangedName. When the model gets it wrong, the validator’s message goes back to it and it corrects itself. - There is no data preview. Workflow SQL runs with runtime values substituted in, and some of it
writes, so the model probes tables with
execute_queryinstead of executing your SQL. - Apply needs this page open, and is refused while you are viewing an old version or an instance preview.
- “Reply in chat” — or “don’t change the workflow” — answers in the conversation and withholds the apply tool for that turn.
The assistant reads the saved definition, so Save before asking if the canvas has changes you want it to see.