Workflow process document and nodes
The process document is owned by Moltaro.Workflow.Engine 5.2.0. The supported
Workflow SDK is 5.2.0; the YAML format version is 2. The
configuration services store revisions;
runtime services execute the pinned document.
For visual authoring, follow Set up Workflows.
Generated process document
Section titled “Generated process document”Moltaro.Workflow.Engine.Documents.WorkflowProcessDefinition.CreateMinimal()
creates the typed Start/End model. WorkflowYamlCodec.Serialize(model) generates
YAML; Parse(yaml) returns a validated model or diagnostics. The model has a
format Version, Start node Id and Nodes dictionary. Start and End are dedicated
node types; transitions belong to nodes.
version: 2start: startnodes: start: type: start to: end end: type: end outcome: completedRevision creation generates Start and End automatically, resolving the owning
Definition’s current enabled default outcome. The example shows a fresh Definition
with completed. Lists do not load the document body.
Content queries return the exact stored YAML and uppercase hexadecimal SHA-256
of its UTF-8 bytes. Publication checks both the body and its hash.
Never-published Drafts can save supported YAML through the content API/SDK or
visual Designer. Active and Deprecated content, including visual data, is immutable.
The Designer edits Start, End, Delay, DevLog, User Input, If and Wait Event nodes
and their transitions. Free-form YAML editing in the UI remains a future increment.
Draft content and Designer
Section titled “Draft content and Designer”Every node may have a title of 1–200 characters. Omit it to use the node’s
default presentation. User Input requires a title before publication.
Titles are presentation text; node keys remain stable
transition identities. The Designer uses a supplied title and shows an If
condition preview with bounded wrapping.
WorkflowProcessDefinition.Designer optionally holds Engine-owned node positions,
input/output anchors, ordered manual waypoints and viewport. It serializes into the same version-2 YAML designer section.
Documents without visual data remain valid. Opening or validating a document does
not rewrite its bytes. Copies retain all bytes and publishing freezes both process
and visual data. No Vue Flow state or second collection of edges is stored.
Input dictionaries use stable node-local keys; without one, a non-Start node has
one left-centered input named in. Outputs use semantic keys: to, a User
Input choice key, true/false of an If, or event/timeout of a Wait Event. WorkflowDesignerOutput.TargetInput names an input on the
semantic destination. Waypoints holds canvas coordinates; empty means automatic
routing. Sides are Top/Right/Bottom/Left and offsets range from zero to one.
Computed routes and bridge arcs are never stored. The server validates visual
references independently of the client router.
An output’s optional display: link draws a short destination link instead of
a routed line. It requires a connected output and cannot carry waypoints. Omitting
it uses line. This choice changes presentation, not execution.
Read content before editing. WorkflowRevisionContentModel provides a consistent
DefinitionYaml, ContentHash, RowVersion and CanEditContent snapshot. Validate
with ValidateWorkflowRevisionContentAsync; save with
SaveWorkflowRevisionContentAsync. Diagnostics include localized Message, stable
Code, Path and optional one-based Line/Column.
var content = await queries.GetWorkflowRevisionContentAsync(definitionId, revisionId, ct);if (!content.Success || !content.Data!.CanEditContent) throw new InvalidOperationException("Revision content is not editable.");var model = WorkflowYamlCodec.Parse(content.Data.DefinitionYaml).Document ?? throw new InvalidOperationException("Unsupported process document.");var visuals = new Dictionary<string, WorkflowDesignerNode>( model.Designer?.Nodes ?? new Dictionary<string, WorkflowDesignerNode>());var entry = visuals.GetValueOrDefault(model.Start) ?? new WorkflowDesignerNode(null);var outputs = new Dictionary<string, WorkflowDesignerOutput>( entry.Outputs ?? new Dictionary<string, WorkflowDesignerOutput>());var output = outputs.GetValueOrDefault("to") ?? new WorkflowDesignerOutput(WorkflowDesignerSideEnum.Right, 0.5);outputs["to"] = output with { Side = WorkflowDesignerSideEnum.Bottom, Offset = 0.5 };visuals[model.Start] = entry with { Outputs = outputs };var authored = WorkflowYamlCodec.Serialize(new WorkflowProcessDefinition( model.Version, model.Start, model.Nodes){ Designer = new WorkflowDesignerMetadata(visuals, model.Designer?.Viewport)});var validation = await queries.ValidateWorkflowRevisionContentAsync( definitionId, revisionId, new ValidateWorkflowRevisionContentRequest(authored), ct);if (!validation.Success || !validation.Data!.CanSave) throw new InvalidOperationException("Invalid process document.");var saved = await commands.SaveWorkflowRevisionContentAsync(definitionId, revisionId, new SaveWorkflowRevisionContentRequest(authored, content.Data.RowVersion, operationId), ct);if (!saved.Success) throw new InvalidOperationException(string.Join(", ", saved.Errors.Select(e => e.Code)));The example uses Moltaro.Workflow.Engine.Documents and
Moltaro.Package.NET.Workflow.Revisions. Serialization from a typed model creates
a new textual representation; use an AST editor when preserving authored comments.
Saving accepts only supported documents and preserves the exact accepted bytes.
It advances the revision version, not the Definition version. The returned
WorkflowRevisionContentMutation includes the revision and new hash. Same-byte
saves do not create another content change or audit event. Mandatory audit,
receipt and cache invalidation share the write transaction.
Keep the same OperationId and immutable request for an uncertain network retry. An exact replay returns its original hash and revision snapshot, which may predate another editor’s later write; reread content before continuing. Changed payloads with the same identity or stale versions conflict. Preserve local work and require explicit resolution. Active/Deprecated cannot save content regardless of UI state. Subject unavailability does not block editing an existing Draft; publication retains its separate Subject validation.
Draft versus publication validity
Section titled “Draft versus publication validity”Version 2 requires exactly one Start and permits multiple End nodes. The readable
outcome key resolves exactly and only within the owning Definition. There is no
implicit outcome, version-1 compatibility parser or automatic document rewrite.
Draft Save and Copy permit missing transitions/settings, no End, disconnected
nodes and unassigned End outcomes. They still reject unknown fields/types, invalid
entry points, nonexistent node/outcome references, unavailable admission references
and invalid nonblank security expressions. Publish additionally requires all
required settings and outputs, at least one End, every node reachable from Start, a path
from every node to End and an outcome for every End. Cycles are allowed; a path
to End does not guarantee termination of every future execution. A Wait Event
without timeout and onTimeout is valid: only its Event output is required.
Content validation returns CanSave and CanPublish separately. Diagnostics lists
save blockers; PublicationDiagnostics lists all publication blockers. The Designer
can save an unfinished Draft and show what must be corrected before publication.
WorkflowProcessValidator.Validate and the YAML parser check structure;
ValidateOutcomes checks the supplied exact keys;
ValidateForPublication checks a complete graph. API/SDK additionally resolve
Definition-owned references and enforce permissions and history protection.
Delay, DevLog and User Input
Section titled “Delay, DevLog and User Input”Version-2 documents support seven node types: Start, End, Delay, DevLog, User Input, If and Wait Event (see If and Wait Event). Existing version-2 Start/End documents remain valid. The designer can add, remove and connect nodes, edit their settings and preserve comments in the YAML. Start is unique and cannot receive incoming transitions or be deleted.
version: 2start: startnodes: start: {type: start, to: wait} wait: {type: delay, duration: {value: 1, unit: minutes}, to: log} log: {type: devLog, message: 'Review requested', level: information, to: decision} decision: type: userInput title: Review request choices: - {key: approve, title: Approve, to: end} - {key: retry, title: Review again, to: wait} admission: {mode: inheritPermission, match: all, permissions: [{key: Update}]} end: {type: end, outcome: completed}Delay accepts a positive whole duration up to 2147483647 and a unit: seconds,
minutes, hours or days. DevLog accepts a message template and a level:
trace, debug, information, warning, error or critical. Both have one
to output. A new Delay starts at one minute; DevLog defaults to information.
User Input has a title, optional instruction and ordered choices. Each choice
has a unique key (up to 64 characters), title and to. A new node has one
Continue choice and an unconfigured admission rule. Incomplete nodes can be
saved in Draft; they cannot be published.
Workflow Configuration Manager can configure and publish the rule with Workflow permissions alone. Additional Entity/Board administration permissions are not required. Available metadata describes configuration, not access to business records. Choose one of three distinct admission modes:
responsible:resourceisentity,boardItemorboardStatus;keyidentifies an enabled responsibility in that scope. Board Status means the item’s current status, not a fixed status selected in the workflow. For multiple selections useanyOf: [{resource: boardItem, key: REVIEWER}, {resource: boardStatus, key: APPROVER}]instead of the singleresource/keypair. Any selected responsibility grants eligibility. The same key in different scopes remains a distinct selection.inheritPermission:permissionscontains{key, transition?}entries;matchisall(default) orany. Entity supports Update, Archive and Delete. Board Item supports EditBoardData, MoveItem, UseTransition with a specific transition, and RemoveItem. The picker shows only currently available entries. Permissions of a Board Item’s related Entity are not included.dsl:expressionis a Boolean expression in the Subject’s existing Security DSL. The same language, limits and compiler apply. Status conditions can be expressed here without a separate status restriction.
Query metadata through the Definition query facade:
var definitions = services.GetRequiredService<IWorkflowDefinitionAutomationQueryService>();var authoring = await definitions.GetUserInputAuthoringAsync(definitionId, ct);if (!authoring.Success) throw new InvalidOperationException(string.Join(", ", authoring.Errors.Select(e => e.Code)));foreach (var permission in authoring.Data!.Permissions) Console.WriteLine($"{permission.Key}: {permission.Transition}");The equivalent HTTP endpoint is
GET /api/workspace/workflow/configuration/workflow-definitions/{definitionId}/user-input-authoring.
Validate, save and publish through the existing revision content operations.
Diagnostics identify the node and its setting, choice or admission field. Missing
or disabled references remain explicit; the server does not replace them silently.
The Worker executes published processes, including Delay, DevLog and User Input. Runtime admission requires visibility of the concrete Subject and rechecks the selected permission rule when accepting a response. No configured rule means no access. Follow the runtime command and status lifecycle above.
DevLog message templates
Section titled “DevLog message templates”A DevLog message can insert scalar context fields with Moltaro template braces,
for example Order {item.Id}: {upper(item.Title)} when the Subject has a
Title field. trim, upper, lower
and coalesce use the existing rule-template grammar; this is not C# execution.
Entity fields use item; Board subjects also expose data and status; use the admitted
item-scoped paths for a particular Board target. Only fields in the exact Subject catalog are accepted.
Runtime variables and optionLabel are not admitted. The shared
template function reference
describes rendering, eager argument evaluation and empty-value handling.
Read the available paths through
IWorkflowDefinitionAutomationQueryService.GetMessageTemplateAuthoringAsync(definitionId)
or GET /api/workspace/workflow/configuration/workflow-definitions/{definitionId}/message-template-authoring.
This operation needs Workflow read permission; it loads metadata without
Entity/Board administration permission or business-record access. Content
validation/save/publish report invalid templates at nodes.<key>.message.
The runtime writes the rendered message to the DevLog when the node runs.
If and Wait Event
Section titled “If and Wait Event”Both nodes run automatically; neither waits for a person.
version: 2start: startnodes: start: {type: start, to: check} check: type: if condition: "COUNT(item.Assemblies) > 0 AND ALL item.Assemblies WHERE (State = 'ready')" onTrue: end onFalse: waitBuilt waitBuilt: type: waitEvent events: mode: any observations: - key: assemblies sourceKind: path path: [item.Assemblies] changes: [membershipChanged, recordChanged] onEvent: check end: {type: end, outcome: completed}If evaluates condition, a Boolean expression in the Subject’s Context
condition language (the language of activation preconditions), against the
persisted data at that moment. Only a valid TRUE takes onTrue (output true);
FALSE and an empty (NULL) result take onFalse (output false). ANY over an
empty collection is false and ALL is true; write COUNT(...) > 0 AND ALL ...
when existence matters. A condition that can no longer be evaluated (a removed
field, an unavailable fact or context) faults the instance with
workflow.runtime.conditionInvalid or workflow.runtime.conditionDependencyUnavailable;
it never chooses False. The decision, its history entry and the next step commit
together, so a retry never re-evaluates a committed decision. A running process
keeps its revision: after conditionInvalid (the data the condition reads has
changed), cancel the process, correct the condition in a new revision and start
it again; Retry only helps once an unavailable dependency is available again.
Wait Event contains a flat group of 1–20 observations, each with a unique
key. Set events.mode to any (OR) to continue after one observation matches,
or all (AND) to wait for a match for every observation, in any order. One event
may match several observations. These modes combine events; they do not assert
that all records currently satisfy a business condition. Use an If → Wait Event
→ If loop for that, as in the example above. Nested groups and Fork are not
supported.
The node takes onEvent (output event) when its group is satisfied, or
onTimeout (output timeout) when its deadline passes. Timeout is optional:
omit both timeout and onTimeout to wait indefinitely. If you set a timeout,
use the Delay duration units and connect its onTimeout output.
Each item in events.observations selects its source:
sourceKind: subjectobserves the process context itself.sourceKind: identitieslists up to 100 explicit records as{resource: entityRecord|boardItem, definitionId, objectId};definitionIdis the Entity Definition or the Board. You can save an Entity record or a Board Item only if it exists (a Board Item on the Board named with it) and you may view it; a missing, a hidden and a Board Item of another Board are reported the same way (workflow.waitEvent.sourceNotFound). This is checked when you configure the process: the running process does not act as you. Retrying a save or copy that already succeeded, with the sameOperationIdand content, returns its stored result even if you can no longer view a record. A Workflow instance is not a resource (workflow.document.eventWorkflowInstanceUnsupported); observe another process through its published Workflow facts instead (below).sourceKind: pathobserves related records using typedchanges. A Reference admitslinkChangedandrecordChanged; an InverseReference collection admitsmembershipChangedandrecordChanged. Select both to observe both membership and content. Membership covers additions, removals and moves between parents, from either side of the relationship. Record changes include updates, lifecycle changes and Board/Workflow fact changes of the reached records. A membership-only write does not also count as a content update. A path follows up to five singleton References and can end in one collection:[item.Customer, Assemblies]is allowed,[item.Assemblies, Components]is not. Cyclic Reference chains are not expanded. RaweventTypesandfilterare unavailable for related paths; put business conditions in If or Wait Until.
Related paths are available for Entity process contexts. A Board Item context can observe itself or explicit Entity records/Board items, but has no related Entity-path source.
For direct sources (subject or identities), eventTypes lists created, updated, archived, restored, deleted and
contextChanged (Board Items publish only Created, Updated, Deleted and Context
changed). contextChanged requires facet: boards with the exact boardId,
or facet: workflow with the exact workflowDefinitionId: its published facts
changed. The selectors follow the observed records: a Board configured as a
context of their Entity Definition, and another Definition that runs on those
records or Board Items. Related recordChanged observations already include
facts of the reached records, without a facet selector. Other selectors are rejected
(workflow.waitEvent.boardNotAssociated,
workflow.waitEvent.definitionNotApplicable). A Definition cannot observe its
own facts.
updated means an update operation on the observed record, even if its values
remain the same. contextChanged means its selected Board or Workflow facts
changed; it does not pretend that the record’s own fields were updated.
Event semantics:
- An observation is armed on its first Wait Event entry, or before evaluating an If directly connected to that Wait Event. This closes the gap between checking state and starting to wait. Events committed before its boundary never count; an event committed later counts even if its transaction started earlier. Later visits of the same node in the instance reuse the observation, so events between visits are kept.
- Any consumes the oldest matching event; All accumulates one match per observation. Further events remain available for later visits. Duplicate delivery never counts twice. Timeout consumes no matches: partial All progress remains available if the process returns to this Wait Event. An Event exit consumes its matches, so they cannot satisfy a later visit.
- An event shows that something happened, not that a state still holds. To continue only while a state is true, check it with an If after the wait.
- When a timeout is configured, each visit has its own fixed deadline. When processing finds the deadline has passed, Timeout wins even over an event that arrived earlier; that event stays for the next visit. Retries, suspension and downtime never extend a deadline.
- While an instance is suspended, events are retained but no exit happens. After Resume, an expired wait takes Timeout.
- Cancel and End close the observation. A replaced event stream (for example
after a database restore) faults the wait with
workflow.runtime.observationStreamChanged; it never completes it.
Read the admitted source modes, categories, Boards and Workflow Definitions with
IWorkflowDefinitionAutomationQueryService.GetWaitEventAuthoringAsync(definitionId)
or GET /api/workspace/workflow/configuration/workflow-definitions/{definitionId}/wait-event-authoring.
Validate one source without saving with ValidateWaitEventSourceAsync or
POST .../wait-event-authoring/validate; enum values are their numeric positions.
Revision content validation, save and publication apply the same rules and
report diagnostics at nodes.<key>.events.observations.<observation>.<field>, nodes.<key>.condition and
nodes.<key>.timeout. Runtime history entries carry OutputKey (true,
false, event, timeout, a choice key or to) and, for an Event exit, the
opaque TriggerEventId; both require Subject View.
The published POST .../wait-event-authoring/filter and SDK
GetWaitEventFilterAuthoringAsync return related-record expression examples.
They do not make a filter setting valid on typed path observations. Use the
condition authoring surface and If or Wait Until for business-state tests.
How related changes reach a wait
Section titled “How related changes reach a wait”The changed record publishes its own durable resource event. Related-context
routing identifies matching registered paths, then confirms the relationship and
the selected kind of change before recording a receipt for an open observation.
Adding or moving a child can therefore satisfy membershipChanged even when the
Order’s own fields were not edited. Updating that child’s state can satisfy
recordChanged. No synthetic Order Updated event is broadcast to every parent.
Receipts and progress belong to the Workflow instance and survive Worker restart. The wake schedules execution; the next If reads current persisted data. It does not assume that a condition was true merely because an event arrived.
Wait Until
Section titled “Wait Until”waitUntil combines the If condition and Wait Event group in one step. It accepts
all the same observation sources, Any/All and optional timeout. Its outputs are
onSatisfied (required for publication) and onTimeout (only with timeout).
version: 2start: startnodes: start: {type: start, to: wait} wait: type: waitUntil condition: "item.Title = 'Ready'" events: mode: any observations: - key: context sourceKind: subject eventTypes: [updated] onSatisfied: done done: {type: end, outcome: done}Observation begins before the first current-state condition check. Initial TRUE
exits Satisfied without requiring an event. FALSE or NULL waits; a complete group
causes a fresh check. A false recheck consumes its group and waits in the same visit,
without dropping distinct pending events or reusing consumed events. Invalid or
unavailable conditions fault exactly as If does. Runtime history records
satisfied or timeout as the final output, without internal check/wait nodes.
An optional timeout: {value: 1, unit: hours} requires onTimeout and applies
from the original visit entry. Retries, events, suspension and downtime do not
extend it. When runnable processing finds it expired, Timeout wins before another
condition check or queued event. A deliberately authored return to the node starts
a new visit and fresh observation scope; standalone Wait Event retains its
existing cross-visit progress semantics. No periodic condition polling is added.
Move on Board
Section titled “Move on Board”moveOnBoard moves a frozen selection through normal Board rules as the trusted
System User. It has required destinationStatusId, optional sourceStatusId,
onSuccess and onFailure. Example node (replace the ids with configuration ids):
move: type: moveOnBoard title: Close assemblies selection: sourceKind: subject path: [item.Assemblies] boardId: assembly-board-id destinationStatusId: completed-status-id onSuccess: done onFailure: reviewSelection sourceKind is subject, boardTarget or fixedBoardItem.
Subject selects the exact Board Item or starts an Entity path. Board target
requires targetEntityDefinitionId and an actual matching external Entity.
Fixed selection requires boardId and boardItemId, without a path or target
type. Authoring requires current access to explicitly selected cards.
Paths admit up to five singleton references and one inverse collection, including singleton suffixes. The limit is 500 source members before deduplication; exceeding it refuses before effects. Selection is frozen at entry. Later additions need a new visit. Missing targets remain refusals; an empty collection succeeds.
Each card commits independently, so Failure may include successful moves. An accessible exact pass already at destination succeeds without a movement or event after the optional source check. Retry recovers the same attempt; a new authored visit makes a fresh selection. The node does not create cards, reopen passes, change Subject or compensate earlier moves.