Skip to content

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.

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

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.

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>

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 &gt; 2000?"><Position X="660" Y="220" /></Node>
<Transition From="ThresholdCheck" To="AdminReview" Expression="@TotalAmount &gt; 2000" Order="1" FlowStatus="Awaiting Admin Approval" />
<Transition From="ThresholdCheck" To="Approve" Expression="true" Order="2" />

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

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.

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.

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 both AppNotification and SendEmail actions.

Import either through the Workflow Designer (/workflow-designer → paste → Save, keeping the Name). See Versioning & company scoping for what happens on save.