Authoring a workflow
A workflow definition is a single XML document with a <Workflow> root, a <Nodes> block, and a
<Transitions> block. This page is the full element and attribute reference, with real snippets from
the Inventory samples. For the mental model, start with the overview.
Root — <Workflow>
Section titled “Root — <Workflow>”<Workflow Name="ProductApproval" Slug="product-approval" Label="Product Approval" EntityType="WizardFormResponse" Category="Inventory"> <Nodes> … </Nodes> <Transitions> … </Transitions></Workflow>| Attribute | Required | Meaning |
|---|---|---|
Name |
yes | Unique, immutable lookup key. Wizard/view hooks reference the workflow by this name; it is not changed on save. |
Label |
yes | Human-readable display name. |
Slug |
no | URL slug; omit to derive a kebab-case slug from Name. |
EntityType |
no | The kind of entity this workflow runs over (e.g. WizardFormResponse, or a view name like PurchaseOrders). |
Category |
no | Grouping label for the designer/listing. |
Nodes — <Node>
Section titled “Nodes — <Node>”<Node Key="PurchasingReview" Type="Approval" Label="Purchasing Review"> <Position X="360" Y="200" /> …</Node>| Attribute / child | Meaning |
|---|---|
Key |
Unique node identifier, referenced by transitions’ From/To. |
Type |
One of Start, Standard, Approval, Action, Decision, Assignment, End. |
Label |
Display label. |
<Position X Y/> |
Canvas coordinates for the designer. Samples also carry Width/Height on Start/End; those are designer layout hints and are ignored by the engine. |
<Description> |
Optional free text. |
Activity attributes (ActivityType, ActivityRoute, ActivityTitle, ActivityFormStyle) point
a node’s task at a form or view — e.g. what opens when a user works the node in a task list.
ActivityFormStyle is Modal (default) or Redirected. Approval nodes default their activity to the
built-in WF_ApprovalForm, so you rarely set these by hand.
Every workflow needs exactly one Start node — saving fails otherwise.
Approval node
Section titled “Approval node”An Approval node pauses the flow until its gate is satisfied. It carries an optional <Sla> and a
required <ApprovalConfig>.
<Node Key="PurchasingReview" Type="Approval" Label="Purchasing Review"> <Position X="440" Y="220" /> <Sla TimeoutMinutes="1440" Escalation="Notify" ReminderMinutes="720" /> <ApprovalConfig> <Type>Any</Type> <Approvers> <Approver Type="Role" Value="Purchasing" /> </Approvers> <OnApprove Transition="PurchasingApproved" /> <OnReject Transition="Rejected" /> </ApprovalConfig></Node><ApprovalConfig>
| Element | Meaning |
|---|---|
<Type> |
Gate policy: Any, Sequential, or Parallel. See Engine — approvals. |
<Approvers> / <Approver> |
Who may decide. Type="Role" with Value="RoleName", Type="User" with a numeric user id, or Type="FieldValue" with a @ContextVar holding a user id. An <Approver> may also carry an <Sql> child (reserved for custom resolution). |
<OnApprove Transition="…"/> |
Transition Label to follow when the gate approves. |
<OnReject Transition="…"/> |
Transition Label to follow when the gate rejects. |
<Sla> — a deadline hint on the node:
| Attribute | Meaning |
|---|---|
TimeoutMinutes |
Minutes until the approval is considered breached. |
Escalation |
Escalation action label (e.g. Notify). |
ReminderMinutes |
Optional — minutes after which a reminder is due. |
<EscalationConfig> |
Optional child for extended escalation settings. |
Action node
Section titled “Action node”An Action node runs a side effect and then auto-advances through its single outgoing transition.
<Node Key="NotifyPurchasing" Type="Action" Label="Notify Purchasing"> <Position X="240" Y="220" /> <ActionConfig> <ActionType>AppNotification</ActionType> <Payload> <To>Role:Purchasing</To> <Message>Purchase Order {{Number}} for {{TotalAmount}} was submitted and is awaiting review.</Message> </Payload> </ActionConfig></Node><ActionType> is case-insensitive and one of:
| ActionType | <Payload> shape |
Effect |
|---|---|---|
ExecuteSql |
Raw parameterised SQL text. | Runs the SQL against the database. |
AppNotification |
Child elements <To> and <Message>. |
Queues an in-app notification (delivered over SignalR/web-push, no extra config). |
SendEmail |
Child elements <To>, <CC>, <Subject>, <MailBody>. |
Queues an email; only sends when SMTP (Notifications:Email) is configured. |
Token interpolation. Subject/MailBody/Message are Handlebars templates rendered over the
workflow context — e.g. {{ProductName}}, {{Number}}, {{TotalAmount}}, and the special
{{FlowStatus}}. Plus {{EntityId}}, {{InstanceId}}, {{EntityType}}.
Recipients. <To> and <CC> are comma/semicolon-separated and each entry is one of:
Role:RoleName— every member of the role,@ContextVar— a context variable holding a user reference (e.g.@AssignedToUserId),- a literal email address (for
SendEmail).
AppNotification resolves recipients to usernames; SendEmail resolves them to email addresses.
ExecuteSql payloads run parameterised with @RecordId, @EntityId, @InstanceId, @FlowStatus,
the session parameters, and every context variable (@ContextVar). Example:
<Node Key="Activate" Type="Action" Label="Activate Product"> <Position X="700" Y="200" /> <ActionConfig> <ActionType>ExecuteSql</ActionType> <Payload>UPDATE Inventory.Products SET Status = 'Active', IsActive = 1, UpdatedAt = GETDATE() WHERE Id = @ProductId;</Payload> </ActionConfig></Node>Decision node
Section titled “Decision node”A Decision node has no config of its own — it auto-advances down the first outgoing transition (by
Order) whose Expression passes. Give the fallback branch Expression="true" and the highest
Order:
<Node Key="ThresholdCheck" Type="Decision" Label="Total > 2000?"><Position X="660" Y="220" /></Node>…<Transition From="ThresholdCheck" To="AdminReview" Expression="@TotalAmount > 2000" Order="1" FlowStatus="Awaiting Admin Approval" /><Transition From="ThresholdCheck" To="Approve" Expression="true" Order="2" />Assignment node
Section titled “Assignment node”An Assignment node sets the instance’s assigned role/user (so it lands in the right queue). Its
<AssignmentConfig> accepts <RoleName> / <UserName> (static, looked up by name) or
<RoleFromContext> / <UserFromContext> (a @ContextVar that takes precedence), plus <ClearUser>
(default true).
Transitions — <Transition>
Section titled “Transitions — <Transition>”<Transitions> <Transition From="Start" To="NotifyPurchasing" Label="Submit" Order="1" FlowStatus="Submitted" /> <Transition From="PurchasingReview" To="ThresholdCheck" Label="PurchasingApproved" FlowStatus="Purchasing Approved" /> <Transition From="PurchasingReview" To="NotifyRejected" Label="Rejected" RequireComment="true" FlowStatus="Rejected" /></Transitions>| Attribute | Meaning |
|---|---|
From |
Source node Key. |
To |
Target node Key. |
Label |
Human name; also the value OnApprove/OnReject and view hooks reference. |
Expression |
Optional guard (see below). On a manual/Decision edge it must pass for the edge to fire. |
Order |
Evaluation order for Decision branching (lowest first). |
RequireComment |
true forces a comment when this transition is taken (e.g. a rejection). |
FlowStatus |
Business status stamped when this transition fires (sticky — see below). |
Transition expressions
Section titled “Transition expressions”The expression evaluator supports:
- comparisons
==,!=,>,<,>=,<=, IS NULL/IS NOT NULL,- boolean
AND/OR(quotes are respected when splitting), - the literals
true/false.
Operands are @ContextVar references or literals. Numbers are compared numerically; strings are
compared case-insensitively. @FlowStatus is available as an operand. An expression the evaluator
can’t parse returns false (fail-safe). Full details in Engine — expressions.
FlowStatus is sticky
Section titled “FlowStatus is sticky”A transition’s FlowStatus only overwrites the instance’s current business status when the value is
non-empty. A Decision fallback like Expression="true" with no FlowStatus therefore leaves the
prior status untouched. FlowStatus is distinct from the engine’s lifecycle Status
(Active/Completed/Cancelled). Each workflow defines its own status vocabulary — there is no
global enum.
Complete sample
Section titled “Complete sample”The two end-to-end references are in sample/Inventory/models/workflows/:
ProductApproval.workflow.xml— a wizard-started approval that activates a product on approval.PurchaseOrderApproval.workflow.xml— a view-started approval with a threshold Decision, an Admin sign-off branch, and bothAppNotificationandSendEmailactions.
Import either through the Workflow Designer (/workflow-designer → paste → Save, keeping the
Name). See Versioning & company scoping for what happens on save.