Skip to content

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.

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: 2
start: start
nodes:
start:
type: start
to: end
end:
type: end
outcome: completed

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

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.

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.

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: 2
start: start
nodes:
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: resource is entity, boardItem or boardStatus; key identifies 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 use anyOf: [{resource: boardItem, key: REVIEWER}, {resource: boardStatus, key: APPROVER}] instead of the single resource/key pair. Any selected responsibility grants eligibility. The same key in different scopes remains a distinct selection.
  • inheritPermission: permissions contains {key, transition?} entries; match is all (default) or any. 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: expression is 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.

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.

Both nodes run automatically; neither waits for a person.

version: 2
start: start
nodes:
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: subject observes the process context itself.
  • sourceKind: identities lists up to 100 explicit records as {resource: entityRecord|boardItem, definitionId, objectId}; definitionId is 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 same OperationId and 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: path observes related records using typed changes. A Reference admits linkChanged and recordChanged; an InverseReference collection admits membershipChanged and recordChanged. 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. Raw eventTypes and filter are 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.

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.

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: 2
start: start
nodes:
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.

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: review

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