# Workflow Engine .NET URL: https://docs.workflowengine.io/ # Workflow Engine Documentation Workflow Engine .NET is a .NET library for workflow automation. It embeds durable, auditable business process automation into any ASP\.NET or .NET application, approvals, document routing, task assignment, with no external service to manage. Get started in 15 minutes with the latest Workflow Engine Free - no license key required. Get a free trial license to unlock commercial features during evaluation. If you are using an AI agent, the agent can obtain a trial key by following the instructions at trial.workflowengine.io/llms.txt. ## Where to start Choose your path based on what you need. ## Explore documentation Browse the full documentation by section. ## Frequently asked questions Common questions about Workflow Engine and how to get started with it. **What is Workflow Engine?** Workflow Engine is an embeddable .NET library for workflow automation. You add it as a NuGet package to your application - it supports .NET Framework 4.6.2 and above, as well as modern .NET and ASP\.NET Core. Connect it to your existing database, and it manages process state, rules, timers, and actions, no separate server required. **Where do I start with Workflow Engine?** Start with Get Started, three short pages take you from installing the NuGet package to running your first workflow instance in under 15 minutes. **Which databases does Workflow Engine support?** Workflow Engine supports SQL Server (2017+), PostgreSQL, MySQL, Oracle, SQLite and MongoDB. You select a provider at startup by installing the matching NuGet package. **Is Workflow Engine free?** Workflow Engine has a free Workflow Engine Free (latest release, updated regularly) that requires no license key. Commercial editions add clustering, scheme inlining, and unlimited process instances. See Workflow Engine Editions for details. **How is Workflow Engine different from building a custom state machine?** Workflow Engine provides persistence, timers, rule evaluation, a visual designer, scheme versioning, clustering, inbox/outbox, and a plugin system out of the box. See Why Not Build Your Own for a time-cost comparison. --- ## Resources - [Trial Key Portal](https://trial.workflowengine.io/): Get a free trial license key for Workflow Engine. Self-service portal - obtain a TRIAL-prefixed key to evaluate the full product. - [API Reference](https://api.workflowengine.io/): Browse the auto-generated API reference for Workflow Engine .NET libraries - namespaces, types, methods, properties, and inheritance. --- # Evaluate The Evaluate section helps you decide whether Workflow Engine is the right fit for your project. Browse through the full feature set including the visual designer, process versioning, clustering, and multitenancy. Compare Workflow Engine editions against each other and against alternatives like Elsa and Hangfire. Review the licensing model, request a trial key, and plan your migration from Elsa 2 if you are coming from that platform. --- # Evaluate Workflow Engine URL: https://docs.workflowengine.io/evaluate/evaluate-workflow-engine/ # Evaluate Workflow Engine Workflow Engine is an embeddable .NET library that adds workflow automation, business process approvals, document routing, task assignment, to any .NET application. It runs inside your process as a NuGet package; there is no separate server to manage. WorkflowRuntime persists all process state to your existing database. This page covers features, architecture, deployment modes, and how to evaluate the product. ## How does Workflow Engine work? `WorkflowRuntime` is the central object, it manages process state, evaluates rules, fires timers, and runs actions. You define a process as a scheme (XML or via the visual Designer), and `WorkflowRuntime` executes it. Workflow Engine runs inside your ASP\.NET Core or .NET application as a library. You can call `WorkflowRuntime` methods directly from C# code, or use the built-in HTTP API to access the same functionality over HTTP. `WorkflowRuntime` persists all state to your existing database through a swappable persistence provider (SQL Server, PostgreSQL, MySQL, Oracle, or MongoDB). The Designer is a frontend component that embeds in your application (React, Angular, Blazor, or vanilla JS). End users create and modify workflow schemes through the Designer without touching code. Workflow Engine supports two deployment modes: single-server mode runs all processing on one instance with no coordination overhead, while multi-server mode distributes process execution across multiple instances that share the same database. Both modes use `WorkflowRuntime` as an in-process library inside each application instance. ## Multitenancy: serve multiple customers from one deployment Workflow Engine supports multitenancy so a single deployment can serve multiple customers or business units with process data isolation. Two models are available: physical tenancy gives each tenant its own database and `WorkflowRuntime` instance, while logical tenancy uses a shared runtime and database with `TenantId`-column filtering. You can even combine both models in the same deployment - give a regulated client its own database while smaller clients share one with filtering. See the Multitenancy feature page for setup code and details. ## Tested and stable: 10+ years of backward-compatible releases Workflow Engine has shipped production releases for over 10 years with guaranteed backward compatibility across major versions. Schemes written for an earlier version continue to run on current releases without schema migration or code changes. For teams building long-lived systems, backward compatibility reduces upgrade friction. A major version upgrade does not require rewriting process definitions or migrating the persistence schema. The guarantee covers the full version history - schemes from early versions run on the current release unchanged. ## Who is this for? - **Developer or tech lead** - start with Features, then Why Not Build Your Own. - **CEO or CTO** - start with Features, then Editions. - **Product Manager** - read Features and Editions in parallel with your technical team. ## Try it free Get a 30-day trial key at trial.workflowengine.io - no credit card required. The trial key unlocks all features for evaluation. See the License Key page for details. ## Start using Workflow Engine Install Workflow Engine via NuGet and run your first process instance in 15 minutes. Workflow Engine Free requires no license key and is suitable for evaluation and small projects. When you are ready for production, request a commercial license to unlock clustering, scheme inlining, and unlimited process instances. ## See also ## Frequently asked questions **What is Workflow Engine?** Workflow Engine is an embeddable .NET library that adds workflow automation, approvals, document routing, task assignment, to any .NET application. It runs inside your process as a NuGet package; `WorkflowRuntime` manages all process state, rules, timers, and actions, and persists everything to your existing database. **Does Workflow Engine require a separate server to run?** No. Workflow Engine runs inside your ASP\.NET Core or .NET application as a library. There is no external service, no agent process, and no sidecar. You initialize `WorkflowRuntime` once at startup, register it with your DI container, and call its methods directly from C#. **What databases does Workflow Engine support?** Workflow Engine ships persistence providers for SQL Server, PostgreSQL, MySQL, Oracle, and MongoDB. You select a provider at startup by registering the matching NuGet package, for example, `WorkflowEngine.NETCore-ProviderForMSSQL` for SQL Server or `WorkflowEngine.NETCore-ProviderForPostgreSQL` for PostgreSQL. **What is the difference between Free and Commercial editions?** Workflow Engine Free requires no license key and includes the full workflow engine with one runtime instance. Commercial editions (Enterprise, NEO, Ultimate) add clustering for multi-server deployments, scheme inlining, multitenancy, and unlimited process instances. See Workflow Engine Editions for a detailed comparison. **Is Workflow Engine backward compatible across major versions?** Yes. Workflow Engine guarantees backward compatibility across major versions. Schemes and persistence data from earlier versions continue to work on current releases without migration scripts or code changes. --- # Workflow Engine Features URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/ # Workflow Engine Features Workflow Engine ships 24 production-ready capabilities. Core features include a visual designer for end users, process versioning that keeps running instances stable during updates, horizontal clustering for scale-out, multitenancy with isolated process data, an HTTP API for non-.NET clients, pluggable security, and a plugin system. Each capability replaces a custom build that would otherwise require weeks of development and ongoing maintenance. ## Major features ## Additional features ## Frequently asked questions **What features does Workflow Engine support?** Workflow Engine ships over 20 production-ready features covering visual design, scheme versioning, clustering, multitenancy, timers, pluggable security, BPMN import/export, an HTTP API, a plugin system, direct state control, process logging, work calendars, pre-execution simulation, programmatic scheme creation, database versioning, offline operations, and workflow templates. The cards above list each feature with a link to its dedicated page. **Do all features require a commercial license?** No. Core features - the runtime, persistence, timers, conditions, rules, and actions - are available in all editions. Commercial features include clustering, multitenancy, scheme inlining, and certain advanced capabilities. See the Workflow Engine Editions page for the full breakdown. **Can I evaluate the features before purchasing?** Yes. The Workflow Engine Free includes all core features and is suitable for evaluation and small projects. There is no time limit or feature gate during evaluation - you install the NuGet package and run your first process in minutes. When you are ready for production features, request a commercial license. **What is the difference between a feature and a plugin?** A feature is a core capability built into the Workflow Engine runtime (versioning, timers, multitenancy, etc.). A plugin is an optional NuGet package that adds reusable workflow patterns (approvals, loops, file operations, Microsoft Entra ID integration). Plugins extend what you can do in a scheme but depend on the runtime to execute. See the Plugin System page for details. ## Next steps --- # Visual Designer URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/visual-designer/ # Visual Designer Every time a business process changes - a new approval step, a different routing rule, an updated SLA - somebody has to file a ticket and wait for a developer to update the code. The wait can take days or weeks. By the time the change ships, the policy that motivated it may have changed again. The Visual Designer is a drag-and-drop editor that lets business users create and modify workflow schemes without writing code. They draw activities on a canvas, connect them with transitions, and configure commands, timers, and rules through property panels - all from within your application. With the Visual Designer, process changes ship in hours, not sprints. An HR coordinator adjusts an onboarding sequence and deploys it the same day. A compliance officer adds a review step to a procurement flow without filing a developer ticket. The scheme the business user designs is the scheme `WorkflowRuntime` executes - no translation layer, no back-and-forth with engineering. ## What it is The Visual Designer is an embeddable component that renders inside your application. Business users see a canvas where they drag activity blocks (approval steps, document reviews, system actions) and connect them with transitions. Each activity has a property panel where the user configures commands (what moves the process forward), timers (when to auto-advance or escalate), conditions (when a transition is allowed), and rules (who can act and what happens next). Think of it as a whiteboard that executes. The diagram the user draws is not a specification for developers to implement - it is the running workflow. Changes take effect immediately when the user saves and deploys through the Designer. The Designer also ships with a built-in library of pre-built workflow schemes that appear in the **Templates** tab in the elements sidebar. These templates demonstrate common patterns - approval flows, parallel processes, timer-based escalation - and users can drag any template onto the canvas to use it as a starting point. Templates are grouped by category, and each one includes a description so users knows what it does before they drag it in. **For developers:** the Designer ships as npm packages for React, Angular, and vanilla JavaScript (`@optimajet/workflow-designer`). It communicates with `WorkflowRuntime` through a controller endpoint in your application. Your job is to mount the component and configure the controller - the Designer handles the rest. The template library is customizable: point the Designer at a URL that serves your own set of template files, and it loads those instead of the built-in ones. Templates you do not replace keep working from the built-in defaults. See Embed Designer in React for setup. ## Why it matters The Visual Designer delivers these outcomes: - **Process changes in hours, not weeks** - business users modify workflows directly, without filing tickets or waiting for the next deployment cycle. A procurement manager adds an escalation step during lunch; it is live after lunch. - **No translation errors** - the diagram the business stakeholder reviews is the exact scheme that runs in production. There is no requirements document to misinterpret, no handoff between business and engineering. - **End-user process modeling in SaaS products** - embed the Designer per-tenant so each customer builds and manages their own workflows. The Designer respects tenant isolation when connected to a multitenant `WorkflowRuntime`, so one deployment serves many tenants without data leakage. - **Full design capability without code** - users configure commands, conditions, timers, actions, rules, and annotations through property panels. They can build complex multi-step processes - parallel approval, timed escalation, conditional branching - entirely through the visual interface. - **Pre-built templates as starting points** - the Designer ships with a library of common workflow patterns. Business users drag a matching template onto the canvas instead of building from scratch, reducing design time and ensuring consistent process structure. ## Who it is for **Evaluator (CEO, CTO, PM):** the Designer removes the engineering bottleneck from process changes. Your team ships workflow updates without touching the application codebase, reducing the cost of process maintenance and accelerating time-to-market for workflow-dependent features. In a SaaS product, the Designer lets customers self-serve their own process configuration, reducing support load and increasing product value. **Developer:** the Designer is a frontend component with a documented API. You mount it, configure the backend controller, and business users take over from there. You are never blocked by business users requesting process changes, and business users are never blocked waiting for you. See Embed Designer in React and Designer Customization for integration details. **Enterprise architect:** the Designer produces standard scheme XML that `WorkflowRuntime` executes directly. The output is portable and version-controlled through the runtime's built-in process versioning. The Designer does not introduce a proprietary storage format or a separate execution engine - it is a presentation layer on top of the same runtime that powers all Workflow Engine deployments. ## When to use it Use the Visual Designer when business stakeholders need to define, modify, or approve workflow logic without developer involvement. Typical examples: - **Approval workflows** - expense reports, purchase orders, contract reviews where the approval chain changes as the organization grows. The business owner adjusts the chain in the Designer; the change is live immediately. - **HR onboarding sequences** - steps, document requests, and system provisioning tasks that vary by department, role, or location. HR configures each onboarding track independently. - **Content publishing pipelines** - draft, review, approve, publish stages where the review requirements differ by content type. Editors modify the pipeline without involving engineering. - **Customer-facing process configuration** - in a SaaS application, tenants configure their own business rules, approval chains, and SLA timers through the embedded Designer. Each tenant sees only their own schemes. - **Templates as starting points** - a team rolling out a new workflow selects a pre-built template from the Designer's Templates tab that matches their process pattern, then customizes it. This is faster than starting from an empty canvas and ensures consistency across similar processes. ## How it compares Workflow Engine supports three ways to define a process - visually, in code, or in XML. Each suits a different audience and use case. The Visual Designer is one of them. Table: Comparison of workflow definition approaches | Approach | Who it is for | Strength | |----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| | **Visual Designer** - drag-and-drop editor in the browser | Business users, analysts, product owners who need to see and modify the process directly | Visual clarity: the process is visible as a diagram. Anyone can understand and change it without reading code or XML. | | **Programmatic** - define the scheme in C# code using `ProcessDefinitionBuilder` | Developers who want version control, code review, and the full power of the language | Full expressiveness: conditions, actions, and parameters are written in C# with compile-time checking and IDE support. | | **Simple Process Notation** - write the scheme as XML directly | Advanced users who need precise control or programmatic generation of scheme files | Portable and diffable: XML is easy to generate, compare, and validate in CI pipelines. | All three approaches produce a scheme that `WorkflowRuntime` can load and execute. You can mix them in the same project - prototype the flow in the Designer, then migrate to `ProcessDefinitionBuilder` when the logic stabilizes, or generate scheme XML from an external system. The Visual Designer is the right choice when the process changes frequently and the people who understand the process are not the people who write the code. ## Frequently asked questions **Can business users really design workflows without writing code?** Yes. The Visual Designer is a drag-and-drop interface with property panels for configuring commands, timers, conditions, and rules. No code, no XML, no SQL. A non-technical user can build a multi-step approval flow in minutes. **Do I need a specific license or edition to use the Designer?** The Visual Designer is included with all Workflow Engine editions. It ships as an embeddable frontend component with a backend controller - no separate license is required to use it in production. **What frontend frameworks does the Designer support?** The Designer ships as npm packages for React, Angular, and vanilla JavaScript. You can also mount it in Blazor or other frameworks using a wrapper component. It communicates with the server through standard HTTP requests and renders in any modern browser. **Can I customize the set of templates that appear in the Templates tab?** Yes. You provide your own template files on a server or CDN, and point the Designer at that location. The Designer loads your templates instead of the built-in ones. Templates that depend on plugins you have not installed are automatically hidden. You only need to provide the templates you want to replace - everything else keeps working from the built-in defaults. See the Designer Customization page for configuration details. **Can I customize the Designer's appearance, available activities, and behavior?** Yes. The Designer exposes configuration for color themes, visible activity types, toolbar buttons, property panel controls, and autocomplete providers. You can hide unused activity types, restrict available commands or actions, and replace default controls with your own. See Designer Customization for the full API. **Does the Designer work with the runtime's existing features like timers, subprocesses, and multitenancy?** Yes. Activities, transitions, timers, commands, conditions, rules, subprocesses, annotations, and localization are all configurable through the Designer interface. When connected to a multitenant `WorkflowRuntime`, the Designer respects tenant isolation - each tenant sees and edits only their own schemes. ## See also --- # Simple Process Notation URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/simple-process-notation/ # Simple Process Notation Most workflow notations were designed for specialists. BPMN defines over 50 element types. UML activity diagrams require formal training. When non-developers need to read or modify a workflow, these notations become a barrier rather than a tool. Simple Process Notation is Workflow Engine's built-in flowchart notation. It uses three building blocks - activities (states), transitions (edges), and commands (triggers) - to define any state-machine workflow. Anyone who has seen a flowchart can read it. No BPMN training, no UML certification, no special tools. With Simple Process Notation, business analysts design and review workflows in the same language developers implement them. The diagram a product manager sketches on a whiteboard maps directly to the scheme that `WorkflowRuntime` executes. ## What it is Simple Process Notation models a workflow as a directed graph. Activities are the states a process instance can occupy, shown as boxes on the canvas. Transitions are the arrows between them, defining which state changes are allowed. Commands are the triggers that move the process across a transition - when a user or system calls `ExecuteCommand`, the runtime checks for an outgoing transition with a matching command and follows it. That is the entire model. There are no gateways (branching is multiple outgoing transitions from an activity, each with its own rule). There are no separate event types (timers are configured as transition properties). There are no pools, lanes, or message flows. Every workflow in Workflow Engine - from a two-step approval to a multi-branch order fulfillment pipeline - is expressed with these three elements. The Visual Designer renders this notation as a flowchart. The Designer saves schemes as XML in Simple Process Notation, which `WorkflowRuntime` loads and executes at runtime. **For developers:** the scheme is stored as XML with a predictable structure. You can read, write, and validate it programmatically without the Designer. The `ProcessDefinitionBuilder` API in C# produces the same runtime model. See the Scheme concept for the XML format reference. ## Why it matters Simple Process Notation delivers these outcomes: - **Shared language across roles** - business analysts, QA engineers, product managers, and developers all read the same diagram. No translation step between "what the business wants" and "what the system does." - **No BPMN toolchain required** - you do not need a BPMN modeling tool, a BPMN certification, or a BPMN runtime. The notation is built into Workflow Engine and works with any text editor. - **Portable and diffable** - the notation is XML. You can store schemes in version control, compare revisions, generate them from templates, and validate them in CI pipelines. - **Full expressiveness without complexity** - conditions, timers, actions, rules, subprocesses, and annotations are all representable within the three-element model. You never outgrow the notation. ## Who it is for **Evaluator (CEO, CTO, PM):** Simple Process Notation eliminates the notation tax. Your team does not need to invest in BPMN tools or training. Flowcharts are universally understood, and the notation maps directly to executable workflows - no gap between design and implementation. **Developer:** the notation is XML with a predictable structure. You can generate, validate, and transform schemes programmatically. The `ProcessDefinitionBuilder` API lets you build schemes in C# with compile-time checking. Compare revisions in pull requests. **Enterprise architect:** the notation is stored as XML files that you can keep in version control, diff in code review, and validate in CI pipelines. The format is independent of any single modeling tool - you can edit schemes in the Visual Designer, in any text editor, or generate them programmatically. ## When to use it Use Simple Process Notation when you want a workflow definition that is easy to read, store in version control, and share across teams. It fits any process that maps to a state machine - and most business processes do. Typical signals that the notation is a good fit: - **Non-developers need to read or review the workflow** - product managers, compliance officers, or business analysts who understand the process but do not write code. The three-element model is readable without training. - **You want to version control workflow definitions** - schemes are XML files. Store them in git, diff them in pull requests, tag releases alongside your application code. - **BPMN is overkill for your use case** - your processes are internal approval flows, document routing, or task sequences. The full BPMN specification adds complexity without benefit. Simple Process Notation covers the same scenarios with fewer concepts. - **You need to generate workflows programmatically** - generate scheme XML from an external system, a configuration file, or a template engine. The predictable XML structure means you can produce valid schemes without the Designer. ## How it compares Workflow Engine supports three ways to define a process. Simple Process Notation is the middle ground between visual modeling and writing code. Table: Comparison of workflow definition approaches | Approach | Best for | Tradeoff | |--------------------------------------------------|--------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------| | **Simple Process Notation** (flowchart XML) | Business analysts and developers who need a readable, portable format that maps directly to execution. | Requires understanding the three-element model. Less expressive than BPMN for cross-system choreography. | | **BPMN 2.0** (import/export via plugin) | Teams that already use BPMN tools and need to exchange diagrams with external partners. | Over 50 element types, requires training. Import fidelity is not lossless. | | **Programmatic** (C# `ProcessDefinitionBuilder`) | Developers who want compile-time safety, version control, and the full power of the language. | Not readable by non-developers. Requires code deployment for every change. | All three approaches produce a scheme that `WorkflowRuntime` can execute. You can choose the approach that fits your team and workflow - use the Visual Designer during prototyping, write XML for version-controlled definitions, or use `ProcessDefinitionBuilder` for compile-time safety. ## Frequently asked questions **How does Simple Process Notation differ from BPMN?** BPMN defines over 50 element types including gateways, events, pools, lanes, and message flows. Simple Process Notation has three elements: activities, transitions, and commands. Branching uses multiple outgoing transitions guarded by rules instead of explicit gateway nodes. Timers and callbacks are transition properties, not separate event types. **Can I import existing BPMN diagrams?** Yes. Workflow Engine supports BPMN 2.0 import and export. The importer maps BPMN tasks to activities, sequence flows to transitions, and start events to initial activities. Complex constructs such as boundary events or multi-instance loops may need manual adjustment after import. **Who can read a scheme written in Simple Process Notation?** Anyone who has seen a flowchart. Activities are boxes, transitions are arrows, and the command name on each arrow tells you what trigger moves the process to the next state. No BPMN training, no UML certification, no special tools required. **Do I need the Visual Designer to create a scheme, or can I write it directly?** Both. The Visual Designer renders and saves schemes in Simple Process Notation. You can also write the XML directly in any text editor or generate it programmatically. The XML format is stable and well-understood by the runtime parser. **Is Simple Process Notation the only notation Workflow Engine supports?** No. Workflow Engine also supports BPMN 2.0 import and export and the programmatic `ProcessDefinitionBuilder` API. Simple Process Notation is the default notation used by the Designer and stored in the persistence layer. You can mix approaches in the same project. ## See also --- # Process Versioning URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/process-versioning/ # Process Versioning Updating a workflow that is already running is risky. If you change the scheme while instances are in flight, some may be mid-transition, some may be waiting on a timer, and others may be in states that no longer exist in the new definition. Without versioning, every deployment forces a choice between aborting running instances or locking the process definition until they all finish. Process versioning lets you publish a new process definition without forcing running instances to change. By default, each running process keeps the definition it started with, while new processes receive the latest published definition. When an in-flight process should move to the new definition, the runtime can update it explicitly or lazily before returning its available commands. With process versioning, you deploy workflow changes on your schedule, not when all running instances happen to finish. A compliance officer adds an approval step to a procurement process; orders already in flight continue under the old rules, and new orders follow the updated flow. ## What it is Think of process versioning like publishing a document revision. When you publish a new version of a document, readers who already have the old copy keep it - they do not get interrupted mid-sentence. New readers automatically receive the latest published version. You decide when to stop distributing the old version. Process versioning works the same way. Each time you save a process definition through the Visual Designer, the system creates a new version. Existing running processes keep the version they started with by default, and new processes start on the latest version. An eligible running process can also migrate after its old scheme is marked obsolete. **For developers:** the runtime exposes `SetSchemeIsObsoleteAsync(schemeCode)` to mark existing scheme records for a given code as obsolete. To enable lazy migration, call `SwitchAutoUpdateSchemeBeforeGetAvailableCommandsOn()` when configuring the runtime and set `IsAutoSchemeUpdate="true"` on safe activities. Before `GetAvailableCommandsAsync` returns commands, the runtime updates an obsolete process only when its current activity permits the update. You can also call `UpdateSchemeIfObsoleteAsync(processId)` explicitly. See Update a Running Scheme for configuration and migration details. ## Why it matters Process versioning delivers these outcomes: - **Zero-downtime workflow updates** - publish scheme changes any time. Running instances are unaffected. There is no deploy-freeze window, no need to wait for all instances to finish before rolling out a change. - **Auditable change history** - each save creates a distinct scheme record. Old versions remain in the persistence store for audit or rollback (if not explicitly cleaned up). You can trace which scheme version a completed instance ran on. - **Controlled cutover** - `SetSchemeIsObsoleteAsync` is a deliberate action. You can save a new scheme, verify it, and deprecate the old one when you are ready. The Designer does this automatically on save, or you can control the timing from code. - **Automatic or selective migration** - eligible running instances can update lazily when the application requests available commands, or you can update instances individually or in bulk via `UpdateSchemeIfObsoleteAsync`. ## Who it is for **Evaluator (CEO, CTO, PM):** process versioning removes the deployment risk from workflow changes. Your team can update process logic without coordinating instance completion times, reducing release cycle time and operational overhead. Process changes ship when they are ready, not when the last running instance finishes. **Developer:** versioning is automatic - every scheme save creates a new record. You manage the cutover with `SetSchemeIsObsoleteAsync`. With the runtime setting enabled, requesting available commands also updates obsolete instances whose current activity has `IsAutoSchemeUpdate` enabled. See Update a Running Scheme for configuration details. **Enterprise architect:** scheme records are stored in the same persistence store as process instances. Each record has a unique GUID identifier and an `IsObsolete` flag. There are no auto-incrementing version numbers, external version registries, or additional infrastructure. The scheme definition is always consistent with the stored scheme record - no drift between what is deployed and what is persisted. ## When to use it Use process versioning whenever your process definitions change over time. It is the default behavior of the runtime - every save through the Designer creates a new version. Typical examples: - **Adding steps to a live process** - a procurement workflow needs an additional manager approval. Save the updated scheme; new purchase orders follow the new path, existing ones continue under the old rules. - **Fixing rule conditions or timer values** - an approval condition was too restrictive, or a timer duration was too short. Update the scheme, mark the old version obsolete, and new instances use the corrected values. - **Phased rollouts** - save a new scheme version, validate it with test instances, then mark the old version obsolete when you are ready to cut over all new instances. - **Regulatory or compliance updates** - a policy change requires a different approval chain. Update the scheme and deprecate the old version. Existing instances keep their original approval chain for audit compliance. ## How it compares Workflow Engine's process versioning is built into the runtime at the persistence layer. The comparison below shows the difference between relying on this built-in mechanism versus building your own. Table: Comparison of process versioning approaches | Approach | What it requires | Result | |------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | **Build your own versioning** (external) | Custom migration scripts, version tables, or branch-per-version workflows. Manual mapping of running instances to scheme versions. | Engineering effort to build and maintain. Risk of drift between deployed and persisted schemes. | | **Workflow Engine process versioning** | Each save creates a scheme record. `SetSchemeIsObsoleteAsync` controls the cutover, and runtime/activity settings control lazy migration. | The runtime keeps each instance bound to a stored scheme until the application updates it. | Workflow Engine keeps each process instance bound to an explicit stored scheme version. The application decides whether an existing instance stays on that version or migrates at a configured checkpoint. ## Frequently asked questions **Does process versioning automatically affect running process instances?** Not by default. Running instances remain on their stored scheme until the application calls `UpdateSchemeIfObsoleteAsync`, or until `GetAvailableCommandsAsync` triggers lazy migration with the runtime setting enabled. Lazy migration also requires `IsAutoSchemeUpdate` on the current activity. **Can I update a running instance to the latest scheme version?** Yes. Call `UpdateSchemeIfObsoleteAsync(processId)` on the runtime. The update proceeds when the stored scheme is obsolete and the current activity has `IsAutoSchemeUpdate` enabled. Pass `ignoreAutoSchemeUpdate: true` to bypass the activity flag. Alternatively, enable automatic update before `GetAvailableCommandsAsync` to migrate eligible instances as users interact with them. **What happens if all scheme versions are marked obsolete?** The runtime creates a new scheme record automatically. `CreateNewProcessAsync` catches the `SchemeNotFoundException` when no non-obsolete scheme exists and generates a fresh scheme definition using the runtime's scheme generator. New instances are created on the generated scheme. There is no manual intervention required, and no exception is thrown. **Do I need a specific license or edition to use process versioning?** No. Process versioning is built into the core runtime and is included with all Workflow Engine editions. Every scheme save creates a new versioned record - there is no configuration required and no additional license. **Can I delete old scheme versions?** Yes. The persistence provider exposes `DropUnusedWorkflowProcessSchemeAsync()` to remove scheme records that are not referenced by any running or completed process instance. This is useful for housekeeping in long-running deployments. Use with care - deleted schemes cannot be used for audit traceability. ## See also --- # Parallel Processes URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/parallel-processes/ # Parallel Processes Most business processes have steps that happen at the same time. A purchase order may need three separate approvals from different departments. An onboarding workflow may provision accounts in five systems simultaneously. Without parallel processes, you either model every combination as a linear sequence or split the workflow into disconnected pieces that must be tracked separately. Parallel processes let a parent workflow spawn child process instances that run independently. Each child has its own state, its own parameters, and its own activity lifecycle. The parent can continue independently or coordinate with children when they finish. The children are full process instances - they can branch, use timers, and even spawn their own children. With parallel processes, you model concurrent work where it belongs - inside the same workflow. A procurement workflow spawns a subprocess for each line item review; reviews run in parallel, and the parent proceeds to fulfillment only when all are complete. ## What it is Think of parallel processes like delegating tasks to a team. You assign work to several people at once, each works independently, and you collect the results when everyone is done. You do not need to know exactly how each person does their work - you just need the outcome. Workflow Engine models parallel processes using fork transitions. A fork transition is a special transition that creates a child process instance. The parent scheme contains parent activities at a lower nesting level and child activities at higher levels, with fork transitions connecting them. When the parent enters a transition from a lower nesting level to a higher one, the runtime creates a child process instance - the subprocess starts. When the child completes and a transition goes back to a lower nesting level, the runtime merges the child back into the parent - the subprocess ends. Each child runs as a full process instance with its own state and its own parameters. The child scheme is derived from the **same built scheme definition** as the parent - activities at different nesting levels within that scheme separate the two. To reuse a separately authored scheme fragment, add it to the parent as an Inline Activity. During scheme build, Workflow Engine copies the fragment into the root scheme before the runtime extracts and versions the subprocess definition. Parameters flow between parent and child through configurable copy and merge strategies: you decide which parameters the child inherits at startup and which it sends back on completion. **For developers:** fork transitions are configured by setting `IsFork = true` on a `TransitionDefinition`. The `ForkType` (ForkStart or ForkEnd) is determined by the nesting levels of the From and To activities. Reusable external fragments must be incorporated through scheme inlining before those subprocess boundaries are built. Subprocess startup behavior is controlled by `SubprocessStartupType` (SameThread, AnotherThread, TimerQueue). Parameter passing is configured via `SubprocessStartupParameterCopyStrategy` and `SubprocessFinalizeParameterMergeStrategy`. See Configure Subprocesses for implementation guidance. ## Why it matters Parallel processes deliver these outcomes: - **Concurrent work in a single workflow** - model parallel approvals, multi-system provisioning, or simultaneous document reviews without splitting your process into disconnected pieces. The runtime tracks all children as a tree, and the parent can use completion conditions to wait for the required branches. - **Independent state per branch** - each child has its own parameters, activity state, and execution timeline. One child waiting for a manager approval does not block another child running an automated check. Children can use timers, conditions, and subprocesses of their own. - **Configurable parameter isolation** - choose which parameters the child inherits from the parent (all, none, or a specified list) and which it merges back on completion. Unmapped parameters stay private to each process instance. - **Reusable process fragments** - define a fragment in a separate scheme and insert it into parent workflows through scheme inlining. The build step embeds the fragment into each root scheme, so the runtime still creates every subprocess from one combined scheme definition. ## Who it is for **Evaluator (CEO, CTO, PM):** parallel processes eliminate the complexity of splitting concurrent work across multiple workflow definitions. A single process definition models the entire flow, including parallel branches, without requiring custom orchestration code outside the workflow engine. **Developer:** subprocesses are configured through fork transitions in the scheme. The runtime handles child instance creation, parameter isolation, and merge on completion. You control execution strategy (same thread, separate thread, or timer queue) and parameter passing. See Configure Subprocesses for setup. **Enterprise architect:** each child process instance is persisted independently with its own scheme version. The parent and children are linked through the process instances tree, which is stored and queryable. Parameter isolation prevents data leakage between branches while allowing explicit data flow where needed. ## When to use it Use parallel processes when a unit of work has its own lifecycle, its own state, and its own completion criteria that differ from the parent workflow. Typical examples: - **Multi-department approvals** - a purchase order requires approval from finance, legal, and operations simultaneously. Each department's review is a subprocess; the parent order waits for all three before proceeding to fulfillment. - **Multi-system provisioning** - an employee onboarding workflow provisions accounts in HR, IT, and facilities systems in parallel. Each system integration is a subprocess that completes independently. - **Reusable workflow fragments** - the same compliance review applies to orders, contracts, and vendor onboarding. Define it as an inlineable scheme and reference it through an Inline Activity in each parent. Saving the fragment through the Designer marks related parent schemes obsolete, so their next built versions include the update while running instances follow normal scheme-versioning rules. - **Parallel data processing** - a document processing pipeline splits a batch into individual items, processes each item independently, and merges results when all items complete. Each item is a subprocess with its own progress tracking. ## How it compares There is no built-in alternative to parallel processes in Workflow Engine. Each parallel branch is represented as a full process instance within the same scheme definition. If you need concurrent work that has its own state, lifecycle, and completion criteria, subprocesses are the mechanism - there is no separate feature that provides the same capability. The only alternative is to model concurrent work externally: create separate process instances from your application code and coordinate them manually. This requires custom orchestration logic and loses the parent-child relationship tracking that the runtime provides. ## Frequently asked questions **How do parallel processes compare to running separate process instances from code?** Running separate process instances from application code gives you concurrent execution, but you lose the built-in parent-child relationship. With subprocesses, the runtime tracks the hierarchy, applies configured parent-state control, and can merge parameters on completion. Manual orchestration requires you to implement these relationships yourself. **Can a subprocess spawn its own subprocesses?** Yes. Each child is a full process instance. It can have its own fork transitions, timers, conditions, and subprocesses. The runtime tracks the entire hierarchy through the process instances tree and enforces parent-child lifecycle rules at every level. **How do I pass parameters between parent and child?** The fork transition defines a startup parameter copy strategy and a finalize parameter merge strategy. At startup, you choose whether the child inherits all parent parameters, none, or only a specified list. On completion, you choose whether child parameters overwrite parent parameters, only null values, or a specified list. Parameters that are not explicitly passed are isolated to their process instance. **What happens to subprocesses if the parent completes or is cancelled?** The runtime checks subprocess validity whenever the parent changes state. If a parent moves to a state where the subprocess can no longer exist, the runtime drops the subprocess. You can disable this behavior per fork transition by setting `DisableParentStateControl` to true, which gives you full control over subprocess lifecycle. **Can I use a separate scheme as a subprocess?** Not directly. Add the separate scheme as an Inline Activity in the parent; the build step embeds its elements into the root scheme, and fork transitions then create the subprocess from that combined definition. Saving the inlined scheme through the Designer marks related parent schemes obsolete so new built versions can include the change. **Can I control when a subprocess starts - on the same thread, a new thread, or a timer?** Yes. The `SubprocessStartupType` property on the fork transition controls this. `SameThread` creates the child synchronously during the parent's command execution. `AnotherThread` creates the child on a background thread. `TimerQueue` schedules child creation through the timer queue for deferred execution. ## See also --- # Timers & Scheduling URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/timers-scheduling/ # Timers & Scheduling Every workflow has steps that should not wait for a person. An approval that auto-escalates after 48 hours. A nightly batch job that runs at 2 AM. An SLA reminder every Monday morning. Without timer support, you need external schedulers, polling loops, or custom cron jobs to advance the process on a schedule. Timers on transitions let Workflow Engine fire state changes automatically - after a delay, at a specific time, or on a recurring interval. The runtime tracks pending timers in the persistence store, evaluates them continuously, and fires the associated transition when the timer condition is met. With timers, you model time-driven behavior directly in the workflow scheme. An approval step has two outgoing transitions: one triggered by the user clicking "Approve" and one triggered by a 48-hour timer. Whichever fires first determines the path. No polling, no external scheduler, no custom code. ## What it is Think of a timer as an alarm clock attached to a transition. When a process instance enters an activity that has a timer-enabled outgoing transition, the runtime sets the alarm. If the user acts first, the alarm is cancelled. If the alarm rings first, the process advances automatically. A timer is defined on a transition by setting its trigger type to `Timer` and attaching a `TimerDefinition` with a name, a type, and a value. The timer type determines how the value is interpreted: - **Interval** - a delay measured in milliseconds from when the process enters the source activity. The value can be a plain number of milliseconds (`"3600000"` for one hour) or a combined string (`"1 d 3 h"` for one day and three hours). - **Time** - a specific time of day, expressed as `"HH:mm"`. If the time has already passed today, the timer fires at that time tomorrow. - **Date** - a specific calendar date. The timer fires at midnight (00:00:00) on that date. - **DateAndTime** - a specific date and time combination, such as `"2025-12-31 23:59"`. - **Expression** - a custom expression that returns a `DateTime` value at runtime. This lets you calculate the timer value dynamically based on process parameters. When the timer fires, the runtime evaluates the transition's condition before executing. If the condition returns false - for example, the process state no longer matches the timer's intent - the timer is consumed without advancing the process. This prevents stale timer firings from moving the workflow when the context has changed. Calendar-aware intervals use the `w` prefix on time units: `whours`, `wdays`, `wmonth`, `wyears`. These intervals skip non-working time defined in the Working Calendar. A timer set for `"48 whours"` from Friday at 5 PM fires on Tuesday instead of Sunday - the working hours counter pauses over the weekend. **For developers:** timers are defined as `TimerDefinition` objects on transitions with `Trigger.Type == TriggerType.Timer`. The runtime registers the timer in persistence when the process enters the source activity and deletes it when the process leaves via another transition. The timer evaluator polls for due timers and fires the associated transition. See Timer concept for the full API reference. ## Why it matters Timers deliver these outcomes: - **Automatic escalation and SLA enforcement** - set a timer on an approval activity to escalate to a manager after a working-day delay. No polling, no custom scheduler, no developer intervention. - **Time-based branching** - a single activity can have two outgoing transitions: a command transition for the user and a timer transition for automatic advance. The first to fire wins. This is how approval escalation works without custom code. - **Calendar-aware scheduling** - working-day and working-hour intervals (`wdays`, `whours`) respect your configured business calendar. Timers fire only during business hours and skip holidays, weekends, and non-working days automatically. - **Dynamic timer values** - the Expression timer type lets you calculate the timer delay from process parameters. For example, the approval deadline can be set per-instance based on the order priority or customer tier. ## Who it is for **Evaluator (CEO, CTO, PM):** timers eliminate the need for external schedulers and polling jobs for workflow automation. Escalation paths, SLA enforcement, and scheduled processing are modeled inside the workflow scheme, not scattered across cron jobs and background services. **Developer:** timers are configured as a property on a transition in the scheme. The runtime handles registration, evaluation, and cleanup automatically. No timer management code is needed in your application. See Configure Timers for setup. **Enterprise architect:** timer records are stored in the persistence database alongside process instances and are evaluated by the runtime's timer manager. In multi-server deployments, timers are distributed across nodes with locking to prevent duplicate execution. The runtime enforces timer consistency even under node failure. ## When to use it Use timer-driven transitions when a process must advance without direct user action. Typical examples: - **Approval escalation** - an expense report approval auto-escalates to the next manager if the first approver does not act within 48 working hours. The timer is defined on the "Pending Approval" activity alongside the "Approve" command. - **SLA enforcement** - a support ticket workflow sends an automated reminder if a ticket remains in "Assigned" state for more than 4 hours. The timer fires, evaluates the condition, and triggers a notification action. - **Scheduled batch processing** - a nightly workflow runs at 2:00 AM to process end-of-day reports. The timer type is `Time` with value `"02:00"`. - **Dynamic deadlines** - a contract review process calculates the deadline based on the contract value. High-value contracts get a 24-hour deadline, standard contracts get 72 hours. The timer uses type `Expression` with a formula that reads the contract value parameter. ## How it compares Timers in Workflow Engine are built into the runtime at the persistence layer. The alternative is to manage scheduling externally. Table: Comparison of timer scheduling approaches | Approach | What it requires | Result | |-------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **External cron / scheduler** | A separate scheduling service, polling logic in the workflow, API calls to execute commands at scheduled times. | Two systems to maintain. Risk of drift between the scheduler schedule and the workflow state. | | **Workflow Engine timers** | Configure the timer on the transition in the scheme. The runtime handles registration, evaluation, firing, and cleanup. | Timer logic lives in the workflow definition. The runtime manages the full lifecycle and guarantees that a timer fires only if the process is still in the expected state. | Workflow Engine timers are not a replacement for a general-purpose task scheduler. They are scoped to the lifecycle of a process instance - the timer exists only while the process is in the source activity and is automatically cleaned up when the process moves on. ## Frequently asked questions **What timer types does Workflow Engine support?** Five types: `Interval` (delay in milliseconds or combined time units), `Time` (specific time of day), `Date` (specific date at midnight), `DateAndTime` (specific date and time), and `Expression` (dynamic value calculated at runtime). All are defined as a `TimerDefinition` on a transition with `TriggerType.Timer`. **Can I use calendar-aware intervals that skip weekends and holidays?** Yes. Prefix the time unit with `w` to make it calendar-aware: `whours`, `wdays`, `wmonth`, `wyears`. These intervals skip non-working time as defined in the Working Calendar. For example, `"48 whours"` fires after 48 working hours, skipping weekends and holidays. **Are timers automatically cancelled when the process moves on?** Yes. When a command or auto-transition executes and moves the process out of the source activity, the runtime deletes all pending timers registered for transitions from that activity. The persistence provider's `DeleteInactiveTimersByProcessIdAsync` handles cleanup. No explicit API call is needed. **Do I need a specific license or edition to use timers?** No. Timer support is included in the core runtime and is available in all Workflow Engine editions. **Can I calculate the timer delay dynamically from process parameters?** Yes. Use the `Expression` timer type. The value is a formula that returns a `DateTime`. For example, you can set the deadline based on an order priority parameter, a contract value, or a customer tier. The expression is evaluated when the process enters the source activity. ## See also --- # Direct State Control URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/direct-state-control/ # Direct State Control A process instance can get stuck. A command may be unavailable because its condition is no longer met. A transition may reference a deleted action. An error handler may loop without advancing. Without a way to manually reposition the instance, the only option is to delete it and start over. Direct state control lets you move a process instance to a target activity directly, bypassing the normal command execution pipeline. You specify the target state, and the runtime places the instance there without selecting a configured transition into the target. The trigger, conditions, and restrictions of such a transition are not used for the direct jump. With direct state control, stuck processes are recoverable without deleting the instance or editing persistence directly. An operator repositions a failed invoice approval from an error activity back to "Pending Approval" and the process continues. ## What it is Think of direct state control as a manual override on a machine. Normal operation follows a configured route with its checks and steps. The override moves the machine to an approved position directly - useful when the normal controls cannot work because the machine is in an unexpected state. `SetStateAsync` is the runtime API that performs the override. It takes a process instance identifier and a target state name. The runtime can move the process only to an activity whose `State` matches that name and whose `IsForSetState` option is enabled. If no such activity exists, the runtime throws an exception. The method supports two modes controlled by the `PreventExecution` parameter: - With execution (default, `PreventExecution = false`) - the runtime runs the target activity's `Implementation`, including its configured actions, and then processes automatic transitions from that activity normally. Conditions on those automatic transitions are evaluated. - Without execution (`PreventExecution = true`) - the runtime persists the target activity without running its `Implementation` or processing automatic transitions. The process is set to `Idled`, or to `Finalized` when the target is final. The direct jump does not execute the current activity's `Implementation` or a configured transition into the target. With execution, configured process logging can record the set-state execution and the resulting activity and state changes. The without-execution path updates persistence but does not produce the same execution log, so add application-level auditing if the operation must always be recorded. **For developers:** call `WorkflowRuntime.SetStateAsync(SetStateParams)` with the process ID and target state name. In the scheme, give the target activity a `State` and set `IsForSetState = true`. Set `PreventExecution = true` to skip target activity execution. See Set State Directly reference for API details. ## Why it matters Direct state control delivers these outcomes: - **Recover stuck processes without data loss** - a process instance stuck in an error loop or an invalid state can be repositioned to a valid activity. All its parameters and history are preserved - no need to delete and restart. - **Bypass an unavailable route** - the direct jump does not use a command, actor rule, or configured transition into the target. The target still needs a matching `State` and `IsForSetState = true`. With execution enabled, conditions on automatic transitions from the target are processed normally. - **Use the runtime instead of editing the database** - modifying the current activity in the database directly bypasses runtime processing. `SetStateAsync` validates the target, updates persistence through the runtime, and leaves the process `Idled` or `Finalized` when execution finishes at that activity. - **Two modes for different recovery scenarios** - with execution lets the process auto-advance after the state change. Without execution lets the operator place the process precisely and decide the next step manually. ## Who it is for **Evaluator (CEO, CTO, PM):** direct state control reduces the operational impact of stuck processes. When exposed through controlled administrative tooling, a failed instance can be recovered without database access or losing the process data accumulated so far. **Developer:** `SetStateAsync` is a single API call. The target activity must be marked with `IsForSetState = true` in the scheme - you decide which activities are safe to jump to. The method does not replace `ExecuteCommandAsync` for normal flow; it is an escape hatch for cases where normal execution cannot apply. See Set State Directly reference for the API reference. **Enterprise architect:** the direct jump does not perform the actor authorization used for command execution. Your application must restrict access to this API at the integration layer. When execution is enabled, the target activity's actions run and conditions on its automatic transitions are evaluated normally. ## When to use it Use direct state control for interventions that cannot be accomplished through normal command execution. Typical examples: - **Recovering stuck instances** - a support ticket workflow encountered an error during a web service call and the error handling activity has no outgoing command. An operator uses `SetStateAsync` to move the instance back to an earlier activity and retry. - **Applying state migrations after scheme changes** - a scheme update removed a transition path that running instances depended on. Use `SetStateAsync` in a migration script to reposition affected instances to compatible activities. - **Admin troubleshooting tools** - build an admin panel that lets support staff move instances between states for testing or diagnosis. The `IsForSetState` flag on activities limits which states are reachable. Do not use `SetStateAsync` as a substitute for `ExecuteCommandAsync` in normal process flow. Reserve it for cases where the normal command pipeline cannot apply. ## How it compares The alternatives to `SetStateAsync` for dealing with stuck processes are destructive or risky. The table below shows the tradeoffs. Table: Comparison of stuck process recovery approaches | Approach | Risk | Result | |-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| | **Delete and restart** | Loses all process parameters, history, and context accumulated in the stuck instance. | Data loss. User must re-enter information. | | **Direct database update** | Skips all runtime validation - may leave the process in an inconsistent state with orphaned timers, wrong status, or broken parameter state. | Runtime consistency can be lost. | | **Workflow Engine `SetStateAsync`** | Validates the target activity and updates persistence through the runtime. Target actions may have side effects unless execution is prevented. | The process keeps its parameters and existing history. Add application-level auditing if required. | ## Frequently asked questions **When should I use SetStateAsync instead of ExecuteCommandAsync?** Use `ExecuteCommandAsync` for normal process progression where conditions, permissions, and actions should be evaluated. Use `SetStateAsync` only for manual overrides - recovering stuck instances, migrating process state after a scheme change, or building tooling that needs to reposition instances outside the normal flow. **Do I need to set IsForSetState on every activity I want to jump to?** Yes. A target activity must have `IsForSetState = true` and a non-empty `State` matching the requested state name. Attempting to set an unavailable state causes the runtime to throw `ActivityNotFoundException`. The target is identified by its state name, not its activity name. **What is the difference between SetStateAsync with and without execution?** With execution (default), the runtime runs the target activity's `Implementation` and then processes automatic transitions from it. Without execution (`PreventExecution = true`), the runtime persists the target without running its `Implementation` or automatic transitions, then sets the process to `Idled` or `Finalized`. **Which activity actions does SetStateAsync execute?** It does not run the current activity's `Implementation`. With `PreventExecution = false`, it runs the target activity's `Implementation` and may continue through automatic transitions. With `PreventExecution = true`, it does not run the target activity or process its automatic transitions. **What are the security implications of SetStateAsync?** The direct jump does not perform the actor authorization used by `ExecuteCommandAsync`. Access to this method in your application should be controlled according to your security requirements - the runtime does not validate whether the supplied identity may perform this operation. ## See also --- # HTTP API URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/http-api/ # HTTP API Integrating a .NET workflow engine with a non-.NET frontend or service usually means building a custom API layer. Every new endpoint - create instance, execute command, check state - has to be written, tested, and maintained. The API surface grows with every workflow operation the frontend needs. The Workflow Engine HTTP API exposes WorkflowRuntime operations as HTTP endpoints. Any client that speaks HTTP - whether .NET, JavaScript, Python, Java, or another .NET service - can create process instances, execute commands, query available commands, and read process state without writing a custom API layer. With the HTTP API, your React frontend calls `ExecuteCommand` via `fetch()` directly. Your Python microservice starts a process instance by POSTing to the create-instance endpoint. Your low-code platform connects over REST without custom middleware. ## What it is Think of the HTTP API as a remote control for the workflow engine. Instead of calling C# methods directly, you send HTTP requests to predefined endpoints. The API translates each request into the corresponding runtime operation and returns the result as JSON. The API is enabled by calling `AddWorkflowApiCore()` during ASP.NET Core service configuration and `MapWorkflowApi()` in the endpoint pipeline. These methods register HTTP endpoints that map to the same operations you would call from C# code - `CreateInstance`, `ExecuteCommand`, `GetAvailableCommands`, `SetState`, and others. The endpoints accept and return JSON, making them consumable from any HTTP client. The HTTP API organizes its main endpoints into RPC, Data, and Search APIs. The **RPC API** exposes action-oriented operations - `ExecuteCommand`, `GetAvailableCommands`, `CreateInstance`, `SetState`, and others - that invoke `WorkflowRuntime` behavior. The **Data API** provides resource-specific read and write access to Workflow Engine persistence entities - runtimes, schemes, process instances, global and per-process parameters, timers, transitions, approvals, inbox entries, and statuses. Each entity exposes only its supported operations rather than universal CRUD. The **Search API** provides filtering, sorting, and pagination for supported entities. The API includes built-in OpenAPI / Swagger metadata. You can publish it with native ASP.NET Core OpenAPI or with `AddEndpointsApiExplorer()` and `AddSwaggerGen()`. This lets teams using non-.NET stacks explore and test the API without reading C# documentation. Security uses standard ASP.NET Core authentication middleware. The host can use JWT bearer tokens, cookie authentication, or any other scheme registered in its pipeline. When `AddWorkflowApiSecurity()` is registered, authorization uses a granular permission system - each endpoint has a unique `OperationId`, and access is controlled through the `IWorkflowApiPermissions` interface. This lets you grant or deny access to specific operations per user or role, down to individual endpoints. **For developers:** add `services.AddWorkflowApiCore()`, a provider-specific registration like `services.AddWorkflowApiMssql()`, and `services.AddWorkflowRuntime()` to your service collection, then call `app.MapWorkflowApi()` on the application builder. See Web API Setup for a step-by-step guide. ## Why it matters The HTTP API delivers these outcomes: - **No custom API layer** - common runtime operations the frontend needs are already exposed as HTTP endpoints. Creating instances, executing commands, reading state, and querying history are built in. - **Two API styles in one** - the **RPC API** handles action-oriented workflow commands (`ExecuteCommand`, `CreateInstance`, `SetState`), while the **Data API** provides resource-specific access to persistence entities and the **Search API** provides filtering, sorting, and pagination. The APIs share the same endpoint base, authentication, and OpenAPI documentation. - **Language-agnostic integration** - any HTTP client can interact with the workflow engine. JavaScript, Python, Java, Go, Ruby, or low-code platforms all use the same REST endpoints and JSON payloads. - **OpenAPI documentation** - the API endpoint metadata can be published in an OpenAPI document and Swagger UI when OpenAPI is configured. Teams can explore and test endpoints without reading C# documentation or inspecting the source code. - **Consistent security model** - the API can use ASP.NET Core authentication middleware with a granular permission system. Each endpoint is identified by a unique `OperationId`, and access is controlled through `IWorkflowApiPermissions`. You can grant or deny access to specific operations per user or role. ## Who it is for **Evaluator (CEO, CTO, PM):** the HTTP API eliminates the need for a dedicated API development effort to expose workflow operations to non-.NET clients. The API is available as a separate Workflow Engine NEO package and covers common operations out of the box. **Developer:** add `AddWorkflowApiCore()`, a database provider, `AddWorkflowRuntime()` or `AddWorkflowTenants()`, and `MapWorkflowApi()` to your ASP.NET Core pipeline. The API endpoints are ready without writing controller code. See Web API Setup and Web API Security for integration details. **Enterprise architect:** the API runs inside your ASP.NET Core application and uses standard authentication middleware. Each endpoint has an `OperationId` that can be used for granular access control through the `IWorkflowApiPermissions` interface. The endpoint metadata can be used to generate an OpenAPI schema for API gateways, documentation tools, and code generators. ## When to use it Use the HTTP API whenever you need to call workflow operations from another process, regardless of the client technology. Typical examples: - **Single-page application frontend** - a React or Vue.js application calls workflow operations via `fetch()` or a generated API client. No backend-for-frontend layer is needed. - **.NET microservices** - another .NET service in your architecture calls workflow operations over HTTP instead of hosting a `WorkflowRuntime` instance directly. The API provides a language-native integration point without tight coupling. - **Polyglot microservices** - a Python, Java, or Go service needs to start a process instance or check process state. The API provides REST endpoints that any language can call. - **Low-code and no-code integration** - tools like Power Automate, Zapier, or custom automation platforms connect over HTTP. The Workflow Engine API exposes workflow operations as standard REST endpoints they can call. ## How it compares The main alternative to the built-in HTTP API is building a custom API layer. The table below shows the tradeoffs. Table: Comparison of HTTP API approaches | Approach | What it requires | Result | |------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | **Build a custom API** | Define controllers, serialize requests/responses, document endpoints, handle errors, maintain versioning. | Engineering effort for every new operation. Risk of inconsistent security or missing endpoints. | | **Workflow Engine HTTP API** | Register the API and provider packages, a runtime or tenants, and `MapWorkflowApi()`. | Built-in API exposure with a consistent security model and OpenAPI metadata. | ## Frequently asked questions **Do I need a specific license or edition to use the HTTP API?** The RPC and Data APIs are Workflow Engine NEO capabilities distributed in the separate `OptimaJet.Workflow.Api` package. The license must enable the API groups you use. See the Editions page for details. **What endpoints does the HTTP API expose?** The API exposes RPC, Data, and Search endpoints. The **RPC API** covers action-oriented operations - `CreateInstance`, `ExecuteCommand`, `GetAvailableCommands`, `SetState`, process history, and subprocess tree queries. The **Data API** covers resource-specific read and write access to runtimes, schemes, process instances, parameters, timers, transitions, approvals, inbox entries, statuses, and global parameters. The **Search API** adds filtering, sorting, and pagination for supported entities. The full endpoint list is visible in the Swagger UI when OpenAPI is enabled. **How does the API handle authentication and authorization?** The host uses ASP.NET Core authentication middleware and can support JWT bearer tokens, cookie authentication, and any other scheme registered in its pipeline. When `AddWorkflowApiSecurity()` is registered, authorization uses a granular permission system - each endpoint has a unique `OperationId`, and access is controlled per operation via the `IWorkflowApiPermissions` interface. This lets you grant or deny access to specific API endpoints per user or role, independent of the workflow runtime's own actor rules. **Does the HTTP API support multitenancy?** Yes. Register tenants with `AddWorkflowTenants()`. A request selects a registered tenant through the `Workflow-Api-Tenant-ID` header or `WorkflowApiCoreOptions.DefaultTenantId`; a permission claim can separately restrict the tenants the caller may access. See the Web API Multitenancy documentation for configuration details. **Can I use the HTTP API with Swagger / OpenAPI?** Yes. The API supplies ASP.NET Core endpoint metadata. Configure native `AddOpenApi()` or `AddEndpointsApiExplorer()` with `AddSwaggerGen()` to generate an OpenAPI document; Swagger UI is an optional host component. ## See also --- # BPMN Support URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/bpmn-support/ # BPMN Support Without BPMN support, migrating process models created in tools like Camunda Modeler, Signavio, or Bizagi means re-drawing every flow in the Workflow Engine Designer by hand - time-consuming, error-prone, and disconnected from how your team already works. BPMN support is a plugin that reads BPMN 2.0 XML files and converts them into executable Workflow Engine schemes. With BPMN support, you upload a BPMN diagram and the plugin produces a working scheme in the Designer, preserving the core flow structure - gateways, events, tasks, and sequence flows. For example, a purchase approval diagram modeled in Camunda Modeler becomes a Workflow Engine scheme ready to run against your persistence database, without re-drawing a single element. ## What it is Think of BPMN support as a translator between two languages. Your team draws processes in a familiar BPMN modeling tool - the standard notation for process design. Workflow Engine executes processes using its own scheme format. The BPMN plugin reads a BPMN 2.0 XML file and produces an equivalent Workflow Engine scheme, mapping each BPMN element type to the closest matching runtime construct. Table: BPMN element mapping to Workflow Engine constructs | BPMN element | What it becomes in Workflow Engine | |-------------------------|-------------------------------------------------------------------------| | Start event | Initial activity (the first activity the runtime executes) | | End event | Final activity (completes the process instance) | | Service task | Intermediate activity with auto-trigger | | Exclusive (XOR) gateway | Custom activity type - outgoing transitions act as conditional branches | | Parallel (AND) gateway | Custom activity type - enables concurrent execution paths | | Timer catch event | Intermediate activity with timer trigger | | Message catch event | Intermediate activity that waits for an external command | | Terminate event | Final activity that ends all parallel branches | Import is not lossless. BPMN elements without a Workflow Engine equivalent - message flows between pools, complex gateways, boundary events, event subprocesses - are skipped during import. The BpmnPlugin logs each skipped element in the import results. Use BPMN import as a migration tool for the structural flow, not as a fidelity-preserving converter. The canonical editing format is Workflow Engine's native scheme format. **For developers:** The BpmnPlugin implements `IWorkflowPlugin` and registers eight custom activity types (`ServiceTaskActivity`, `ExclusiveGateway`, `ParallelGateway`, `MessageCatchEvent`, `TimerCatchEvent`, `StartEvent`, `EndEvent`, `TerminateEvent`) plus a Designer action (`bpmnplugin_uploadbpmn`). Install the `OptimaJet.Workflow.BpmnPlugin` NuGet package and register the plugin with `runtime.WithPlugin(new BpmnPlugin())`. See the BPMN import guide for setup and element mapping details. ## Why it matters BPMN support delivers these outcomes: - **No process re-drawing** - Import existing BPMN diagrams directly. A team migrating from Camunda Modeler to Workflow Engine does not need to re-author each flow. The import preserves gateways, events, tasks, and sequence flow structure. - **Single source of truth for process design** - Process analysts can continue using their preferred BPMN modeling tool for design and documentation. The imported scheme is the executable version of the same diagram. No translation layer, no manual transcription. - **Faster migration from incumbent engines** - Existing BPMN assets from legacy workflow products or BPMN-based services can be imported in bulk. The core flow structure migrates; only runtime-specific configuration (command bindings, rule assignments, code actions) needs manual setup. ## Who it is for **Evaluator (CEO, CTO, PM):** BPMN support removes the re-drawing bottleneck when migrating from BPMN-based workflow tools. If your team uses BPMN for process modeling and you are evaluating Workflow Engine, you can import your existing diagrams and evaluate against real processes, not toy examples. **Developer:** The plugin reduces integration effort by translating BPMN XML to a runtime scheme in one step. You implement rule providers and action providers once - the same code works for imported and hand-authored schemes. See the BPMN support page for setup details and the full list of supported elements. **Enterprise architect:** BPMN import preserves the structural intent of enterprise process models. Combined with Workflow Engine's persistence and clustering, you can model processes in BPMN-standard tools and deploy them on a horizontally scalable runtime without manual schema translation. ## When to use it Use BPMN import when existing process diagrams are in BPMN 2.0 format. Typical examples: - **Migrating from Camunda, Signavio, or Bizagi** - You have hundreds of BPMN diagrams in production on another engine. Import each diagram to Workflow Engine, add runtime configuration (actions, rules, commands), and deploy. The core flow structure transfers; the runtime-specific wiring is new. - **Procurement or compliance documentation** - Your compliance team maintains process documentation as BPMN diagrams in a modeling tool. Import the approved diagram to create the executable scheme, ensuring the running process matches the documented design. - **Partner or vendor process exchange** - A partner provides integration workflows as BPMN 2.0 XML. Import the file directly rather than translating the flow manually into the Designer. ## How it compares The table below shows the tradeoffs between the Workflow Engine approach and the alternative. Table: Comparison of BPMN import approaches | Approach | What it requires | Result | |-----------------------------|----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| | Re-author by hand | Copy each BPMN element into the Designer: re-create activities, transitions, gateways, and conditions manually | Time per process: hours. Risk of transcription errors. Divergence between design model and running process. | | Workflow Engine BPMN import | Upload the BPMN 2.0 XML file through the Designer. Add runtime wiring (actions, rules) after import. | Minutes per process. Core flow structure preserved. Design model and running process stay in sync. | Import fidelity depends on element complexity. Simple flows (start - tasks - exclusive gateway - end) import cleanly. Advanced BPMN constructs (complex gateways, boundary events, event subprocesses, pools and message flows) are not supported and are skipped with a log entry. ## Frequently asked questions **Does Workflow Engine support exporting schemes as BPMN 2.0 XML?** Not currently. The BpmnPlugin is import-only. Schemes edited in the Designer or modified at runtime cannot be exported back to BPMN format. Use the Designer's built-in scheme export for backups and the Simple Process Notation for version control. **Do I need a specific license for BPMN import?** Yes. The BpmnPlugin checks for a BPMN license restriction at registration. If your license does not include BPMN support, the plugin throws on `OnPluginAdd`. See Workflow Engine Editions for per-feature license mapping. **Which BPMN 2.0 elements are not supported in import?** Complex gateways, event subprocesses, boundary events (attached to activities), pools and message flows, data objects, and artifacts are skipped. The import log lists each skipped element with the reason. Custom extension elements and non-executable BPMN attributes (documentation, graphical layout) are also ignored. **What happens to transitions and conditions during import?** Sequence flows become transitions. Conditions on exclusive gateway outgoing flows become the condition expressions on each transition. Parallel gateways produce concurrent outgoing transitions - the runtime executes all branches. The converter handles subprocesses by creating inlined schemes (sub-schemes referenced from the main process). ## See also --- # Plugin System URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/plugin-system/ # Plugin System Without a plugin system, every common workflow pattern - sending an email, routing an approval chain, assigning a task to a user - requires hand-written action code in every project. Developers write the same HTTP client, the same approval logic, the same task inbox queries, project after project. The plugin system is a set of pre-built workflow capabilities packaged as .NET assemblies that register with the runtime at startup. Each plugin provides ready-to-use activity types, actions, and conditions that you can add to any scheme in the Designer. With the plugin system, you install a NuGet package (or reference the assembly), register the plugin with a single line of configuration, and use its activities in any process model. For example, installing the Approval plugin and adding the "Approval" activity to a scheme turns a linear flow into a multi-step approval chain - no code written. ## What it is Think of a plugin as a pre-assembled toolkit for a specific workflow pattern. Instead of building an approval system from scratch - writing activities, actions, conditions, and Designer configuration - you install the Approval plugin and its activities appear in the Designer palette, ready to drag onto a scheme. The runtime knows how to execute them because the plugin registered the behavior during startup. The following plugins ship with Workflow Engine. Plugins marked as in-project live in the Core assembly; plugins marked as separate ship as independent NuGet packages. Table: Shipped Workflow Engine plugins | Plugin | Where it lives | What it provides | |--------------------|---------------------------------------------|---------------------------------------------------------------------------------------| | Basic | In `OptimaJet.Workflow.Core` | HTTP request action, email sending, subprocess invocation, basic conditions and rules | | Approval | In `OptimaJet.Workflow.Core` | Multi-step approval chains with sequential and parallel review, approval history | | Loop | In `OptimaJet.Workflow.Core` | `For` and `ForEach` loop patterns for repeating activities and subprocess iteration | | Files | `OptimaJet.Workflow.FilesPlugin` | File upload, download, and attachment actions for process instances | | Active Directory | `OptimaJet.Workflow.ActiveDirectoryPlugin` | User and group lookup from Active Directory for rule evaluation | | Real-Time Tracking | `OptimaJet.Workflow.RealTimeTrackingPlugin` | Real-time process state updates via SignalR | All plugins follow the same registration pattern: ```csharp runtime.WithPlugin(new BasicPlugin()) .WithPlugin(new ApprovalPlugin()) .WithPlugin(new RealTimeTrackingPlugin()); ``` Each call to `WithPlugin` triggers `IWorkflowPlugin.OnPluginAdd`, which calls the plugin to register its activity types, action providers, rule providers, custom conditions, autocomplete providers, and parameter format providers with the runtime. The runtime aggregates these capabilities, making them available to both the execution engine and the Designer API. **For developers:** Every plugin implements `IWorkflowPlugin`, which defines `Name`, `Disabled`, `OnPluginAdd(WorkflowRuntime, List)`, and `OnRuntimeStartAsync(WorkflowRuntime)`. The runtime auto-detects the plugin's capabilities when it implements any of these interfaces: - `IWorkflowActionProvider` - registers actions and conditions for use in schemes - `IWorkflowRuleProvider` - registers actor and role resolution rules - `ICustomActivityProvider` - registers custom activity types that appear in the Designer palette - `ICustomConditionProvider` - registers custom condition types for transitions - `IDesignerParameterFormatProvider` - registers custom parameter format providers for Designer property panels - `IDesignerAutocompleteProvider` - registers autocomplete suggestions for code action editors - `IWorkflowExternalParametersProvider` - registers external parameter lookups for process parameters The runtime auto-detects these interfaces during `WithPlugin()` and registers each with the corresponding `With*Provider` method. During `OnPluginAdd`, the plugin can perform additional setup. See the Basic Plugin reference for an example implementation. ## Why it matters The plugin system delivers these outcomes: - **No re-implementation of common patterns** - Approval chains, email notifications, HTTP calls, and file attachments are ready-to-use. A project that needs purchase order approval installs the Approval plugin and configures the steps - no custom activity code. - **Consistency across projects** - Every team uses the same tested implementation of approval routing, email sending, and subprocess invocation. The surface area of custom code shrinks to project-specific actions and rules only. - **Extensible by design** - You can write your own plugins that follow the same registration pattern as the shipped plugins. Package custom activities, actions, and conditions as a reuse unit and share them across projects, teams, or tenants. - **Designer integration out of the box** - When a plugin registers custom activities via `ICustomActivityProvider`, the Designer displays them as palette elements with their own SVG templates, forms, and parameter editors. Business users can add plugin-provided activities to schemes without developer involvement. ## Who it is for **Evaluator (CEO, CTO, PM):** The plugin system means faster initial integration and less custom code to maintain. Common workflow patterns are tested by the Workflow Engine team and ship with the product. Time to first running workflow drops from days to hours - install, register, and model. **Developer:** You interact with plugins through the `IWorkflowPlugin` interface and the `runtime.WithPlugin()` registration method. Shipped plugins work out of the box. Custom plugins follow the same pattern: implement the interface, register your providers in `OnPluginAdd`, and use the activities in any scheme. See the Basic Plugin implementation for a code example. **Enterprise architect:** Plugins can be scoped per scheme - register a plugin with a scheme list and it only provides activities to those schemes. This prevents cross-scheme interference when two workflows use plugins that register activities with the same name. Plugin dependencies (such as Active Directory integration) are managed through standard NuGet package references. ## When to use it Use a plugin when your scheme needs a capability that the plugin provides. Typical examples: - **Adding approval workflows** - Your scheme needs a review-and-approve step where multiple reviewers vote sequentially or in parallel. Install the Approval plugin and add approval activities to the scheme with configurable roles and thresholds. - **Integrating with external systems** - A scheme needs to call an HTTP API, send an email, or create a file. Use the Basic plugin's HTTP action or the Files plugin's upload/download actions instead of writing custom action code. - **Looking up identity sources** - Scheme rules need to resolve users and groups from Active Directory. Register the Active Directory plugin as the rule provider, and scheme designers can reference AD groups directly in rule definitions. - **Building custom extensions as packages** - You have a reusable pattern across multiple projects (for example, a credit-check subprocess with custom activities and rules). Package it as an `IWorkflowPlugin` assembly, distribute via NuGet or file share, and register with `runtime.WithPlugin()`. ## How it compares The following table compares the Workflow Engine plugin system with the alternative of building every workflow pattern from scratch. Table: Comparison of plugin system approaches | Approach | What it requires | Result | |----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| | Build every pattern from scratch | Write custom activities, actions, conditions, Designer forms, and SVG templates for each pattern. Write and maintain tests for all of them. | High initial cost. Ongoing maintenance across every project. Duplicate implementations of the same patterns. | | Workflow Engine plugin system | Install a plugin package, register with `runtime.WithPlugin()`, and use the activities in the Designer. | Hours to integrate. Single maintained implementation per pattern. Activities available to any scheme immediately. | ## Frequently asked questions **What plugins does Workflow Engine ship, and are they all included with the base license?** The Basic, Approval, and Loop plugins are part of the Core package (`WorkflowEngine.NETCore-Core`) and do not require a separate license. The Active Directory and Files plugins are separate packages that may have their own licensing terms. See Workflow Engine Editions for per-plugin license mapping. **How do I register a plugin with the runtime?** Call `runtime.WithPlugin(instance)` during initialization, where `instance` is an object implementing `IWorkflowPlugin`. For example: `runtime.WithPlugin(new ApprovalPlugin())`. The call must happen before `runtime.Start()`. See the WorkflowRuntime setup page for the complete configuration chain. **Can I write my own plugin?** Yes. Implement `IWorkflowPlugin` in a class. In `OnPluginAdd`, register custom activities, action providers, rule providers, and other capabilities with the runtime. Package the assembly as a NuGet package or distribute as a project reference. **Can I load plugins from an external DLL without recompiling?** Yes. Use `Assembly.LoadFrom()` to load the external assembly, find types implementing `IWorkflowPlugin`, instantiate each, and call `runtime.WithPlugin()`. The runtime auto-detects the plugin's capabilities - action providers, rule providers, custom activities, and conditions - and integrates them into the Designer and execution engine. **Can I restrict a plugin to specific schemes?** Yes. `WithPlugin` accepts an optional list of scheme codes. When provided, the plugin's activities, actions, and rules are only available in the specified schemes. This prevents name collisions when two plugins register activities with the same type name. ## See also --- # Pluggable Security URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/pluggable-security/ # Pluggable Security Without pluggable security, a workflow engine must either manage its own users and roles - adding a second identity system to maintain - or hard-code authorization logic that cannot adapt to different deployments. Every transition, every command, needs a permission check, but the identity source changes from one organization to the next. Pluggable security is a provider interface - `IWorkflowRuleProvider` - that delegates every authorization decision to your application code. Workflow Engine never stores passwords, manages users, or maintains role hierarchies. It asks your code: "Can this person execute this command on this process instance?" With pluggable security, you connect Workflow Engine to your existing identity system in one integration point. For example, a rule named "Manager" on an approval transition causes your provider to check whether the calling user belongs to the Manager group in Active Directory, Azure AD, or your own user table - whatever you implement. ## What it is Think of the rule provider as a security guard that your company trains. You teach the guard how to recognize authorized people - by checking your employee directory, your SSO system, your custom database - and the guard stands at every door (transition) and makes the call. Workflow Engine does not need to know how your directory works. It just asks the guard: "Is this person allowed through this door?" When a user executes a command on a process instance, the runtime evaluates every outgoing transition from the current activity. For each transition, it asks the rule provider: can this user pass? If the provider says no, the runtime tries the next transition. If no transition passes, the command fails. The provider receives the user's identity ID, the rule name, and an optional parameter - typically a role name, a group name, or a permission string. The provider's job is to return `true` or `false`. **For developers:** Implement `IWorkflowRuleProvider`, which has seven members: synchronous (`GetRules`, `Check`, `GetIdentities`) and asynchronous variants (`CheckAsync`, `GetIdentitiesAsync`), plus `IsCheckAsync` and `IsGetIdentitiesAsync` to declare which variant the runtime should use. Register the implementation with `runtime.WithRuleProvider(new MyRuleProvider())`. The `Check` / `CheckAsync` methods receive the process instance, the runtime reference, the user's identity ID, the rule name, and an optional parameter string. See the Rules concept page for the full interface and implementation walkthrough. ## Why it matters Pluggable security delivers these outcomes: - **Your identity system stays in charge** - No user synchronization, no password storage, no role duplication. Workflow Engine never becomes a second source of truth for who can do what. If you deactivate a user in Active Directory, that user immediately loses access to all workflows. - **One integration point for all authorization** - Implement one interface and every transition in every scheme uses it. There is no per-transaction or per-activity authorization code to scatter across your integration. The rule provider is the single funnel for all command authorization. - **Works with any identity provider** - Active Directory, Azure AD, Okta, Auth0, LDAP, a custom SQL database, a REST API - if your application code can query it, your rule provider can use it. See the Active Directory plugin for a reference implementation. - **No lock-in to the provider choice** - Swap from LDAP to Azure AD by replacing the rule provider implementation. No scheme changes, no model changes, no database changes. The runtime only depends on the interface, not the implementation. ## Who it is for **Evaluator (CEO, CTO, PM):** Pluggable security means Workflow Engine adapts to your existing security infrastructure rather than forcing you to adapt to it. You do not need to provision users, sync directories, or manage roles inside the workflow engine. Your security team's existing processes - onboarding, offboarding, role changes - continue to work unchanged. **Developer:** You implement a single interface (`IWorkflowRuleProvider`) with a `Check` method that queries your identity system. Register it with `runtime.WithRuleProvider()`. The runtime calls your provider on every command execution. See the Rules concept page for code examples and the `DeactivatedWorkflowRuleProvider` wrapper for toggling providers on and off at runtime. **Enterprise architect:** The rule provider interface is the authorization perimeter for all workflow operations. Combined with the HTTP API or Web API, you can deploy Workflow Engine with no direct access to your identity infrastructure - only the provider implementation knows how to reach Active Directory or Azure AD. Audit all authorization decisions by wrapping a logging provider around your real implementation. ## When to use it Use pluggable security in any deployment that needs command-level authorization - which is almost every deployment. Typical scenarios: - **Role-based transitions on a purchase approval process** - Only users in the "Manager" role can approve requisitions above $5,000. The rule provider checks the user's role memberships against the rule name on each transition. - **Group-based document review** - Only members of the "Legal Review" group can advance a contract to the signature stage. The scheme designer creates a rule named "LegalReviewGroup" and the provider resolves it against the identity system. - **User-level self-service** - Only the original requestor can cancel their own expense report. The provider checks the identity ID from the user context against a process parameter that stores the requestor ID. - **Integration with an existing custom auth system** - Your application has a custom permissions table with fine-grained resource-level access. The rule provider queries that table using the rule name (which encodes a permission key) and the optional parameter (which encodes the resource scope). ## How it compares The table below compares Workflow Engine's pluggable security approach with the alternative of built-in user management in the engine. Table: Comparison of security approaches | Approach | What it requires | Result | |----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | Built-in user management in the engine | The engine stores users, roles, and passwords. User lifecycle (onboarding, offboarding, role changes) must be managed in two places. Password policies, MFA, and SSO integration become the engine's problem. | Two identity systems to maintain. Engine must be kept secure as a user store. Difficult to meet enterprise security compliance. | | Workflow Engine pluggable security | Implement `IWorkflowRuleProvider`. Register it with `runtime.WithRuleProvider()`. The engine delegates every authorization check to your code. | One identity system - yours. No user data in the engine. Compliance stays in your existing infrastructure. | ## Frequently asked questions **What identity systems does Workflow Engine support?** Any system your application code can query - Active Directory, Azure AD, Okta, Auth0, LDAP, a custom SQL database, a REST API - through the `IWorkflowRuleProvider` interface. There is no built-in user store, no login page, no password validation. Workflow Engine does not impose a specific identity provider. See the Active Directory plugin for an out-of-box implementation. **How does Workflow Engine know who the current user is?** The calling code passes the user's identity ID to every runtime API method (`ExecuteCommand`, `GetAvailableCommands`, etc.). In an ASP.NET Core application, you typically extract this from `HttpContext.User.Identity.Name` or a JWT claim and pass it to the API calls. Workflow Engine does not read `HttpContext` - the identity ID is an explicit parameter on every command-related method. **When are rules evaluated during process execution?** Rules are evaluated on every command execution (`ExecuteCommand`, `ExecuteCommandWithParameters`, etc.). The runtime evaluates all restrictions on every outgoing transition from the current activity. If a transition has no restrictions, it is available to all users. If the rule provider returns false for a restriction, the user is blocked from that transition. If all outgoing transitions are blocked, the command fails with no matching transition. If a referenced rule name is not implemented by any provider or code action, the runtime throws `NotImplementedException`. **Can I have multiple rule providers registered at the same time?** Yes. Each call to `WithRuleProvider` adds the provider to an aggregation chain. The runtime queries providers in registration order and returns true if any provider returns true for the rule. This lets you compose providers: a role-based provider handles most transitions, while a special-case provider handles override scenarios for administrators. ## See also --- # Clustering URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/clustering/ # Clustering Without clustering, a single Workflow Engine server is a single point of failure. If it goes down, timers stop, processes pause, and automated transitions halt until the server is restored. Scaling to higher throughput means vertical scaling - more CPU, more memory - which has hard limits and diminishing returns. Clustering is the ability to run multiple Workflow Engine runtime instances across separate servers, all connected to the same persistence database, with no external coordinator or message broker. With clustering, you add or remove servers without downtime. If one server fails, the remaining servers continue processing timers and executing commands. Timer execution is coordinated through the database itself - no leader election, no Redis, no ZooKeeper. For example, three application servers running in a cluster all poll for due timers every polling interval. The first instance to acquire a database-level lock on a timer record executes it; the other instances skip that timer. ## What it is Think of clustering as a team of workers sharing a single to-do list on a shared whiteboard. Each worker checks the list for new items, writes their name on the one they are picking up, and works through it. If a worker leaves, the others keep going. Nobody needs to tell the team who the leader is - the whiteboard (database) handles coordination naturally. Workflow Engine clustering is database-backed. All runtime instances read and write the same process instances, schemes, and timers in the shared persistence store. The mode is configured at runtime initialization: - **AsSingleServer** - Default mode. One runtime instance owns all timers and process execution. Uses an in-process timer loop that polls the database without distributed locking. Suitable when the application runs one instance. - **AsMultiServer** - Cluster mode. Each runtime instance competes for timer work using database-level locking. Each instance polls for due timers at a configurable interval, acquires a lock on the timer record, and executes it. No single point of failure. Dead runtime instances are detected through missing heartbeat signals in the `WorkflowRuntime` table. The runtime does not require sticky sessions, shared file systems, or distributed caches. Each instance is stateless with respect to runtime data - all state lives in the persistence database. **For developers:** `AsMultiServer()` creates a `MultiServerServiceRunner` and a `MultiServerTimeManager`. Timer polling interval, lock timeout, heartbeat interval, and maximum instance count are configurable through `MultiServerSettings`. Each runtime instance must have a unique ID (set via the `WorkflowRuntime(Guid)` or `WorkflowRuntime(string)` constructor). Heartbeat signals are written to the `WorkflowRuntime` database table. See Multi-Server Mode for configuration details and Single-Server Mode for the non-clustered alternative. ## Why it matters Clustering delivers these outcomes: - **No single point of failure** - If one server in the cluster fails, the remaining instances pick up its work. Timer execution, process completion, and automated transitions continue without interruption. The dead runtime's running processes are restored on surviving nodes via heartbeat-based detection. - **Horizontal scale-out without infrastructure complexity** - Add throughput by adding more application servers, not by upgrading hardware. The database-backed coordination model requires no Redis, no message broker, no ZooKeeper, no load balancer with sticky sessions. Each instance is identical - add or remove them by starting or stopping the process. - **Rolling updates with zero downtime** - Deploy a new version of the application one server at a time. The cluster remains operational throughout the update. Running process instances are not interrupted - they continue on the remaining instances and are handled by the updated instances once they rejoin. - **Linear cost model** - Each additional server improves throughput proportionally. The shared database is the only resource that needs capacity planning. Workflow Engine supports SQL Server, PostgreSQL, MySQL, MongoDB, and Oracle as the coordination database. ## Who it is for **Evaluator (CEO, CTO, PM):** Clustering removes the single-server bottleneck from production deployments. If you anticipate growth in process volume or need guaranteed uptime for workflow operations, clustering provides both - without adding infrastructure services to manage. The database-backed approach keeps operational complexity low: your team already manages databases. **Developer:** You configure clustering by calling `runtime.AsMultiServer(settings)` on each runtime instance. Each instance requires a unique ID. All clustering settings - polling interval, heartbeat interval, lock timeout - are in `MultiServerSettings`. See Multi-Server Mode for the configuration reference and Failover and Restore for process recovery behavior. **Enterprise architect:** Clustering uses the existing database for coordination, which fits into standard enterprise database backup, replication, and disaster recovery procedures. Runtime instances are stateless and can be deployed behind round-robin load balancers. Process-runtime associations are tracked in the `WorkflowRuntime` table, enabling targeted restore when a specific instance fails. ## When to use it Use clustering when a single server cannot meet your availability or throughput requirements. Typical examples: - **Production deployment with uptime requirements** - Your workflow system must remain operational during server maintenance, updates, or failures. A three-node cluster ensures that no single server outage stops process execution. The remaining nodes assume the load automatically. - **High-throughput process automation** - Your business processes generate thousands of process instances per hour with automated timer-driven transitions. A cluster spreads timer execution across multiple servers, increasing aggregate throughput beyond what a single instance can handle. - **Multi-region or multi-datacenter deployment** - Runtime instances in different data centers all point to the same shared database. The database-backed coordination model works across network boundaries without additional messaging infrastructure. Latency between instances affects timer polling responsiveness but does not break coordination. - **Rolling upgrade without process interruption** - You need to deploy a new version of the workflow application without stopping running processes. With clustering, stop each instance one at a time, update it, and restart. The cluster operates at reduced capacity during the update but never fully stops. ## How it compares The table below compares Workflow Engine's database-backed clustering approach with alternatives that require external coordination services. Table: Comparison of clustering approaches | Approach | What it requires | Result | |---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| | External coordinator (Redis, ZooKeeper, RabbitMQ) | Deploy and maintain a separate coordination service. Configure runtime instances to connect to it. Handle coordinator failover separately. | Three-tier infrastructure: application + database + coordinator. Each coordinator adds operational overhead, version dependencies, and its own failure modes. | | Workflow Engine database-backed clustering | All runtime instances share the same persistence database. No additional services. Instances coordinate through database-level locking and heartbeat signals. | Two-tier infrastructure: application + database. Coordination is an incidental property of the shared database - no extra service to deploy, monitor, or upgrade. | ## Frequently asked questions **What infrastructure do I need for clustering?** A shared database accessible from all cluster nodes. Workflow Engine supports SQL Server, PostgreSQL, MySQL, MongoDB, and Oracle as persistence providers. Each cluster node runs a standard ASP.NET Core application with the Workflow Engine NuGet package. No additional services - message queues, load balancers with sticky sessions, or distributed caches - are required. **How do timers coordinate across cluster nodes in multi-server mode?** Each runtime instance polls the database for due timers at a configurable interval (default 1000 ms in `MultiServerSettings`). When an instance finds a due timer, it acquires a database-level lock on that timer record. Only the instance holding the lock executes the timer. The polling interval and lock timeout prevent duplicate execution even with many nodes. The first instance to lock wins; others skip and continue polling. **Do I need a specific license for clustering?** Clustering in multi-server mode requires a commercial license. Workflow Engine Free supports `AsSingleServer` mode only. See Workflow Engine Editions for the full comparison. **What happens when a server in the cluster fails?** The surviving instances detect the failure through missed heartbeat signals. The `MultiServerServiceRunner` on each survivor checks the `WorkflowRuntime` table for instances that have not sent a heartbeat within the configured timeout. When a dead instance is detected, one survivor acquires a lock on the dead instance's record and restores all process instances that were associated with it. Other survivors detect the lock and skip, preventing duplicate recovery. See Failover and Restore for the detailed flow. **Can I limit the number of cluster nodes?** Yes. Set `MaxNumberOfInstances` in `MultiServerSettings`. On startup, the `MultiServerServiceRunner` counts active runtime instances in the `WorkflowRuntime` table. If the limit is reached, the new instance waits and retries according to `StartStatusCheckPauseInterval` and `StartStatusCheckAttemptCount`. This prevents license violations when running under a fixed-instance license. ## See also --- # Multitenancy URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/multitenancy/ # Multitenancy Without multitenancy, every customer or business unit needs its own deployment of Workflow Engine - its own application, its own database, its own monitoring. That model becomes expensive and hard to operate as you add customers. Every new client means provisioning another environment, applying another set of upgrades, and managing another set of backups. Multitenancy lets a single Workflow Engine deployment serve many customers or business units (tenants) from one application instance. Tenant separation works at two levels: - **Physical tenancy** - each tenant gets its own database and its own `WorkflowRuntime` instance. Full isolation at the persistence layer. Suitable for SaaS platforms where each client must have an independent database. - **Logical tenancy** - tenants share the same `WorkflowRuntime` and database. Tenant-aware persistence records are isolated by `TenantId`. Suitable for multi-division enterprise deployments where operational simplicity is more important than physical database separation. The Workflow Engine Web API supports both levels and a hybrid model. Each registered physical tenant owns a runtime and data provider, and can serve one or more logical tenant IDs. With multitenancy, you can add new customers without duplicating infrastructure. For example, a SaaS document approval platform can host workflows for dozens of client organizations from one application - each client gets isolated data, but the operations team manages a single deployment. ## What it is Multitenancy is like an apartment building. All tenants share the same building entrance and hallways (the application), but each apartment has its own lock and its own mailbox. Depending on your model, either each apartment has its own building (physical - separate database per tenant) or each apartment has its own locked box inside the same building (logical - `TenantId` filtering in shared storage). A hybrid model gives some tenants separate buildings while other tenants share one building. The tenant identifier is a string value set when a process instance is created: ```csharp var createParams = new CreateInstanceParams { SchemeCode = "OrderApproval", TenantId = "acme-corp", IdentityId = "user-42" }; await runtime.CreateInstanceAsync(createParams); ``` The `TenantId` is stored as a system parameter on every process instance. It is automatically propagated to subprocesses and persisted with tenant-aware process data, including statuses, parameters, timers, transition history, inbox entries, and approval history. Workflow Engine Web API uses the selected logical tenant ID for Data API filtering and strict RPC API validation. A process from another logical tenant is not exposed to the caller. The tenant header selects context; API permissions separately determine whether the caller may access that tenant. ### Physical tenancy (HTTP API) In the HTTP API layer, each physical tenant has its own `WorkflowRuntime` instance and data provider. The `IWorkflowTenantLocator` resolves which physical tenant to use for each request based on the `Workflow-Api-Tenant-ID` HTTP header and the request-scoped tenant snapshot. The header name is defined as the constant `WorkflowApiConstants.TenantIdHeader`. A request without the header is rejected with `WorkflowTenantIdNotProvidedException` (unless a default tenant is configured). ```csharp // Program.cs - configure tenants with separate databases builder.Services.AddWorkflowApiCore(options => { options.DefaultTenantId = null; // Require header on every request }); builder.Services.AddWorkflowTenants( new WorkflowTenantCreationOptions { TenantIds = ["acme-corp"], ConnectionString = "Server=db-acme;Database=Workflow;...", PersistenceProviderId = PersistenceProviderId.Mssql }, new WorkflowTenantCreationOptions { TenantIds = ["globex"], ConnectionString = "Server=db-globex;Database=Workflow;...", PersistenceProviderId = PersistenceProviderId.Mssql }); ``` Each `IWorkflowTenant` holds one `WorkflowRuntime`, one `IDataProvider`, and one or more logical tenant IDs. The snapshot maps every logical ID to its physical tenant, and the locator resolves that mapping for each HTTP request. ### Logical tenancy (Core runtime and HTTP API) In the logical model, one `WorkflowRuntime` and data provider serve multiple tenant IDs. Core operations use the `TenantId` parameter on `CreateInstanceParams` and tenant-aware persistence records for data isolation. In Workflow Engine Web API, list multiple IDs in one `WorkflowTenantCreationOptions.TenantIds` array. Requests for those IDs resolve to the same `IWorkflowTenant`, while the selected ID scopes Data API and RPC API operations. Your action and rule code can read `processInstance.TenantId` to make tenant-specific decisions: ```csharp public async Task ExecuteActionAsync(string name, ProcessInstance process, WorkflowRuntime runtime, string parameter, CancellationToken token) { var tenantId = process.TenantId; // Use tenantId to load tenant-specific configuration } ``` ### Hybrid tenancy (HTTP API) Hybrid tenancy combines physical and logical mappings in one Workflow Engine Web API host. Configure it by passing multiple `WorkflowTenantCreationOptions` entries to `AddWorkflowTenants()`. Each entry creates a physical tenant with its own runtime and provider, while its `TenantIds` array assigns one or more logical tenant IDs to that tenant: ```csharp // Physical: acme-corp has its own database and runtime // Logical: globex and stark share a database with TenantId filtering builder.Services.AddWorkflowTenants( new WorkflowTenantCreationOptions { TenantIds = ["acme-corp"], ConnectionString = "Server=db-acme;...", PersistenceProviderId = PersistenceProviderId.Mssql }, new WorkflowTenantCreationOptions { TenantIds = ["globex", "stark"], ConnectionString = "Server=db-shared;...", PersistenceProviderId = PersistenceProviderId.Mssql }); ``` ## Why it matters Multitenancy delivers these outcomes: - **Lower infrastructure cost per customer** - A single application server handles all tenants. You do not provision separate application infrastructure for each customer. - **Faster customer onboarding** - Adding a new tenant means configuring a tenant identifier and a database connection string. Onboarding takes minutes instead of days. - **Centralized operations** - One application to deploy and monitor, one set of upgrades to apply. Operations teams manage one system, not one per customer. - **Flexible isolation model** - Choose physical isolation for separate databases, logical isolation for shared storage, or a hybrid model for different customer tiers in one API host. - **Data separation at the persistence level** - Physical tenants can use separate databases. For logical tenants, Data API queries use the selected `TenantId`, and RPC API operations validate the process tenant. ## Who it is for **Evaluator (CEO, CTO, PM):** Multitenancy is the infrastructure that lets you offer process automation as a SaaS product. It reduces per-customer cost, simplifies operations, and removes the architectural barrier to adding new clients. Choose physical tenancy for regulated industries that require database-level separation, or logical tenancy for internal multi-division deployments. **Developer:** Configure one or more tenant identifiers for each runtime and data provider. In the HTTP API, use the `Workflow-Api-Tenant-ID` header to select a logical tenant. In the core runtime, set `TenantId` on `CreateInstanceParams`. Read `processInstance.TenantId` in actions and rules for tenant-specific behavior. **Enterprise architect:** Physical tenancy gives a tenant its own `WorkflowRuntime` and `IDataProvider`. Logical tenancy maps multiple tenant IDs to one runtime and provider with `TenantId` filtering. Hybrid tenancy combines both mappings in the same API host. ## When to use it Use multitenancy when your business model requires data separation between customers or business units but you want operational efficiency from a shared deployment. Typical examples: - **SaaS process automation platform** - You host approval workflows, document reviews, or compliance processes for multiple client organizations from a single application. Physical tenancy gives selected clients an independent database, while logical tenancy keeps other clients isolated in shared storage. - **Multi-division enterprise deployment** - A single enterprise runs Workflow Engine for HR, finance, and operations. Each division has isolated data, and regulatory requirements demand data separation between divisions. Logical tenancy with `TenantId` filtering is usually sufficient. - **ISV embedding workflows** - Your product ships with embedded workflow capabilities. Each of your customers runs their own workflows in isolation, but you manage one deployment rather than one per customer. ## How it compares There are four approaches to tenant separation. The table below shows the tradeoffs. Table: Comparison of tenant isolation approaches | Approach | What it requires | Result | |----------------------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------| | Separate deployment per tenant | Provision, monitor, and maintain one full application and database per customer | High infrastructure cost, complex operations, slower onboarding | | Workflow Engine logical tenancy | One `WorkflowRuntime` and provider serve multiple tenant IDs. | Lower cost, centralized management, tenant-aware data filtered by `TenantId` | | Workflow Engine physical tenancy | One application deployment. Each tenant has its own database and `WorkflowRuntime`. | Lower infrastructure cost, full data isolation, per-tenant backup/restore | | Workflow Engine hybrid tenancy | One API host maps some tenant IDs to dedicated runtimes and providers and groups other IDs on shared ones. | Different isolation levels for different customer or regulatory requirements | The key tradeoff is operational complexity vs. isolation strength. Separate deployments give full independence (including per-tenant application versions) but add operational overhead. Logical tenancy shares everything except the `TenantId` filter. Physical tenancy shares the application but not the database. Hybrid tenancy applies either model per tenant group without adding another API host. ## See also ## Frequently asked questions **Does multitenancy require a specific license?** Yes. Multitenancy requires a Workflow Engine NEO license that enables the multitenancy feature. See Workflow Engine Editions for licensing details. **How does the API identify which tenant a request belongs to?** The HTTP API reads the `Workflow-Api-Tenant-ID` HTTP header on each request. The header value identifies a logical tenant, and the request snapshot maps it to the physical tenant's `WorkflowRuntime` and data provider. If no header is provided, the API uses `DefaultTenantId`; if neither value is available, the API returns an error. **Can a tenant access another tenant's processes if they know the process ID?** In physical tenancy, each tenant can use separate storage. In logical and hybrid tenancy, Data API operations filter by the selected tenant ID and RPC API operations validate the process tenant. A process from another tenant is returned as not found rather than exposed to the caller. **How does multitenancy affect backups and restores?** In the physical model, each tenant can have its own database, so you can back up and restore tenants independently. In the logical model, tenants share a database, so restore affects that tenant group. In the hybrid model, the database connection assigned to each physical tenant group determines the backup boundary. **What happens to running instances when I add a new tenant?** Adding a new tenant is a registry change. Register a new `IWorkflowTenant`, or create one from `WorkflowTenantCreationOptions` with one or more logical IDs. The registry publishes a new immutable snapshot for new requests; existing requests keep their previous snapshot until they complete. **How does TenantId propagate to subprocesses?** `TenantId` is a system parameter with `MergeIntoParentProcessIsProhibited = true`. It is carried forward when a subprocess is created from the parent, but when the subprocess merges back, the parent's `TenantId` is not overwritten. This ensures the tenant identity is preserved throughout the process tree. **Can I control which users can access which tenants?** Yes. The HTTP API includes a granular permission system. Use `IWorkflowApiPermissions.BuildClaim(...)` and `IWorkflowApiPermissionsBuilder` to configure tenant-level rules: `AllowAllTenants()`, `DenyAllTenants()`, `AllowAllTenantsExcept(...)`, or `DenyAllTenantsExcept(...)`. API authorization checks both operation and tenant permissions on every secured request. --- # Attachable Forms URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/attachable-forms/ # Attachable Forms Without attachable forms, you build a custom data entry screen for every activity in every workflow scheme. An expense report with three approval steps needs three different UIs, each hardcoded to that step's fields. Adding a step means building another UI and redeploying. Attachable forms is a plugin system that ties form definitions to workflow activities so that each activity renders the right form, with fields mapped to process parameters, without custom UI code per step. With attachable forms, you can define a form once on each activity in the scheme and have your application render it automatically. For example, an expense approval workflow defines an "Expense Details" form on the submit activity, a "Manager Review" form on the approval activity, and a "Finance Approval" form on the final sign-off - each with fields mapped to process parameters, each served by the runtime without per-activity UI code. ## What it is Think of an airport departure board. Each gate shows the right flight information for that gate - passengers at gate A see gate A's flight details, passengers at gate B see gate B's. Attachable forms work the same way: each activity in a workflow scheme has its own form definition, and when a process instance reaches that activity, the runtime surfaces the correct form for that step. The Forms Plugin adds a "Show Form" custom activity type to the Designer. When you add this activity to a scheme, you configure which form it should display, which process parameters the form reads from and writes to, and which commands (approve, reject, submit) the form should expose. Form definitions are stored in the database through the `IFormDataProvider` persistence interface - the same provider that stores schemes and process instances. On the frontend, your application calls the `FormsRuntimeApi` to discover which forms are available for the current activity, retrieve the form definition, and submit filled forms back. The plugin handles parameter mapping, draft persistence, and command execution. **For developers:** Install the `WorkflowEngine.NETCore-FormsPlugin` NuGet package. Create a `FormsPluginSettings` instance (requires the `FormsManagerUrl` pointing to your form manager frontend) and register `FormsPlugin` with `WorkflowRuntime`: ```csharp var formsSettings = new FormsPluginSettings { FormsManagerUrl = "https://forms.example.com" }; runtime.WithPlugin(new FormsPlugin(formsSettings)); ``` Use `FormsRuntimeApi` to interact with forms at runtime: `GetFormAsync()` to load a form definition, `GetExecutableFormsAsync()` to discover forms available for the current activity, `SaveFormAsync()` to persist draft data, and `ExecuteFormAsync()` to submit form data and execute the associated command. Forms use versioned definitions stored via `IFormDataProvider`. ## Why it matters Attachable forms deliver these outcomes: - **No per-activity UI code** - Define forms on activities in the Designer instead of building custom screens for workflow steps that need user input. The runtime surfaces the right form automatically. - **Forms evolve with schemes** - Change a form definition or add a new activity with its own form without redeploying your application. The Designer stores form versions, and the plugin resolves the current version at runtime. - **Parameter mapping is built in** - Form fields map directly to process parameters. Submit a form and the runtime writes field values to parameters and executes the associated command in one operation. - **Draft support out of the box** - The plugin manages draft data per form and per activity. A user can start filling a form, save progress, and return later without losing data. ## Who it is for **Evaluator (CEO, CTO, PM):** Attachable forms remove the bottleneck of building custom UIs for workflow steps that need user input. Your team designs forms in the Designer alongside the process flow, not in a separate frontend sprint. **Developer:** Create a `FormsPluginSettings` with the `FormsManagerUrl`, register `FormsPlugin` with `WorkflowRuntime`, and add the "Show Form" custom activity in schemes. Call `FormsRuntimeApi` methods from your frontend to fetch forms, save drafts, and submit data. No controller code per form is needed. **Enterprise architect:** Forms are stored as versioned definitions in the same persistence provider as process data. The plugin integrates with the runtime's existing rule provider for permission checks on form field access and command execution. ## When to use it Use attachable forms when some activities in your workflow need user input and you want the form-to-activity mapping managed by the runtime instead of hardcoded in your application. Typical examples: - **Structured approval workflows** - An expense report shows an "Expense Details" form at submission, a "Manager Review" form at approval, and a "Finance Approval" form at final sign-off. Each form has different fields and different associated commands. - **Onboarding sequences** - A new employee onboarding workflow shows a "Personal Details" form, then a "Department Setup" form, then an "IT Access Request" form - each at a different activity in the same scheme. - **Case management** - A customer support workflow presents different forms depending on the case type. The scheme routes to the appropriate activity, and the plugin resolves the form definition for that activity. - **Multi-version form deployments** - Update a form definition while instances are running. Previous instances use the form version that was current when they reached the activity; new instances use the updated version. ## How it compares The table below shows the tradeoffs between the Workflow Engine approach and the alternative. Table: Comparison of attachable forms approaches | Approach | What it requires | Result | |----------------------------------|----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------| | Build custom UIs per activity | Build, test, and deploy a separate data entry screen for every workflow step | High development effort, tightly coupled to scheme changes, redeployment required for each form change | | Workflow Engine attachable forms | Define forms on activities in the Designer; use `FormsRuntimeApi` to render and submit | Forms managed alongside the scheme, parameter mapping built in, no per-activity UI code | The Forms Plugin gives you the flexibility of the schema-driven approach without forcing you into a specific frontend framework. Your application decides how to render forms; the plugin handles discovery, parameter mapping, validation, and persistence. ## Frequently asked questions **Does attachable forms require a specific license?** Yes. The Forms Plugin is a license-gated feature. Your license must include the "Forms" capability. See Workflow Engine Editions for licensing details. **Which databases support the Forms Plugin?** Any database that has a persistence provider implementing `IFormDataProvider`. All six built-in providers - SQL Server, PostgreSQL, MySQL, MongoDB, Oracle, and SQLite - include form storage support. The form schema and version data are stored in a forms table created during provider migration. **How do form fields relate to process parameters?** Each form defines input parameters (data loaded into the form), output parameters (data written when a command executes), and draft parameters (progress saved mid-form). These are configured in `FormSettings` when setting up the "Show Form" activity. The plugin reads and writes the named process parameters automatically on form submission. **Can I use the Forms Plugin with a custom frontend framework?** Yes. The plugin ships with frontend packages (`@optimajet/workflow-forms-manager` and `@optimajet/workflow-forms-viewer`) for React. You can also build a custom frontend using any framework - the `FormsRuntimeApi` returns form definitions as structured data, not pre-rendered HTML. **What happens to in-progress forms when the form definition is updated?** The plugin stores form versions. A process instance that has already loaded a form uses the definition version that was current when the form was displayed. Only new form loads (new process instances reaching the activity after the update) use the latest version. You can manage versions through the Designer's form management actions. ## See also --- # Customization URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/customization/ # Customization Without customization, the Visual Designer and runtime offer a fixed set of activity types, input controls, and condition options. They work for any domain but are not tailored to yours - activity types use generic names, input fields are plain text boxes, and autocomplete suggestions know nothing about your business terms. Customization lets you keep the standard components while adapting them to your domain: register custom activity types that appear in the Designer palette, provide autocomplete suggestions for your business terms, replace plain text fields with domain-specific controls, and change the Designer's look and feel to match your brand. With customization, you can give the Designer your application's look and feel, replace a generic text input with a user picker dropdown, and provide autocomplete suggestions for your domain terms so business analysts can write conditions without learning C#. ## What it is Think of a car dashboard. The manufacturer offers a standard layout and controls, but you can swap the radio for a touchscreen, change the gauge colors, and add a GPS unit. The dashboard stays the same shape and works the same way - you customize the parts that matter to you. The Visual Designer exposes extension points that let you change how it looks and behaves without touching its source code. You configure these customizations when you initialize the Designer in your application. The main extension points are CSS and template overrides for visual appearance, parameter format providers for custom input controls, autocomplete providers for domain-specific suggestions, action and rule providers for Designer selection lists, custom activity types that appear in the Designer palette, and custom condition types for transitions. Each extension point is opt-in - you customize only what you need and leave the defaults for everything else. **For developers:** Implement `IDesignerParameterFormatProvider` to register custom input controls. Implement `IDesignerAutocompleteProvider` to supply domain-specific suggestions. Implement `IWorkflowActionProvider` to register actions and conditions, and implement `IWorkflowRuleProvider` to register rules. The names returned by `GetActions()`, `GetConditions()`, and `GetRules()` appear in the corresponding Designer lists. Implement `ICustomActivityProvider` to add custom activity types to the Designer palette. Implement `ICustomConditionProvider` to register custom condition types. Register providers with `WorkflowRuntime.WithDesignerParameterFormatProvider()`, `WorkflowRuntime.WithDesignerAutocompleteProvider()`, `WorkflowRuntime.WithActionProvider()`, `WorkflowRuntime.WithRuleProvider()`, or `WorkflowRuntime.WithCustomActivities()`. Custom styles are applied through CSS overrides at Designer initialization. Plugins that implement these interfaces are auto-detected and registered by `WithPlugin()`. ## Why it matters Customization delivers these outcomes: - **Custom logo watermark** - Add your company logo to the Designer canvas. Configure `settings.logo` with an image URL and `settings.logoPosition` to place it in any corner (`top-left`, `top-right`, `bottom-left`, `bottom-right`). The logo renders as a watermark on the scheme canvas. - **On-brand user experience** - Override CSS classes (`.WorkflowDesignerContainer`, `.WorkflowDesignerToolbar`, and others) to apply your theme, color palette, and typography to the Designer so users do not perceive it as a separate tool. - **Domain-specific input controls** - Replace generic text fields with controls that match your domain. A cost-center selector instead of a free-text field, a user picker instead of a text input for assignee selection, a date range picker for deadline configuration. - **Business-user-friendly condition editing** - Provide autocomplete suggestions for domain terms like department names, document types, or approval thresholds. Business analysts write conditions without knowing C# or the underlying parameter names. - **Custom activity types and conditions** - Add custom activity types that appear in the Designer palette, and custom condition types that scheme designers can use on transitions. Both are registered through provider interfaces and auto-detected by plugins. Action and rule providers separately supply the action, condition, and rule names shown in Designer lists. ## Who it is for **Evaluator (CEO, CTO, PM):** Customization means the Designer feels like part of your product, not a third-party tool bolted on. Business users work with terms they already know, and the look matches your brand's design system. **Developer:** Register custom providers at Designer initialization. The `IDesignerParameterFormatProvider`, `IDesignerAutocompleteProvider`, `ICustomActivityProvider`, and `ICustomConditionProvider` interfaces are simple - implement one or two methods and register the instance. Use `IWorkflowActionProvider` and `IWorkflowRuleProvider` with `WithActionProvider()` and `WithRuleProvider()` to supply executable Designer choices. CSS overrides go in a stylesheet you load alongside the Designer component. Plugins that implement these interfaces register them automatically through `WithPlugin()`. See the Customization Overview for the full API. **Enterprise architect:** Each application instance can have its own set of customization providers. Two different teams embedding the Designer can configure different parameter controls and autocomplete sources without affecting each other. Customizations are scoped per `WorkflowRuntime` instance. ## When to use it Use customization when the Designer's default appearance or input controls do not match your application's requirements. Typical examples: - **Brand-aligned process designer** - Your SaaS product embeds the Designer for customers to build their own workflows. The Designer must match your product's theme, not the Workflow Engine default look. Apply CSS overrides at Designer initialization. - **User picker for assignee fields** - A workflow for a healthcare application needs a "Doctor" selector that queries the hospital's directory service. Implement `IDesignerParameterFormatProvider` to register a custom user-picker control. - **Simplified approval conditions** - Business analysts configure approval routing without C#. Implement `IDesignerAutocompleteProvider` to supply terms like "ApprovalLimit", "DepartmentHead", and "BudgetCode" as autocomplete candidates in the condition editor. Use `IWorkflowActionProvider` and `IWorkflowRuleProvider` when actions, conditions, and rules must be selected from lists. - **Custom activity palette** - A document review workflow needs a "Signature" activity type that is not part of the default palette. Implement `ICustomActivityProvider` to register it, and it appears in the Designer alongside built-in activity types. ## How it compares The following table compares the Workflow Engine customization approach with the alternative of forking the Designer source code. Table: Comparison of customization approaches | Approach | What it requires | Result | |-------------------------------------|-------------------------------------------------------------------------------------|-------------------------------------------------------------------------------| | Fork and modify the Designer source | Maintain a fork, track upstream changes, rebuild on every update | High maintenance cost, risk of divergence from upstream, complex upgrade path | | Workflow Engine customization | Configure CSS, implement one or two provider interfaces, register at initialization | No fork to maintain, updates are non-breaking, customize only what you need | The CSS and provider-based approach means you stay on the main Designer release while tailoring what matters to your users. New Designer features arrive with upgrades automatically. ## Frequently asked questions **What parts of the Designer can I customize without modifying its source code?** You can customize the logo watermark (`settings.logo` and `settings.logoPosition`), CSS styles (toolbox, canvas, activity shapes, property panels), parameter input controls (via `IDesignerParameterFormatProvider`), autocomplete suggestions (via `IDesignerAutocompleteProvider`), custom activity types (via `ICustomActivityProvider`), and custom condition types (via `ICustomConditionProvider`). `IWorkflowActionProvider` and `IWorkflowRuleProvider` also populate the action, condition, and rule selection lists. You cannot change the scheme logic, serialization format, activity behavior, or runtime execution through the Designer. **How do I register a custom parameter control?** Implement `IDesignerParameterFormatProvider` which has a single method: `GetFormat(CodeActionType type, string name, string schemeCode)`. Register it with `WorkflowRuntime.WithDesignerParameterFormatProvider()`. The Designer calls your provider when it needs to render a parameter input in the activity property panel. **Does each application instance get its own set of customizations?** Yes. Customization providers are registered per `WorkflowRuntime` instance. Two applications embedding the Designer in different parts of your system can use different providers, different styles, and different autocomplete sources - independently configured. **Can plugins add custom Designer providers?** Yes. The `ActiveDirectoryPlugin` ships with `ActiveDirectoryDesignerParameterFormatProvider` and `ActiveDirectoryDesignerAutocompleteProvider` implementations that register user and group selectors from Active Directory. Plugins register providers through `WorkflowRuntime.WithDesignerParameterFormatProvider()` just like application code. **What happens during Designer upgrades - do customizations break?** Provider interfaces (`IDesignerParameterFormatProvider`, `IDesignerAutocompleteProvider`) follow semantic versioning within Workflow Engine. CSS class names may change between major versions but are stable within a major version. Review the upgrade notes for CSS changes before each major upgrade. ## See also --- # Multi-Database Support URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/multi-database-support/ # Multi-Database Support Workflow Engine requires a database to run. Every process instance, timer, scheme definition, and history record is stored in a persistence provider. The choice is not whether to use a database - it is which database to use. The runtime survives restarts, multiple servers coordinate through the same database, and you pick the database your organization already manages. For example, an organization standardized on PostgreSQL installs the PostgreSQL provider, calls `RunMigrations()` to create the schema, and the runtime begins persisting instances, timers, and history - without writing a single migration script. ## What it is Think of a universal power adapter. It has the same plug on one end (your application) and interchangeable connectors on the other end (the database). Multi-database support works the same way: `WorkflowRuntime` talks to a provider interface, and the provider translates those calls into native SQL (or document queries) for your chosen database. Workflow Engine ships providers for six databases: SQL Server, PostgreSQL, MySQL, MongoDB, Oracle, and SQLite. Each provider is a separate NuGet package - you install only the one for your database. Every provider implements the same `IPersistenceProvider` interface. Switching databases means changing the NuGet package and connection string - your application code does not change. Each provider includes automatic schema management. Calling `RunMigrations()` on the runtime creates all required tables, indexes, and procedures. When you upgrade Workflow Engine, the same method detects the current schema version and applies only the pending migrations. You do not write migration scripts or track schema versions manually. **For developers:** Install the NuGet package for your database. The package IDs follow the pattern `WorkflowEngine.NETCore-ProviderFor{Name}` - for example, `WorkflowEngine.NETCore-ProviderForMSSQL` or `WorkflowEngine.NETCore-ProviderForPostgreSQL`. Register the provider with `WorkflowRuntime`, call `RunMigrations()`, and the schema is created automatically. See the Persistence Providers page for installation steps. ## Why it matters Multi-database support delivers these outcomes: - **Choose your existing database** - Use the database your operations team already manages. No need to introduce a new database platform just for workflow state. Every provider speaks the same interface; switching requires only package and connection string changes. - **Automatic schema management** - Call `RunMigrations()` once during startup. Tables, indexes, and procedures are created automatically. Version upgrades apply only the migrations you need - no manual SQL scripting. - **Clustering enabled** - The persistence provider is the shared state that lets multiple `WorkflowRuntime` instances coordinate as a cluster. Each instance reads and writes through the same database, distributing timer execution and respecting instance locks. - **Extensible to unsupported databases** - The `IPersistenceProvider` interface is public. If your database is not among the six built-in options, implement the interface and register the custom provider with the runtime. ## Who it is for **Evaluator (CEO, CTO, PM):** Multi-database support means Workflow Engine fits into your existing infrastructure. If your organization runs PostgreSQL, use the PostgreSQL provider. If you are on SQL Server, use the SQL Server provider. The runtime does not dictate your database choice. **Developer:** Install one NuGet package, register the provider, call `RunMigrations()`. Application code uses the same `WorkflowRuntime` API regardless of which database is underneath. **Enterprise architect:** The provider layer enforces a consistent data contract across all supported databases. Five of the six providers (SQL Server, PostgreSQL, MySQL, Oracle, SQLite) implement `IMigratable` for managed schema evolution. The MongoDB provider uses a document model with the same schema contract. ## When to use it Use a persistence provider in every Workflow Engine deployment - a provider is required for the runtime to function. The choice is which database to use. Typical scenarios: - **Standardize on your existing database** - Your organization runs PostgreSQL for all application data. Install the PostgreSQL provider, call `RunMigrations()`, and use the same database infrastructure for workflow state. No new database platform required. - **Embedded or local deployment** - A desktop application or single-server deployment uses SQLite for lightweight persistence with no database server setup. The provider runs as a file-based database. - **Clustered production deployment** - Multiple application servers run Workflow Engine in a cluster. All instances connect to the same SQL Server or PostgreSQL database, using the persistence provider as the coordination layer for timers and instance locks. - **Custom or unsupported database** - Your organization uses a database not in the six built-in providers. Implement `IPersistenceProvider` for your database and register the custom class with `WorkflowRuntime`. The runtime treats it identically to a built-in provider. ## How it compares The table below compares Workflow Engine's persistence provider approach with alternatives for managing workflow state. Table: Comparison of persistence provider approaches | Approach | What it requires | Result | |--------------------------------|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | Built-in persistence providers | Install one NuGet package, register the provider, call `RunMigrations()` | Automatic schema management, clustering support, same API across all six databases | | Custom persistence provider | Implement `IPersistenceProvider` for your database and register it with the runtime | Full control over storage, same runtime API, manual maintenance of provider code | Workflow Engine ships providers for the six most-requested databases. If you need a database not in this list, the `IPersistenceProvider` interface is public for custom implementations. ## Frequently asked questions **Which databases does Workflow Engine support for persistence?** Six databases: SQL Server, PostgreSQL, MySQL, MongoDB, Oracle, and SQLite. Each has its own NuGet package and implements `IPersistenceProvider`. The NuGet package IDs use the format `WorkflowEngine.NETCore-ProviderFor{Name}`. **Can I switch databases while process instances are running?** No. Switching the persistence provider after process instances exist is not supported. Each provider stores runtime data in its native format, and there is no cross-provider migration tool. Choose your database before the first production deployment. **Does the provider create database tables automatically?** Yes. The runtime's `RunMigrations()` method creates all required tables, indexes, and procedures during startup. On subsequent runs, it detects the current schema version and applies only pending migration steps. You do not need separate SQL scripts. **Can I use a different database for different runtime instances?** Yes. Each `WorkflowRuntime` instance is configured with one persistence provider. You can configure different instances with different providers - for example, a development instance with SQLite and a production instance with PostgreSQL - by changing the provider registration and connection string. **Can I implement a custom provider for an unsupported database?** Yes. The `IPersistenceProvider` interface is public. Implement the required methods for your database and register the provider with `WorkflowRuntime`. All six built-in providers are open-source implementations you can use as reference. **Do I need a specific license for multi-database support?** No. The persistence providers are part of the core runtime and available in all editions. See Workflow Engine Editions for licensing details. ## See also --- # Bulk API URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/bulk-api/ # Bulk API Running the same workflow operation on hundreds or thousands of process instances one at a time is slow, error-prone, and hard to manage. Every failure needs manual error handling, and sequential execution wastes time. The Workflow Engine Bulk API runs runtime operations on multiple process instances in parallel through a single method call. Each operation is isolated, so one failure never blocks the others. With the Bulk API, you can create, query, command, or delete hundreds of process instances with one call. For example, approving a queue of 500 leave requests by executing the same command on each instance, with per-item success or failure reporting. ## What it is Think of it as an express lane for batch operations. Instead of walking each item through the regular counter one at a time - calling a single-instance method in a `foreach` loop with manual `Task.WhenAll` and per-item error handling - you hand the whole list at once. The system processes them in parallel up to a configurable limit, tracks each result independently, and reports back which succeeded and which failed. **For developers:** The Bulk API is exposed as `runtime.Bulk` on `WorkflowRuntime`. It mirrors single-instance methods with collection-based inputs and returns a `Dictionary` where every input key maps to its own result or exception. ```csharp using OptimaJet.Workflow.Core.Runtime; // Create 100 process instances from the same scheme var processIds = Enumerable.Range(1, 100) .Select(i => Guid.NewGuid()) .ToList(); Dictionary results = await runtime.Bulk.CreateInstanceAsync(processIds, "InvoiceApproval"); ``` The Bulk API supports the following operations: Table: Bulk API methods | Method | Description | |---------------------------------------------------|---------------------------------------------------------------------------------------------------------------| | `CreateInstanceAsync` | Create process instances from a scheme by `CreateInstanceParams` or by list of process IDs with a scheme code | | `GetProcessInstanceAndFillProcessParametersAsync` | Fetch multiple instances with their parameters | | `GetProcessInstancesTreeAsync` | Fetch the process instance trees for multiple root processes | | `GetAvailableCommandsAsync` | Get available commands for multiple instances, with identity filtering and culture support | | `ExecuteCommandAsync` | Execute a command on multiple instances | | `ExecuteCommandWithRestrictionCheckAsync` | Execute commands with permission verification | | `UpdateSchemeIfObsoleteAsync` | Update scheme on multiple instances if a newer version exists | | `DeleteInstanceAsync` | Delete multiple instances | | `IsProcessExistsAsync` | Check existence of multiple instances | ### Per-instance error isolation When `throwOnError` is `false` (the default), every operation runs independently. The returned dictionary contains a `BulkTaskResult` for each input identifier, with `State` set to `Completed` or `Failed` and the `Exception` property populated on failures: ```csharp var results = await runtime.Bulk.CreateInstanceAsync(processIds, "InvoiceApproval"); foreach (var (processId, result) in results) { if (result.State == BulkTaskState.Failed) { Console.WriteLine($"Process {processId} failed: {result.Exception?.Message}"); } } ``` Set `throwOnError` to `true` to throw an `AggregateException` containing all errors when any operation fails. You can also configure defaults through `runtime.Bulk.DefaultThrowOnError`. ### Concurrency control The `maxDegreeOfParallelism` parameter controls how many operations run concurrently. The default is set by `runtime.Bulk.DefaultMaxDegreeOfParallelism`, which defaults to `Environment.ProcessorCount`. Lower it to reduce database connection pressure in high-contention environments: ```csharp var results = await runtime.Bulk.CreateInstanceAsync( processIds, "InvoiceApproval", maxDegreeOfParallelism: 4 ); ``` ### Per-task progress tracking Pass a `taskFinalizedHandler` to receive a callback as each individual operation completes: ```csharp var results = await runtime.Bulk.CreateInstanceAsync( processIds, "InvoiceApproval", taskFinalizedHandler: (sender, args) => { Console.WriteLine( $"Process {args.LastFinalizedTaskId}: {args.LastFinalizedTaskResult.State}" ); } ); ``` ## Why it matters The Bulk API delivers these outcomes: - **Faster batch processing** - Operations run in parallel up to the configured concurrency limit, not sequentially. Creating 500 process instances with one call is significantly faster than a `foreach` loop with 500 individual calls. - **Resilient to partial failures** - One bad process ID does not derail the entire batch. Each item succeeds or fails independently. Failed items are reported with their exception details so you can retry or audit them later. - **Less integration code** - No manual `Parallel.ForEach` with error handling, no `Task.WhenAll` with result aggregation, no try-catch blocks around every single call. The Bulk API handles parallelism, error isolation, and result mapping. - **Progress visibility** - The optional `taskFinalizedHandler` callback fires after each item completes, enabling progress bars, logging, or early warning of failure patterns. ## Who it is for **Evaluator (CEO, CTO, PM):** Bulk operations reduce the time and code needed for batch workflows - data migrations, mass approvals, system cleanup. This means faster deployment of bulk processing features and fewer integration bugs. **Developer:** The Bulk API replaces complex parallel execution patterns with a single method call and handles error isolation automatically. Use `runtime.Bulk` on `WorkflowRuntime` with collection-based inputs. **Enterprise architect:** The concurrency limit prevents resource exhaustion. Each operation is isolated, so a corrupted process instance never blocks other operations. ## When to use it Use the Bulk API when you need to perform the same workflow operation on many process instances at once: - **Bulk data migration or import** - Create hundreds or thousands of process instances from the same scheme using `CreateInstanceAsync` with a list of process IDs or `CreateInstanceParams` objects. - **Mass approval or escalation** - Collect available commands from multiple instances, then execute the same command (approve, reject, escalate) across all of them. - **Batch cleanup** - Delete completed, abandoned, or expired process instances by calling `DeleteInstanceAsync` with a list of IDs, instead of looping with individual delete calls. - **Batch scheme update** - Call `UpdateSchemeIfObsoleteAsync` to bring a set of in-flight instances up to the latest scheme version, useful after deploying a process change. ## How it compares The table below shows the tradeoffs between the Workflow Engine approach and the alternative. Table: Comparison of bulk processing approaches | Approach | What it requires | Result | |---------------------------------|-----------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| | Manual loop with `Task.WhenAll` | Write parallel execution code, manage task limits, handle per-item exceptions manually, aggregate results | Working solution, but more code to write, test, and maintain - each developer may implement it differently | | Workflow Engine Bulk API | Pass a collection of identifiers and optional concurrency limit | Built-in parallelism, error isolation, progress tracking, and result mapping - consistent across every bulk operation | The Bulk API standardizes a pattern you would otherwise write from scratch every time, with proper error isolation and concurrency control already built in. ## Frequently asked questions **What operations does the Bulk API support?** The Bulk API supports create instance, get process instance with parameters, get process instances tree, get available commands, execute command, execute command with restriction check, update scheme if obsolete, delete instance, and process existence check. **Does the Bulk API require a NEO license?** No. The Bulk API on `WorkflowRuntime` (`runtime.Bulk`) is available in all editions. The HTTP endpoints that wrap bulk operations are part of the RPC API and subject to its licensing. **How does the Bulk API handle partial failures?** By default, every operation runs independently. The returned dictionary contains a `BulkTaskResult` for each input - some complete successfully, others report a failure. One failed item does not cancel the rest. Set `throwOnError` to `true` if you prefer an exception that collects all failures after all tasks complete. **What is the default level of parallelism?** The default is set by `runtime.Bulk.DefaultMaxDegreeOfParallelism`, which defaults to `Environment.ProcessorCount`. Each operation runs on its own task, and the Bulk API limits concurrent execution to this value to avoid overwhelming the database. **Do I need to change my application code to handle bulk results differently?** The Bulk API returns the same data types as single-instance methods, organized per input identifier. You iterate over the results dictionary to check success or failure for each item. A single method call replaces what would otherwise be a manual loop with parallel execution and error handling code. ## See also --- # Work Calendar URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/work-calendar/ # Work Calendar A timer that expires after "2 days" does not always mean 48 hours. In business workflows, a 2-day approval deadline means 2 working days - excluding weekends, holidays, and after-hours time. Without a calendar, every timer with a business deadline needs custom code to skip non-working time. A Work Calendar defines which days and hours count as working time. Timers that use calendar-aware interval units - `whours`, `wdays`, `wminutes` - skip non-working time automatically. You define the calendar once, and every timer in every workflow respects it. With a Work Calendar, you can set a timer to "4 whours" and know it fires after 4 hours of actual working time, not 4 wall-clock hours. For example, a "2 wdays" timer started at 17:00 on Friday fires at 10:00 on Tuesday (assuming 9-to-6, Monday-to-Friday), skipping the weekend entirely. ## What it is A Work Calendar defines when work happens and when it does not. You specify the start of the workday, the number of working hours per day, which days of the week are weekends, and which specific dates are holidays. The timer subsystem then counts only working time when a calendar-aware interval is used. Think of it as a business-day calendar you hang on the wall: "Monday to Friday, 9 AM to 6 PM, closed on public holidays." Any timer set in working hours, working days, or working months follows this calendar - non-working time simply does not count toward the deadline. When a timer interval is specified in calendar-aware units, the runtime computes the elapsed time using only the working periods defined in the calendar. Regular time units (`hours`, `days`, `minutes`) always use wall-clock time and ignore the calendar. **For developers:** Implement the `ICalendar` interface from `OptimaJet.Workflow.Core.Runtime.Calendars` or use the built-in `Calendar` class with chainable configuration via extension methods. Register calendars with `runtime.WithCalendars(...)` and set a default with `runtime.SetDefaultCalendar(...)`. The runtime resolves which calendar to use in this order: process instance calendar, then scheme calendar, then runtime default calendar. ```csharp using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.Core.Runtime.Calendars; // Use the built-in Calendar class with chainable setup var calendar = Calendar.Create("StandardBusiness") .SetStartOfWork(new TimeSpan(9, 0, 0)) .SetWorkingHours(new TimeSpan(8, 0, 0)) .SetWeekends(DayOfWeek.Saturday, DayOfWeek.Sunday); // Register with the runtime and set as default runtime.WithCalendars(calendar); runtime.SetDefaultCalendar(calendar.Name); ``` ### Calendar-aware interval units The following interval units trigger calendar-aware calculation. Any interval value that uses one of these units is evaluated against the assigned Work Calendar. Table: Calendar-aware interval units | Unit | Aliases | Example | Meaning | |-----------------|-----------------------------|---------------|----------------------------------| | Working hours | `whours`, `whour`, `wh` | `4 whours` | 4 hours of working time | | Working days | `wdays`, `wday`, `wd` | `2 wdays` | 2 full working days | | Working minutes | `wminutes`, `wminute`, `wm` | `30 wminutes` | 30 minutes of working time | | Working months | `wmonths`, `wmonth`, `wmm` | `1 wmonth` | 1 calendar month of working days | | Working years | `wyears`, `wyear`, `wy` | `1 wyear` | 1 calendar year of working days | Calendar-aware and regular time units cannot be combined in a single interval value. The runtime throws an `ArgumentException` if they are mixed. ### Calendar resolution The runtime resolves which Work Calendar to use in this order: **process instance calendar > scheme calendar > runtime default calendar**. If a process instance has a `CalendarName` set, that calendar is used. If not, the scheme's `CalendarName` is checked. If neither is set, the runtime's `DefaultCalendarName` applies. This lets you use different calendars per tenant, per workflow type, or per individual instance. ### Custom calendars For advanced cases, implement the `ICalendar` interface directly. The interface requires you to define working days, working hours, and methods for finding workday boundaries and calculating working time ranges. You can also define per-year holiday overrides using the `Year` class, specifying which dates are holidays or extra workdays for a specific year. ## Why it matters A Work Calendar delivers these outcomes: - **Accurate business-time deadlines** - SLA timers, approval escalations, and contract expiry dates count only working time. A "3 wdays" approval timer always means 3 business days, regardless of weekends or holidays. - **No custom date arithmetic** - Without a calendar, every timer-based workflow needs custom code to skip weekends and holidays. The calendar eliminates this code entirely. - **Per-tenant or per-process calendars** - Different business units or tenants can use different calendars. An APAC team can use one calendar while an EMEA team uses another, even in the same runtime. - **Holiday-aware scheduling** - Public holidays defined in `Year` objects are automatically excluded. No need to update scheduling logic when holiday dates change each year. ## Who it is for **Evaluator (CEO, CTO, PM):** A Work Calendar ensures that time-sensitive workflows match how your business actually operates - weekends and holidays are skipped, SLAs are measured in business time, and no custom code is needed to handle regional differences. **Developer:** Define a calendar once with the built-in `Calendar` class, register it with `runtime.WithCalendars(...)`, and use `w` -prefixed timer units in your scheme. The timer subsystem handles the rest. **Enterprise architect:** Calendar resolution flows from instance to scheme to runtime default, supporting multi-tenant deployments with different business calendars. Year-level holiday overrides let you plan known non-working days in advance. ## When to use it Use a Work Calendar whenever a timer interval must align with business availability: - **Approval escalation timers** - An approval that auto-escalates after 4 working hours should not escalate on Saturday afternoon. The calendar skips non-working time automatically. - **Service-level agreements** - An SLA measured in business days that starts at 16:00 on a Friday should fire on Tuesday, not Sunday. A Work Calendar makes this the default behavior. - **Scheduled notifications** - A notification that should only fire during business hours needs no custom time-of-day checks. Calendar-aware timers handle the filtering. - **Multi-region deployments** - Each region gets its own calendar with different holidays and working hours. The runtime default calendar applies when neither instance nor scheme specifies one. ## How it compares The table below compares using a Work Calendar with the alternative of custom date arithmetic. Table: Comparison of work calendar approaches | Approach | What it requires | Result | |-------------------------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------| | Custom date arithmetic | Write skip-weekend logic, maintain holiday tables, update for each new workflow | Fragile, duplicated code that must be maintained across every timer-based workflow | | Workflow Engine Work Calendar | Define a calendar once, register it with the runtime, use `whours`/`wdays` units in timer intervals | Automatic exclusion of non-working time for every timer, configurable per process, scheme, or runtime | The calendar approach removes the code and maintenance burden of date arithmetic. Every timer in every workflow automatically respects the same calendar rules. ## Frequently asked questions **What timer interval units are calendar-aware?** The calendar-aware units are `whours`, `wdays`, `wminutes`, `wmonths`, and `wyears` (with short aliases like `wh`, `wd`, `wm`, `wmm`, `wy`). Regular `hours`, `days`, `minutes`, `months`, and `years` use wall-clock time and ignore the calendar. **How do I assign different calendars to different process instances?** Set the `CalendarName` parameter on the process instance. The runtime resolves the calendar in this order: process instance calendar first, then scheme calendar, then runtime default calendar. This lets you assign calendars per tenant, per workflow type, or per individual instance. **What happens if I mix calendar-aware and regular time units in one interval?** The system throws an `ArgumentException`. Calendar-aware and regular time units cannot be combined in a single interval value. Use either calendar units (prefixed with `w`) or regular units, not both. **Do I need special licensing for the Work Calendar feature?** No. The `ICalendar` interface and the built-in `Calendar` class are part of the core runtime and available in all editions. ## See also --- # Pre-Execution (Simulation) URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/pre-execution-simulation/ # Pre-Execution (Simulation) A user clicks "Approve" on a purchase order. The workflow might route to the direct manager, skip to the department head, or trigger an escalation - depending on the amount, the user's role, and the time of year. Without running the actual command, there is no way to show the user what will happen next. Pre-Execution simulates the workflow path from any activity without modifying the process instance's persisted state. The runtime evaluates transition conditions and auto-transitions, but it executes actions only from each activity's `PreExecutionImplementation` collection. Regular execution uses the separate `Implementation` collection; neither collection invokes the other automatically. With Pre-Execution, you can build "Are you sure? This will route to Director Approval." prompts that show the predicted path before the user confirms. For example, simulating an invoice approval shows the user which approval chain their action will trigger, with no history records written and no state changed. ## What it is Think of it as a dress rehearsal for a workflow transition. The process instance is loaded, its parameters are populated, and transition conditions are evaluated. Activities run their pre-execution implementations, and Workflow Engine does not persist the simulated process changes. You can start a simulation from three entry points: - **From the current activity** - simulates what happens if the process continues from where it is now. - **From the initial activity** - simulates the process from the very start. - **From any named activity** - simulates the path from a specific activity by name. The result is a list of `ActivityDto` objects showing which activities the process would visit and which transitions connect them. **For developers:** Call `PreExecuteAsync(processId, fromActivityName, ignoreCurrentStateCheck)` on `WorkflowRuntime`. The runtime fills system parameters, sets `processInstance.IsPreExecution = true`, and runs the simulation without persisting process state. The return type is `List`, where each entry has an `Activity` name and a `Transitions` collection. ```csharp // Simulate from the current activity List result = await runtime .PreExecuteFromCurrentActivityAsync(processId); foreach (var activity in result) { Console.WriteLine($"Would visit: {activity.Activity}"); } ``` ### Detecting pre-execution in action code An activity has two separate action collections: `Implementation` for regular execution and `PreExecutionImplementation` for simulation. An action runs during simulation only when it is included in `PreExecutionImplementation`; actions from `Implementation` do not run automatically. If the same action is deliberately included in both collections, it can check `IsPreExecution` on the `ProcessInstance` to perform only the simulation-specific part or use different behavior. Actions used only for regular execution do not need the flag to keep them out of simulation. ```csharp if (processInstance.IsPreExecution) { // Run only the simulation-specific part of this shared action return; } // Run the regular-execution part of the shared action await _notificationService.SendAsync(/* ... */); ``` Condition evaluation also handles pre-execution: conditions with a `ResultOnPreExecution` value use that value during simulation instead of evaluating the full expression. This lets you define expected outcomes for pre-execution separately from normal execution. ### What does not happen during pre-execution No simulated process state is persisted by Workflow Engine. Specifically: - No history records are written. - No parameter changes are saved. - No timer registrations are created. - No subprocess instances are started. The persisted process instance is left unchanged. Actions in `PreExecutionImplementation` can still interact with external systems, so configure those actions for simulation intentionally. ## Why it matters Pre-Execution delivers these outcomes: - **Confidence before committing** - Users see the predicted workflow path and can confirm, cancel, or choose a different action. This reduces errors from incorrect assumptions about what a command will do. - **Non-persistent what-if analysis** - Administrators can simulate scheme changes on in-flight instances to verify the new path without altering the persisted process instance. - **Explicit simulation actions** - Each activity has a separate pre-execution implementation, so only actions selected for simulation run. When one action is reused in both implementations, `IsPreExecution` lets it select partial or different behavior. - **Better user experience** - Approval UIs can show the predicted approval chain, escalation path, or next activity before the user commits, making the workflow transparent. ## Who it is for **Evaluator (CEO, CTO, PM):** Pre-Execution enables confirmation dialogs and what-if analysis that make workflows transparent and predictable. Users trust the system more when they can preview the result before acting. **Developer:** Call `PreExecuteAsync`, `PreExecuteFromCurrentActivityAsync`, or `PreExecuteFromInitialActivityAsync` on `WorkflowRuntime`. Place simulation actions in `PreExecutionImplementation`. If the same action is also in `Implementation`, use `processInstance.IsPreExecution` to select its simulation-specific behavior. The API returns a `List` describing the predicted path. **Enterprise architect:** Pre-Execution does not persist simulated process state. Actions configured in `PreExecutionImplementation` still run, so any external behavior must be appropriate for simulation. ## When to use it Use Pre-Execution when you need to show the user what would happen next without actually moving the process forward: - **Confirmation dialogs** - An approval UI displays "This request will route to Director Approval" with the predicted path before the user confirms. The user clicks "Approve" and sees the expected next step before committing. - **What-if analysis** - An administrator simulates what a scheme change would do to an in-flight instance, verifying the new path is correct before deploying the update. - **Validation checks** - Verify that a process instance can reach a desired state before setting the state directly. Pre-execution confirms the path exists. - **Multi-step command preview** - Before executing a complex command with multiple conditional transitions, show the user which branch the workflow will follow based on current parameters. Do not use pre-execution as a replacement for `ExecuteCommandAsync` in normal workflow execution. Pre-execution does not persist the simulated process state. Use `ExecuteCommandAsync` when the user confirms the action. ## How it compares The table below compares Workflow Engine's pre-execution approach with the alternative of manually evaluating transition conditions. Table: Comparison of pre-execution approaches | Approach | What it requires | Result | |-------------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| | Manual condition evaluation | Duplicate transition logic in the UI layer, keep it in sync with the scheme, handle every branch manually | Fragile, hard to maintain - UI logic drifts from scheme logic over time | | Workflow Engine Pre-Execution | Call `PreExecuteAsync` with the process ID and activity name | Workflow Engine evaluates the simulated path using transition conditions and each activity's pre-execution implementation | The simulation uses the runtime's transition selection and condition handling, while activity actions come from `PreExecutionImplementation` rather than `Implementation`. Configure simulation actions so the predicted path reflects the behavior you want to preview. ## Frequently asked questions **What happens during pre-execution simulation?** The runtime loads the process instance, fills its system parameters, sets `IsPreExecution = true` on the process instance, evaluates outgoing transitions with their conditions, executes activity pre-execution implementations, and returns the predicted path as a list of `ActivityDto`. No state is persisted - no history records, no parameter changes, no timer registrations. **When should I check the IsPreExecution flag in my activity code?** Check `IsPreExecution` when the same action is deliberately included in both `Implementation` and `PreExecutionImplementation` and needs partial or different behavior during simulation. Actions present only in `Implementation` are not invoked by pre-execution. **Can pre-execution trigger side effects like sending emails?** Pre-Execution can trigger side effects from actions explicitly placed in `PreExecutionImplementation`. Workflow Engine does not invoke regular `Implementation` actions automatically, but it also does not suppress external effects from simulation actions. Use `IsPreExecution` when sharing an action between both collections. **Do I need a specific license for Pre-Execution?** No. Pre-Execution is part of the core runtime and available in all editions. ## See also --- # Process Logs URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/process-logs/ # Process Logs Compliance regulations such as HIPAA, SOX, and GDPR require a record of who moved a process from one state to another and when. Without built-in logging, every workflow system needs custom code to capture audit data - and auditors need to trust that the data is complete. Workflow Engine records every state change on a process instance automatically, creating a permanent audit trail of every transition, timer firing, and command execution. Each entry captures the source and destination activity, the timestamp, the actor identity, and the trigger that caused the change. With Process Logs, you get a complete, queryable history of every process instance from start to completion - with no custom logging code. For example, after a purchase order is approved, the log shows who approved it, when, from which activity, and whether a timer or user command triggered the transition. ## What it is Every time a process instance changes activity - through a command, a timer firing, setting state directly, or any other mechanism that triggers a transition - the runtime writes a history record to the persistence provider. Think of it as a black box for each process instance: every state change is recorded automatically, and the sequence of entries reconstructs the exact path the instance followed through the scheme. The history is stored separately from the active instance state. You can query the log for a completed instance, a running instance, or even an instance that has been deleted from the active store - as long as the history table still has the data. **For developers:** The `ProcessHistoryItem` class captures each transition. Query it with `GetProcessHistoryAsync(processId, paging)` on `WorkflowRuntime`, which returns a `List` ordered by timestamp. ```csharp // Get the full history for a process instance List history = await runtime .GetProcessHistoryAsync(processId); foreach (var entry in history) { Console.WriteLine( $"[{entry.TransitionTime:u}] {entry.FromActivityName} " + $"-> {entry.ToActivityName} by {entry.ActorIdentityId}"); } ``` ### What each log entry contains `ProcessHistoryItem` captures the following data for every state change: Table: Process history item fields | Field | Description | |------------------------|-----------------------------------------------------------| | `ProcessId` | The process instance ID | | `FromActivityName` | The source activity name before the transition | | `FromStateName` | The source state name (if set on the activity) | | `ToActivityName` | The destination activity name after the transition | | `ToStateName` | The destination state name (if set on the activity) | | `TransitionTime` | UTC timestamp when the transition executed | | `StartTransitionTime` | UTC timestamp when the transition started executing | | `TransitionDuration` | Duration of the transition execution in milliseconds | | `TransitionClassifier` | Classifier of the transition direction (direct, or other) | | `TriggerName` | The command or timer name that triggered the transition | | `ActorIdentityId` | The user ID on whose behalf the command was executed | | `ExecutorIdentityId` | The user ID who executed the command | | `ActorName` | The user name on whose behalf the command was executed | | `ExecutorName` | The user name who executed the command | | `IsFinalised` | Whether this transition finalized the process | ### Separate runtime logging In addition to the transition history, Workflow Engine has a separate runtime log system via `IProcessLogProvider`. This captures runtime-level events - messages, errors, exceptions - as `ProcessLogEntry` objects. The default implementation is `MemoryProcessLogger(100, 100)`, which keeps the last 100 entries per process for up to 100 processes in memory. ```csharp // Configure a custom process log provider runtime.WithProcessLogger(new MyDatabaseProcessLogger()); ``` The runtime log is distinct from the transition history. Transition history records state changes; the process log records operational events, exceptions, and debugging information during execution. ### Paging support For instances with many transitions, use the `Paging` parameter with `GetProcessHistoryAsync` to paginate results: ```csharp var firstPage = await runtime.GetProcessHistoryAsync( processId, new Paging(1, 50)); var totalCount = await runtime.GetProcessHistoryCountAsync(processId); ``` ## Why it matters Process Logs deliver these outcomes: - **Audit-ready compliance** - Every state change is recorded by default. The `ProcessHistoryItem` contains actor identity, timestamp, and the transition taken - exactly what compliance frameworks require. No custom logging code needed. - **Faster debugging** - When a process takes an unexpected path, the log shows every transition the instance followed, the timestamps, and the actor that triggered each step. This is faster and more reliable than reproducing the sequence by re-running the process. - **Complete path reconstruction** - The sequence of log entries, from the initial activity through every transition to completion (or current activity), reconstructs the exact path the instance took through the scheme. - **Separate operational logging** - The runtime log via `IProcessLogProvider` captures exceptions, errors, and debug messages during execution, separate from the transition history. You can query recent logs, filter by time, or paginate results. ## Who it is for **Evaluator (CEO, CTO, PM):** Built-in process logs satisfy audit requirements without custom development. The transition history is automatically recorded, queryable, and contains the actor identity and timestamp data that compliance audits require. **Developer:** Query `ProcessHistoryItem` records with `GetProcessHistoryAsync` on `WorkflowRuntime`. For runtime operational logging, configure an `IProcessLogProvider` with `WithProcessLogger()` and use `ProcessLogger.ReadAllAsync`, `ReadLastAsync`, or `ReadEarlyAsync` to query entries. **Enterprise architect:** History records are stored in the same persistence provider as process instances, so they respect the same isolation and backup policies. The `Paging` parameter on `GetProcessHistoryAsync` supports large-scale queries without loading the full history into memory. ## When to use it Process logs are valuable in any workflow that needs an audit trail or debugging capability: - **Compliance-required audit trails** - Regulated workflows (healthcare approvals, financial transactions, document sign-offs) need a permanent record of every state change. The `ProcessHistoryItem` captures exactly the data auditors look for. - **Debugging unexpected workflow paths** - When a process takes an unexpected route, the log shows every transition, the conditions that led to it, and the actor that triggered each step. No need to reproduce the scenario. - **User action tracking** - The `ActorIdentityId` and `ExecutorIdentityId` fields capture who performed each action and on whose behalf, making it possible to trace user activity across the workflow. - **Performance analysis** - The `StartTransitionTime` and `TransitionDuration` fields capture how long each transition took to execute, helping identify slow activities or bottlenecks. ## How it compares The following table compares Workflow Engine's automatic process logging with the alternative of custom audit logging. Table: Comparison of process logging approaches | Approach | What it requires | Result | |------------------------------|----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| | Custom audit logging | Write logging code in every action, maintain a separate audit table, ensure it captures all transitions | Fragile, easy to miss transitions, hard to keep in sync with the scheme | | Workflow Engine Process Logs | Transition history is recorded automatically for every state change; query with `GetProcessHistoryAsync` | Complete, consistent audit trail - no custom code, no missed transitions | The `ProcessHistoryItem` captures every state change by default, including the transition direction classifier, trigger name (command or timer), and execution duration - data a custom logging implementation would need to reconstruct manually. ## Frequently asked questions **Does Workflow Engine capture process logs automatically?** Yes. Every transition - whether triggered by a command, timer, direct state change, or subprocess completion - writes a `ProcessHistoryItem` to the persistence provider. No configuration is needed to enable it. **What data does each process log entry capture?** Each `ProcessHistoryItem` records the source and destination activity names, the source and destination state names, the UTC transition time, the start transition time and duration, the transition classifier, the trigger name (command or timer), the actor identity, the executor identity, and whether the transition finalized the process. **Is the process log stored with the same persistence provider?** Yes. The transition history is stored in the same persistence provider (MSSQL, PostgreSQL, MySQL, etc.) as the process instances. It is a separate table indexed by process instance ID. **Is there a separate runtime log for debugging?** Yes. The runtime has an `IProcessLogProvider` interface (implemented by `MemoryProcessLogger` by default) that captures operational messages, exceptions, and debugging events during execution. This is separate from the `ProcessHistoryItem` transition history. **Can I customize what data gets logged during a state change?** The `ProcessHistoryItem` fields are fixed and automatically populated. For custom operational data, use the `IProcessLogProvider` runtime log, which accepts `ProcessLogEntry` objects with a free-form `Properties` field (`JObject`) and `Message` string. ## See also --- # Workflow as Code URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/workflow-as-code/ # Workflow as Code The Programmatic Scheme Builder is a fluent C# API - `ProcessDefinitionBuilder` - that lets you construct workflow schemes in code instead of using the visual Designer. Available since Workflow Engine 13.3.0, it is useful when you need to generate schemes dynamically, enforce naming conventions programmatically, or build workflows in environments without a browser-based Designer. `ProcessDefinitionBuilder` produces a `ProcessDefinition` object through chainable factory methods - `CreateActivity`, `CreateTransition`, `CreateCommand`, `CreateTimer`, `CreateActor`, and others. After building, extract the result from `builder.ProcessDefinition` and register it with the runtime. Workflow Engine also provides a build step pipeline - a chain of transformations that run every time a scheme is loaded into memory, regardless of how the scheme was authored. Whether the scheme came from the visual Designer, an XML file, or `ProcessDefinitionBuilder`, the same build steps apply: they can add activities, enforce naming conventions, inject transitions, or reject invalid schemes before they are cached. Build steps are available from **Workflow Engine 4.0** and later. ## What it is Think of `ProcessDefinitionBuilder` as a code-based alternative to drawing a scheme in the Designer. Instead of dragging activities onto a canvas and connecting them with transitions, you call methods that declare the same structure: "Create this activity, mark it as initial, connect it to that activity with a transition triggered by that command." The builder has three static factory methods: - `ProcessDefinitionBuilder.Create(name)` - creates a new builder for a fresh scheme. - `ProcessDefinitionBuilder.WithProcessDefinition(pd)` - wraps an existing `ProcessDefinition` for reading. - `ProcessDefinitionBuilder.WithMutableProcessDefinition(pd)` - wraps an existing `ProcessDefinition` for modification. Each `Create*` method returns a sub-builder with chainable configuration methods for that element. You set properties, attach actions, add conditions, and capture references for later use - all without touching XML. **For developers:** Use `CreateActivity(name)` with `.Initial()` or `.Final()` to mark start and end states. Capture references with `.Ref(out ActivityDefinition var)` and `.Ref(out CommandDefinition var)`, then pass them to `CreateTransition(name, from, to)` and `.TriggeredByCommand(cmd)`. Extract the final definition with `builder.ProcessDefinition` and register it via `runtime.Builder.SaveProcessSchemeAsync(schemeCode, pd)`. To add a custom build step, extend `BuildStep` and override `Name` and `ExecuteAsync`. Register it with `workflowBuilder.AddBuildStep(order, position, step)` before passing the builder to `WorkflowRuntime`. The step receives the `ProcessDefinition`, mutates a clone, and returns `BuildStepResult.Success(modified)` or `BuildStepResult.Fail(error)`. The same pipeline handles built-in features like scheme inlining and activity timeouts. ```csharp using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Model.Builder; using OptimaJet.Workflow.Core.Runtime; var builder = ProcessDefinitionBuilder.Create("ApprovalScheme"); // Create activities and capture references for transitions builder.CreateActivity("Draft").Initial() .Ref(out ActivityDefinition draft); builder.CreateActivity("ManagerApproval") .Ref(out ActivityDefinition managerApproval); builder.CreateActivity("DirectorApproval") .Ref(out ActivityDefinition directorApproval); builder.CreateActivity("Approved").Final() .Ref(out ActivityDefinition approved); builder.CreateActivity("Rejected").Final() .Ref(out ActivityDefinition rejected); // Create commands and capture references builder.CreateCommand("Submit") .Ref(out CommandDefinition submitCmd); builder.CreateCommand("Approve") .Ref(out CommandDefinition approveCmd); builder.CreateCommand("Reject") .Ref(out CommandDefinition rejectCmd); // Create transitions with activity and command references builder.CreateTransition("toManagerApproval", draft, managerApproval) .TriggeredByCommand(submitCmd); builder.CreateTransition("toDirectorApproval", managerApproval, directorApproval) .TriggeredByCommand(approveCmd); builder.CreateTransition("toApproved", directorApproval, approved) .TriggeredByCommand(approveCmd); builder.CreateTransition("toRejected", managerApproval, rejected) .TriggeredByCommand(rejectCmd); // Build and register the definition var definition = builder.ProcessDefinition; var (success, errors, failedStep) = await runtime.Builder.SaveProcessSchemeAsync("ApprovalScheme", definition); ``` ### What the builder supports The builder covers the same elements as the Designer: - **Activities** - initial, intermediate, and final, with state names, position on the canvas, and auto-scheme-update flags. - **Transitions** - direct and reverse classifiers, command triggers, timer triggers, auto triggers, and conditional expressions. - **Commands** - named triggers that appear in the UI for users to execute. - **Timers** - interval, date, and expression-based timers for scheduled transitions. - **Actors and rules** - define who can execute which command through actor definitions and restriction rules. - **Actions** - attach code actions to activities and transitions with parameters. - **Conditions** - code conditions and expression-based conditions for branching. - **Localization** - per-language state names via `CreateOrUpdateLocalizationForState`. - **Annotations** - key-value metadata on activities and transitions. - **Inline schemes** - embed another scheme as a sub-process via `CreateInlineActivity`. ### Registering with the runtime After building, call `runtime.Builder.SaveProcessSchemeAsync(schemeCode, definition)` to persist the scheme to the database. The runtime can then create process instances from it: ```csharp var definition = builder.ProcessDefinition; var (success, errors, failedStep) = await runtime.Builder.SaveProcessSchemeAsync("ApprovalScheme", definition); if (success) { await runtime.CreateInstanceAsync("ApprovalScheme", Guid.NewGuid()); } ``` ## Why it matters Workflow as Code delivers these outcomes: - **Compile-time safety** - The builder catches missing activities, dangling transitions, and invalid configurations at build time, not when the runtime tries to execute a malformed scheme. - **Dynamic scheme generation** - Approval chains, escalation paths, and branching rules can be read from a database or API response and turned into a valid scheme on the fly, without storing XML templates per variation. - **Version-controlled schemes** - Scheme definitions live in source code alongside the rest of your application, subject to the same code review, testing, and CI/CD pipeline as any other source file. - **No Designer dependency** - Schemes can be created in headless environments - CI/CD pipelines, automated tests, or server-side code - where no browser-based Designer is available. - **Enforce conventions across all schemes** - Register a build step that validates naming rules, injects audit activities, or rejects non-compliant schemes at load time. The same rules apply to every scheme, whether it was designed visually, authored in XML, or generated with `ProcessDefinitionBuilder`. ## Who it is for **Evaluator (CEO, CTO, PM):** The Programmatic Scheme Builder lets developers generate and maintain workflow definitions in code, reducing the operational cost of managing per-tenant scheme variations and making scheme changes part of the standard deployment pipeline. **Developer:** Call `ProcessDefinitionBuilder.Create(name)` and chain `CreateActivity`, `CreateTransition`, `CreateCommand`, and `CreateTimer` to define the full scheme. Capture references with `.Ref(out ...)` to pass `ActivityDefinition` and `CommandDefinition` objects between builder methods. Extract the result with `builder.ProcessDefinition` and register it via `runtime.Builder.SaveProcessSchemeAsync(schemeCode, pd)`. **Enterprise architect:** The builder enables scheme-as-code patterns where workflow definitions are generated, validated, and deployed through the same CI/CD pipeline as application code. Existing schemes can be modified through `WithMutableProcessDefinition` without rebuilding from scratch. ## When to use it Use the Programmatic Scheme Builder when the scheme structure must be determined at application startup based on configuration, user input, or external data. A typical scenario is migrating an existing business rules engine into Workflow Engine: the rules are already defined in a database table or configuration file (who approves what, under which conditions, with which deadlines), and the builder reads those rules and constructs a matching scheme - without manually translating each rule into an XML scheme. Use build steps when the same structural transformation must apply to every scheme in your deployment, regardless of how it was authored - Designer, XML, or programmatic. Typical examples: - **Mandatory compliance activities** - Every scheme must include an audit trail activity. A build step injects it at load time, so no one can forget to add it - whether they designed the scheme in the visual Designer or generated it in code. - **Naming convention enforcement** - A build step validates that initial activities follow a naming standard like `Start*` and rejects non-compliant schemes with a clear error before they are cached. - **Cross-cutting transitions** - Every scheme needs a timeout transition that escalates stalled tasks. A build step reads the timeout configuration from the activity properties and generates the required timer and transition automatically. Do not use the builder when the scheme is authored by a business analyst or end user - those teams work in the visual Designer. The builder is a developer tool for scenarios where the scheme is generated from code. ## How it compares The table below compares using the Programmatic Scheme Builder with the alternative of authoring schemes in the visual Designer. Table: Comparison of scheme authoring approaches | Approach | What it requires | Result | |---------------------------------------------|-------------------------------------------------------------------------|-------------------------------------------------------------------------------------------| | Visual Designer | A browser-based UI, manual drag-and-drop, exporting XML per scheme | Best for human-authored workflows; requires manual effort per scheme variation | | Workflow Engine Programmatic Scheme Builder | C# code using `ProcessDefinitionBuilder` with chainable factory methods | Schemes generated, validated, and deployed as code - repeatable, testable, CI/CD-friendly | Both approaches produce the same `ProcessDefinition` format and support the same runtime features. The choice depends on who authors the scheme and whether the scheme structure is fixed or dynamic. ## Frequently asked questions **When should I use the Programmatic Scheme Builder instead of the visual Designer?** Use the builder when schemes are generated from code - dynamic approval chains, conventions enforced at build time, or rules migrated from an existing system. Use the Designer when a human authors the scheme. **Does the builder support all Designer features?** Yes. `ProcessDefinitionBuilder` covers activities, transitions, commands, timers, actors, conditions, actions, localization, annotations, and inline subprocesses. Each element has a dedicated `Create*` method and chainable configuration. **How do I register a builder-created scheme with the runtime?** Extract the `ProcessDefinition` with `builder.ProcessDefinition` and pass it to `runtime.Builder.SaveProcessSchemeAsync(schemeCode, definition)`. The method returns validation errors. The scheme is persisted to the same database tables as Designer-authored schemes. **What are build steps?** Build steps are transformations that run every time a scheme is loaded into memory, regardless of how the scheme was created - visual Designer, XML file, or `ProcessDefinitionBuilder`. They can add activities, inject transitions, enforce naming conventions, or reject invalid schemes. The same pipeline powers built-in features like scheme inlining and activity timeouts. Custom build steps are registered through `IWorkflowBuilder.AddBuildStep()` and apply uniformly to every scheme in your deployment. **What API changes came with the Programmatic Scheme Builder?** The builder was introduced in Workflow Engine 13.3.0. Earlier versions required XML authoring through the Designer API or manual XML construction. ## See also --- # Database Versioning URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/database-versioning/ # Database Versioning Every Workflow Engine upgrade can bring new tables, columns, or indexes to the persistence schema. Without an automated migration, each upgrade means finding and running the right SQL script for your provider - and missing a step breaks the runtime. Since Workflow Engine 13.0.0, the runtime provides automatic database schema migration through the `RunMigrations()` fluent method. Calling `RunMigrations()` after configuring your persistence provider and before calling `StartAsync()` creates or upgrades the required tables to match the runtime version. With `RunMigrations()`, upgrading Workflow Engine is a single line change in your startup code. For example, moving from version 13.0.0 to 14.0.0 requires only updating the NuGet package reference - the migration runs automatically on the next startup. ## What it is `RunMigrations()` is an extension method on `WorkflowRuntime` that applies the database schema changes needed for the current runtime version. When you configure a persistence provider for SQL Server, PostgreSQL, MySQL, Oracle, or SQLite, the runtime knows which tables, indexes, and columns each provider requires. Internally, `RunMigrations()` delegates to FluentMigrator, which tracks applied migrations in a `VersionInfo` table and applies only the missing ones. The migration is idempotent - running it multiple times on an up-to-date database produces no errors. During an upgrade from an older Workflow Engine version, FluentMigrator detects which migration steps have already been applied and runs only the new ones. **For developers:** Call `RunMigrations()` after `WithPersistenceProvider()` and before `StartAsync()` in your startup sequence. The method returns the `WorkflowRuntime` instance so you can chain it: ```csharp var runtime = new WorkflowRuntime() .WithPersistenceProvider(new MSSQLProvider(connectionString)) .RunMigrations(); await runtime.StartAsync(); ``` ### Supported providers `RunMigrations()` works with the following persistence providers. Each has its own set of migration scripts with provider-specific SQL syntax. Table: Persistence provider migration support | Provider | Migration support | |------------|------------------------------------------------------------------------| | SQL Server | Yes - uses `IF NOT EXISTS (...)` guard clauses | | PostgreSQL | Yes - uses `CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS` | | MySQL | Yes | | Oracle | Yes | | SQLite | Yes | | MongoDB | No - schema is managed differently | ### Manual SQL scripts For teams that prefer explicit control, each Workflow Engine NuGet package ships migration scripts for every version gap. These are embedded SQL files in the provider assembly, versioned and ordered. You can extract and review them before applying, use them in a DBA-managed maintenance window, or integrate them into an existing database change management process. ## Why it matters Database Versioning delivers these outcomes: - **Zero-mistake upgrades** - The runtime applies exactly the schema changes it needs. No risk of forgetting a migration step, running scripts in the wrong order, or using the wrong provider variant. - **Single-step setup** - Initial deployment creates all required tables automatically. No need to hunt for the correct `CREATE TABLE` script for your provider. - **Idempotent by design** - Calling `RunMigrations()` on every startup is safe. If the schema is already current, the method returns immediately with no side effects. - **Manual option for DBAs** - SQL scripts are available in the provider assembly for teams that require explicit review and approval before schema changes. ## Who it is for **Evaluator (CEO, CTO, PM):** Database Versioning eliminates schema management from the upgrade process. Upgrading Workflow Engine becomes a NuGet version bump and a restart - no separate migration step, no DBA involvement needed for routine updates. **Developer:** Call `RunMigrations()` after `WithPersistenceProvider()` in the startup chain. The method is safe to call on every startup. For production deployments with strict change control, use the embedded SQL scripts instead. **Enterprise architect:** Migrations use FluentMigrator under the hood with a `VersionInfo` table that tracks applied steps. The same mechanism supports both automatic and manual workflows, and the migration assembly is provider-specific so each database type receives the correct SQL syntax. ## When to use it Use `RunMigrations()` in all environments after configuring the persistence provider. In development and staging, automatic migrations reduce the risk of missing a step when switching branches or testing upgrades. In production, evaluate whether automatic or manual migrations suit your deployment process. Automatic migrations work well when you control the deployment pipeline and can start the application in maintenance mode. Manual migrations give your DBA team a reviewable SQL script that can be version-controlled alongside application changes and tested against a production clone. ## How it compares The table below compares automatic migrations with manual script management. Table: Comparison of database migration approaches | Approach | What it requires | Result | |-----------------------------------|---------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| | Manual SQL scripts | Find the correct script for your provider and version gap, run it in the right order, verify it applied | Error-prone, easy to miss or misorder steps, requires DBA involvement for every upgrade | | Workflow Engine `RunMigrations()` | One line in startup code - `RunMigrations()` after `WithPersistenceProvider()` | Automatic, idempotent schema updates - no missing steps, no ordering errors, safe on every startup | ## Frequently asked questions **When should I run RunMigrations?** Call `RunMigrations()` after configuring the persistence provider and before calling `StartAsync()`. Running it on every application startup is safe because the method is idempotent - it applies only missing migrations and returns immediately if the schema is current. **Can I run migrations manually instead of using RunMigrations?** Yes. Each provider assembly contains embedded SQL scripts ordered by version. You can extract them using a reflection tool or access them through the provider package. Manual migration is the recommended approach for production environments where a DBA must review schema changes before they are applied. **Which persistence providers support RunMigrations?** `RunMigrations()` works with SQL Server, PostgreSQL, MySQL, Oracle, and SQLite. Each provider has its own set of migration scripts with database-specific SQL syntax. MongoDB manages schema differently and does not support `RunMigrations`. **What happens if the database schema is newer than the runtime version?** The runtime does not check for this condition. FluentMigrator applies migrations forward-only; it never reverts. If the database has a schema from a newer runtime version, `RunMigrations()` runs successfully (no new migrations to apply), but the runtime may encounter compatibility issues with its older binary. Ensure the runtime version matches or exceeds the schema version. ## See also --- # Offline API URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/offline-api/ # Offline API Without the Offline API, computing the approval map for a process instance or executing a sequence of commands requires multiple round-trips to the runtime, checking available commands for each step, and handling errors one at a time. For client applications that need to present a complete picture of what can happen next - or execute a chain of commands without re-querying after each one - this adds complexity and latency. The Offline API groups these operations into batch calls. Given a process instance or scheme, it computes the full map of available transitions for each activity. Given a list of commands, it executes them in sequence and returns results for all of them. ## What it is The Offline API is a set of methods on `WorkflowRuntime.OfflineApi` that work with process schemes and commands in bulk. It has two capabilities: - **Approval mapping** - given a process scheme or a running instance, returns a dictionary of all activities and their available transitions. For a running instance, it also marks which activity is the current position and filters transitions by the user's authorization. - **Batch command execution** - given a list of commands (optionally grouped by process ID), executes them in order and returns the result of each one. If one command fails, the remaining commands in that chain are skipped with an error message, preserving the transactional semantics of a command sequence. These methods compute results using the current scheme definition and process state. They do not require a persistent connection to a specific runtime node - hence the name "Offline API." **For developers:** access the API through `runtime.OfflineApi`. Two method groups are available, described below. ### Approval map Use `GetApprovalMapAsync` to get the complete transition structure for a process: ```csharp // Get the approval map by scheme definition var scheme = await runtime.Builder.GetProcessSchemeAsync("OrderApproval"); var mapByScheme = await runtime.OfflineApi.GetApprovalMapAsync(scheme); // Get the approval map for a running instance, filtered for a specific user var processInstance = await runtime.GetProcessInstanceAndFillProcessParametersAsync(processId); var mapForUser = await runtime.OfflineApi.GetApprovalMapAsync(processInstance, "user-42"); // Each entry in the result maps an activity name to its details and transitions foreach (var entry in mapForUser) { var activityName = entry.Key; var activity = entry.Value; Console.WriteLine($"{activity.Activity}: {activity.Transitions.Count} transitions available"); } ``` The map is a `Dictionary`. Each `ActivityDto` contains a list of `TransitionDto` objects describing the transitions available from that activity. For the instance-based overload, transitions are filtered by the user's authorization rules, and `IsCurrent` is set to `true` on the activity where the process currently is. ### Batch command execution Use `ExecuteSomeCommandsAsync` to execute several commands in one call: ```csharp var commands = new List { new Command("Approve", "user-42", DateTime.UtcNow, processId), new Command("Submit", "user-42", DateTime.UtcNow, processId), }; var results = await runtime.OfflineApi.ExecuteSomeCommandsAsync(commands); foreach (var result in results) { Console.WriteLine($"Command '{result.CommandName}': success={result.WasExecuted}"); } ``` The commands are grouped by `ProcessId` internally and executed in order by `DateTime` within each group. If a command fails (for example, the transition is not available), the remaining commands in that process group are skipped with a descriptive error. This prevents a broken command chain from leaving the process in an unexpected state. ## Why it matters The Offline API delivers these outcomes: - **Single-call approval map** - Build a complete picture of what a user can do at each step of a process with one API call. No need to loop through activities querying available commands one by one. - **Atomic command sequences** - Execute a chain of commands with automatic ordering and failure isolation. If one step fails, the remaining steps in that process group are skipped without affecting other groups. - **Scheme-level inspection** - The `GetApprovalMapAsync(ProcessDefinition, ...)` overload works with an in-memory scheme definition, no database or running instance needed. Use it for reporting, analysis, or UI building without loading process data. - **Reduced round-trips** - Batch operations replace multiple individual calls with one request, cutting latency and simplifying client-side error handling. ## Who it is for **Evaluator (CEO, CTO, PM):** The Offline API enables client applications to present the full workflow structure in a single call, reducing UI complexity and improving responsiveness. **Developer:** Call `GetApprovalMapAsync` on `runtime.OfflineApi` to get the full transition map for a scheme or a running instance. Use `ExecuteSomeCommandsAsync` to execute a sequence of commands in one batch. Each result includes the command name, success status, and error message on failure. **Enterprise architect:** The Offline API's scheme-based overload works with parsed definitions, making it suitable for read-only analytics and reporting services that inspect process structure without connecting to a running runtime node. ## When to use it Use the Offline API when you need to work with process structure or commands in a batch-oriented way: - **Displaying an approval flow UI** - a client application shows the user all activities and transitions available in the process. Call `GetApprovalMapAsync` once to build the complete map instead of querying available commands activity by activity. - **Executing a predefined command sequence** - a background job or script needs to advance a process through several known states. Use `ExecuteSomeCommandsAsync` with the full list; the API handles ordering, error isolation, and chain termination. - **Computing process structure offline** - a reporting or analytics tool loads a scheme definition and inspects its transition graph without connecting to a running runtime instance. The scheme-based overload of `GetApprovalMapAsync` works with any loaded `ProcessDefinition`. ## How it compares Below is how the Offline API compares to the alternative of querying available commands one by one. Table: Comparison of offline API approaches | Approach | What it requires | Result | |-------------------------------------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------| | Query available commands one by one | Call `GetAvailableCommandsAsync` for each activity. Loop through transitions manually. Handle errors per command. | Multiple round-trips. Manual error handling. No built-in chain semantics. | | Workflow Engine Offline API | One call to `GetApprovalMapAsync` for the full transition map. One call to `ExecuteSomeCommandsAsync` for batch execution. | Single call for the complete map. Automatic ordering, filtering, and chain termination on failure. | ## Frequently asked questions **What is the Offline API in Workflow Engine?** The Offline API is a set of batch-oriented methods on `WorkflowRuntime.OfflineApi`. It provides two capabilities: computing the approval map (all activities and their available transitions for a scheme or a running instance) and executing multiple commands in sequence with automatic error handling. **Does the Offline API work without a database connection?** No. The Offline API requires access to the persistence store through the runtime, just like any other runtime API call. It processes data in batch rather than requiring a transaction per operation, but it is not a disconnected or network-offline mode. **How does GetApprovalMapAsync handle authorization?** The instance-based overload accepts an `identityId` parameter. It filters transitions by calling the runtime's authorization rule executor, so only transitions the specified user is allowed to execute appear in the result. **What happens when ExecuteSomeCommandsAsync fails mid-sequence?** Commands are grouped by `ProcessId` and sorted by `DateTime`. Within each group, execution stops at the first failure. The failed command and all remaining commands in that group get an error result with the failure message. Commands for other process IDs are unaffected. ## See also --- # Interactive Designer URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/interactive-designer/ # Interactive Designer Without real-time updates, monitoring a running process means polling the database or asking users to refresh the page. By the time the UI catches up, the process may have moved to a different state. The Interactive Designer uses Workflow Engine's Real-Time Tracking Plugin to push state changes to the designer as they happen. When a timer fires, a command executes, or a transition completes, the designer updates the visual representation - highlighting the current activity and showing the new state - without a page reload. ## What it is The Interactive Designer is a combination of a server-side SignalR hub (`WorkflowEngineHub`) and a client-side SignalR connection built into the Visual Designer component. The server tracks process instance state changes and broadcasts them to all connected designer instances. The designer receives the updated state and re-renders the current activity, available transitions, and status indicators. The feature is activated when all three conditions are met: the runtime has the `RealTimeTrackingPlugin` registered (detected through the designer API), a `ProcessId` is available, and the designer receives a `realTimeTrackingUrl` pointing to the SignalR hub. **For developers:** On the server, register the SignalR hub and add the `RealTimeTrackingPlugin` to the runtime. On the client, pass the hub URL to the `WorkflowDesigner` constructor: ```csharp // Server-side: register the hub and plugin app.MapRealTimeTracking("/workflowHub"); runtime.WithPlugin(new RealTimeTrackingPlugin()); ``` ```typescript // Client-side: pass the SignalR hub URL in the designer settings const designer = new WorkflowDesigner({ apiurl: "https://your-server/designer-api", renderTo: "designer-container", realTimeTrackingUrl: "https://your-server/workflowHub", processId: "a1b2c3d4-...", }); ``` ### How it works 1. **SignalR connection** - The designer opens a SignalR connection to the configured hub endpoint. SignalR negotiates the best available transport (WebSocket, SSE, or long polling) automatically. 2. **State change detection** - The Real-Time Tracking Plugin subscribes to process instance state changes. In single-server mode, changes are detected directly. In multi-server mode, a background poller checks the database at a configurable interval (default 2 seconds). 3. **Broadcast** - When a change is detected, the server calls `ProcessChanged` on the SignalR hub, which pushes the updated process instance state to all connected clients. 4. **Rendering** - The designer's `UpdateCurrentState` method parses the updated state and calls `Draw` to re-render the affected elements. ### Multi-server mode In clustered deployments, the Real-Time Tracking Plugin uses a background poller (`MultiServerNotifier`) that checks the persistence provider for changes at a configurable interval. When any node in the cluster advances a process, the poller detects the timestamp change and broadcasts the update through SignalR. No external event bus or coordinator is required. ## When to use it Use the Interactive Designer whenever users need to monitor running process instances without refreshing the page: - **Operations dashboards** where support staff monitor active process instances and their current states. - **Approval workflows** where managers see approval requests arrive and can respond immediately. - **Process monitoring** in production environments where rapid feedback on execution status is critical. - **Demonstration and training** scenarios where showing process execution as it happens improves understanding. The Interactive Designer is for observation and monitoring rather than process design. Use the standard Visual Designer for creating and editing process schemes. ## Frequently asked questions **Does the Interactive Designer require additional server configuration?** Yes. The server must register the SignalR hub with `MapRealTimeTracking("/workflowHub")` and add the `RealTimeTrackingPlugin` to the runtime. The hub endpoint is configurable; the default is `/workflowHub`. SignalR handles transport negotiation automatically, but WebSocket connections may require WebSocket support on your load balancer or reverse proxy. **Does the Interactive Designer work with clustered deployments?** Yes. In multi-server mode, the Real-Time Tracking Plugin polls the shared database at a configurable interval (default 2 seconds) and broadcasts changes through SignalR when a transition is detected. No external event bus is needed. **What happens when the real-time connection drops?** SignalR's client library automatically attempts to reconnect. The designer does not need custom reconnection logic. Once reconnected, the next state change triggers a full update of the process visualization. **Is the Interactive Designer available with all editions?** The Real-Time Tracking Plugin is part of the commercial licensing model. The Workflow Engine Free includes the standard Visual Designer without real-time capabilities. ## See also --- # Workflow Templates URL: https://docs.workflowengine.io/evaluate/workflow-engine-features/workflow-templates/ # Workflow Templates Without scheme composition, every scheme that needs an approval step, an escalation path, or a review gate must define those activities from scratch. Updating the pattern means editing every scheme individually - error-prone and hard to maintain. Scheme inlining is a build-time transformation that replaces an `Inline` activity placeholder in a parent scheme with the complete set of activities, transitions, and parameters from a child scheme. The result is a single expanded ProcessDefinition - no separate instance, no runtime coordination, and no process boundary. With scheme inlining, define a reusable workflow pattern once - for example, a three-step approval - mark it as inlinable, and reference it from any parent scheme. When the pattern changes, update the child scheme and all parent schemes pick up the change on the next scheme load. ## What it is Scheme inlining is like building with prefabricated parts. Instead of designing every step from scratch each time - every approval, every review gate, every escalation path - you define the pattern once as a template, then drop it into any scheme wherever you need it. When the scheme is loaded, the template is expanded in place, and the result is a single seamless process definition. Think of a scheme as a blueprint for a process. Some steps in that blueprint repeat across many processes - for example, a three-step manager approval. Without inlining, you copy those steps into every blueprint manually. With inlining, you create the approval pattern once, mark it as a reusable template, and reference it anywhere. When the approval rules change, you update the template in one place, and every blueprint that uses it picks up the change automatically. ### How it works When a scheme is loaded and compiled, the engine finds every `Inline` activity placeholder, reads the referenced child scheme, and replaces the placeholder with the child's complete set of activities, transitions, and parameters. The child's start activity takes the place of the `Inline` activity in the parent flow, and the child's final activity is connected to whatever comes next. The expanded definition behaves exactly as if the activities were designed in a single scheme - there is no separate process instance, no child lifecycle to monitor, and no merge step. ### In the visual Designer In the Designer, an `Inline` activity is added through the activity context menu. It appears with a dashed border. Double-clicking it opens a properties dialog where you select the inlined scheme code from a dropdown. The **CanBeInlined** flag is toggled through a toolbar button. A scheme must have at least one final activity before it can be marked as inlinable. Parameter mappings between parent and child are configured in the inline activity properties dialog. The dialog contains **Input Parameters** and **Output Parameters** sections where you specify which values cross the boundary. Each mapping has a target parameter name, a purpose (Temporary or Persistence), and an expression. When an inlined scheme is selected, its defined parameters are loaded into the dropdown for mapping. **For developers:** Use `ProcessDefinitionBuilder.CreateInlineActivity(name, schemeCode)` to programmatically add an inline activity. Configure parameter mappings with `.SetInputParameters(...)` and `.SetOutputParameters(...)`. Mark a scheme as inlinable with `builder.Inlined()`. ```csharp using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Model.Builder; // Build the child scheme (the reusable pattern) var child = ProcessDefinitionBuilder.Create("StandardApproval"); child.CreateActivity("Start").Initial() .Ref(out ActivityDefinition start); child.CreateActivity("ManagerApprove").Final() .Ref(out ActivityDefinition managerApprove); child.CreateCommand("Approve") .Ref(out CommandDefinition approveCmd); child.CreateTransition("approve", start, managerApprove) .TriggeredByCommand(approveCmd); child.Inlined(); // Mark as inlinable // Build the parent scheme with an inline reference var parent = ProcessDefinitionBuilder.Create("ExpenseReport"); parent.CreateInlineActivity("ApprovalStep", "StandardApproval"); ``` ## Why it matters Workflow Templates deliver these outcomes: - **Reusable workflow patterns** - Define a standard approval, escalation, or review process once. Reference it from any parent scheme without duplicating activities, transitions, or commands. - **Consistency across schemes** - All parent schemes that inline the same child use the exact same process definition. When the child scheme is updated, all parents pick up the change on the next scheme load. - **No runtime overhead** - Inlining is a build-time transformation. The expanded definition is a single `ProcessDefinition` with no separate process instance, no coordination, and no merge step. - **Parameter isolation** - Input and output mappings control exactly which values cross the scheme boundary. The child scheme can define its own parameters without exposing them to every parent. ## Who it is for **Evaluator (CEO, CTO, PM):** Scheme inlining reduces duplication across workflow definitions. Common approval patterns are defined once and reused, making the process library easier to maintain and audit. **Developer:** Use `ProcessDefinitionBuilder.CreateInlineActivity(name, schemeCode)` in code, or add an `Inline` activity in the Designer and set the `SchemeCode` property. Mark child schemes with `CanBeInlined="true"` in XML or use `builder.Inlined()`. Configure parameter mappings through the `IInlineActivityBuilder` API or directly in the Designer properties dialog. **Enterprise architect:** Inlining is a build-time composition mechanism that produces a single expanded definition. It does not create independent child instances, so it avoids the lifecycle and monitoring complexity of subprocesses. The expanded definition is subject to the same scheme-level limits (activity count, transition count) as a hand-authored scheme. ## When to use it Use scheme inlining when a reusable workflow fragment must be embedded without creating a separate process instance. Inlining is the right choice when the child scheme represents a structural pattern - "Manager Approval", "Three-Level Escalation", "Document Review Gate" - that is logically part of the parent flow, not an independent process. Inlining is also useful for enforcing consistency across schemes. Define the approval pattern once with `CanBeInlined`, then inline it into every scheme that needs that pattern. Updates to the inlined scheme propagate to all parents on the next scheme load - but only if the parent scheme is reloaded and rebuilt. Do not use scheme inlining when the child should run independently with its own state lifecycle. For independent child execution with separate persistence, use subprocesses instead. The two approaches are not mutually exclusive. A single scheme can contain both `Inline` activities (for build-time expansion) and subprocess calls (for runtime child instances). For example, a document review process might inline a standard approval pattern as a fixed template, then launch subprocesses for parallel department sign-offs that each track their own status. ## How it compares The table below compares scheme inlining with the alternatives of defining activities directly in each scheme and using subprocesses. Table: Comparison of workflow template approaches | Approach | What it requires | Result | |-------------------------------------------|---------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------| | Define activities directly in each scheme | Duplicate the same activities, transitions, and commands in every scheme that needs the pattern | Repetitive, inconsistent when patterns change - every scheme must be updated individually | | Subprocess | Create a separate child scheme, spawn a child instance at runtime, merge results on completion | Independent lifecycle with persistence, monitoring, and merge logic - appropriate when the child must run autonomously | | Workflow Engine scheme inlining | Mark the child scheme as inlinable, add an `Inline` activity in the parent, optionally configure parameter mappings | Build-time expansion into a single definition - no runtime overhead, no duplication, consistent across all parents | ## Frequently asked questions **Does scheme inlining require a commercial license?** Yes. Scheme inlining is covered by Workflow Engine's commercial license. See the Editions page for details. **How does scheme inlining differ from a subprocess?** Scheme inlining embeds the child's activities and transitions into the parent at build time - there is no separate process instance. A subprocess spawns a child instance that runs independently and merges back on completion. Inlining is a build-time expansion; subprocess is a runtime relationship between two instances. **How do I set up parameter mapping for an inlined scheme?** Parameter mapping can be configured in two ways. In the Designer UI, open the inline activity properties dialog and use the **Input Parameters** and **Output Parameters** tables - each mapping specifies a target parameter name, purpose (Temporary or Persistence), and an expression. Expressions can reference parent parameters using the `@` prefix. Programmatically, use `IInlineActivityBuilder.SetInputParameters` and `SetOutputParameters` methods with `ParameterMapping` objects. **Can an inlined scheme itself reference other inlinable schemes?** Yes. The inlining engine resolves the full tree of nested references. Circular references are detected and rejected. ## See also --- # Workflow Engine Editions URL: https://docs.workflowengine.io/evaluate/workflow-engine-editions/ # Workflow Engine Editions Workflow Engine ships in seven editions across two product families. All editions are embeddable - you add the `WorkflowEngine.NETCore-Core` NuGet package to your .NET application and run the engine in-process. The edition is determined solely by the license key you provide at runtime. Database providers and optional plugins are distributed as separate NuGet packages. For detailed feature comparisons and pricing, contact the sales team. ## Workflow Engine Base Editions The base editions embed directly into your .NET application. They share the same core API and database providers and differ primarily in usage limits and scaling capabilities. All NEO editions (described below) are also embeddable - they extend the base engine with API and multi-tenancy capabilities. ### Workflow Engine Free Workflow Engine Free is a perpetual license for personal, non-commercial use. It is ideal for evaluating the engine, building a proof of concept, or learning how to model processes visually. Free includes the full visual designer and all core runtime features but is limited to 10 schemas and 4 execution threads. Activities, transitions, and commands are unrestricted. The designer displays Optimajet branding. Free is a great starting point because you can migrate to a commercial edition later without code changes - just replace the license key. ### Workflow Engine Team Workflow Engine Team is a perpetual license for internal commercial use. It is designed for teams building applications to automate company processes such as approvals, document routing, and back-office tasks. Team removes schema count limits and raises thread, command, activity, and transition limits significantly compared to Free. It includes 365 days of free updates and supports single-server deployment. The designer retains Optimajet branding. Choose Team when you need commercial-grade reliability and a clear upgrade path to NEO without rewriting your solution. ### Workflow Engine Complete Workflow Engine Complete is a perpetual license for internal commercial use with no feature restrictions. It removes all usage limits - unlimited schemas, threads, commands, activities, and transitions. Complete adds multi-server clustering for horizontal scalability and removes Optimajet branding from the designer. It includes 365 days of free updates. Choose Complete when you need unlimited usage, horizontal scalability, and the flexibility to build the engine integration layer yourself without a web API, while keeping a path to NEO in the future. ## Workflow Engine NEO The NEO family is the evolution of Workflow Engine for SaaS, multi-tenant, and enterprise product scenarios. Like the base editions, NEO editions are embeddable - they run in-process inside your .NET application. NEO editions add RPC and REST API, full hybrid multi-tenancy, multi-server clustering, and built-in forms integration on top of the core engine. All NEO editions include the visual designer without branding. ### Workflow Engine NEO Workflow Engine NEO is a subscription-based edition for public web applications and SaaS platforms. It includes the full NEO feature set: RPC and REST API, full hybrid multi-tenancy, multi-server clustering, and forms integration. Billed annually. Choose NEO when you are building a public, multi-tenant platform and need everything included under a subscription with the flexibility to choose support levels separately. ### Workflow Engine NEO Business Workflow Engine NEO Business is a perpetual license for internal commercial applications that need NEO API capabilities. It includes the RPC and REST API, multi-server clustering, and forms integration, but does not include full hybrid multi-tenancy. Includes 365 days of free updates. Choose NEO Business when you need a perpetual license for an internal application with NEO features and no recurring fees as you scale. ### Workflow Engine NEO SaaS Workflow Engine NEO SaaS is a perpetual license for public web applications and SaaS platforms. It includes the complete NEO feature set - RPC and REST API, full hybrid multi-tenancy, multi-server clustering, forms integration, Active Directory connector, and BPMN import - all under a single one-time payment. Includes 365 days of free updates. Choose NEO SaaS when you need the full NEO feature set permanently with no recurring fees as you scale. ### Workflow Engine NEO Enterprise Workflow Engine NEO Enterprise is a perpetual license for enterprise platforms, OEM distribution, and white-label deployments. It includes all NEO features plus white-label designer canvas, OEM redistribution rights, the FormEngine drag-and-drop React form builder module, Active Directory connector, BPMN import, and one year of source code access. Includes 365 days of free updates. Choose NEO Enterprise when you need to redistribute the engine inside your product, require white-label branding, and want direct vendor support backed by an SLA. ## Which edition is right for you The license key you pass to `WorkflowRuntime.RegisterLicense()` determines which edition is active. Free requires no key. All other editions require a purchased license key. | Scenario | Recommended Edition | |------------------------------------------------|----------------------------------| | Evaluation, POC, learning, personal projects | Workflow Engine Free | | Internal company process automation | Workflow Engine Team or Complete | | SaaS or multi-tenant platform (subscription) | Workflow Engine NEO | | Internal app with NEO API features (perpetual) | Workflow Engine NEO Business | | SaaS or multi-tenant platform (perpetual) | Workflow Engine NEO SaaS | | OEM, white-label, enterprise redistribution | Workflow Engine NEO Enterprise | You can start with Free and upgrade to any commercial edition by supplying a license key - no code changes required beyond setting the key. ## How to get a license To purchase a license or request a trial key, visit the pricing page. You can also obtain a trial or commercial key directly from the License Key page. For detailed feature comparisons and custom enterprise requirements, contact the sales team at sales@optimajet.com. ## Frequently asked questions **Which edition should I start with?** Start with Workflow Engine Free. It requires no license key and includes all core runtime features. When you are ready for production, purchase a commercial license and pass it to `WorkflowRuntime.RegisterLicense()` - no code changes needed. **What is the difference between Workflow Engine Team and Workflow Engine Complete?** Both are perpetual licenses for internal commercial use. Team has usage limits (8 threads, 100 commands, 100 activities, 500 transitions per schema) and single-server deployment. Complete removes all limits, adds multi-server clustering, and removes Optimajet branding. **What is the difference between Workflow Engine NEO and Workflow Engine NEO SaaS?** NEO is a subscription billed annually. NEO SaaS is a perpetual license with a one-time payment. Both include RPC and REST API, full hybrid multi-tenancy, multi-server clustering, and forms integration. **When should I choose Workflow Engine NEO Enterprise?** Choose NEO Enterprise when you need OEM redistribution rights, white-label designer canvas, and the ability to redistribute the engine inside your product. It includes all features plus source code access. **Can I upgrade from one edition to another?** Yes. All editions use the same core NuGet package `WorkflowEngine.NETCore-Core`. You may also need to add separate packages for database providers and plugins depending on your setup. To upgrade, replace the license key via `WorkflowRuntime.RegisterLicense()` - no code changes required beyond setting the new key. **Where can I get detailed feature comparisons?** Contact the sales team for detailed feature comparisons, pricing, and custom enterprise requirements. ## See also --- # License Key URL: https://docs.workflowengine.io/evaluate/license-key/ # License Key Workflow Engine requires a license key for production use. Without a key, it runs in Workflow Engine Free mode, which limits the number of schemes and threads. A license key is a string provided by OptimaJet after purchase or trial registration. Register it with `WorkflowRuntime` before calling `Start()`. ## Get a trial license key You can obtain a free 30-day trial key at trial.workflowengine.io. No credit card is required. ### Via the web form 1. Open trial.workflowengine.io in your browser. 2. Enter your **company name** and your **business email** (personal email domains such as gmail.com are not accepted). 3. Click **Get Trial Key**. 4. Check your inbox - the license key and setup instructions are sent to the email you provided. The key has the `TRIAL-` prefix and unlocks all features for 30 days. When the trial expires, the runtime falls back to Workflow Engine Free mode with the same caps as an unlicensed installation. ### Via API (for LLM agents and automation) The trial portal exposes a programmatic API for agents and automated workflows. The API reference is available at trial.workflowengine.io/llms.txt. ```json POST https://trial.workflowengine.io/api/trial/llm Content-Type: application/json { "email": "user@company.com", "companyName": "Company Name" } ``` A successful response returns the license key directly and also sends it by email: ```json { "success": true, "message": "Check your email. We've sent the trial license key and setup instructions.", "licenseKey": "TRIAL-..." } ``` **Limits:** - Maximum 3 trial keys per email address (lifetime). - Business email required - personal and disposable domains are rejected. - Rate limit: one request per email per 24 hours. - The key is also sent by email for reference. ## Purchase a commercial license key To purchase a commercial license, contact OptimaJet at optimajet.com or sales@optimajet.com. Once you receive your key, it will begin with a customer identifier prefix (your company name for commercial keys or `TRIAL-` for trial keys). ## Register the license key Call the static method `WorkflowRuntime.RegisterLicense` once before creating any runtime instance. This is the recommended approach for console apps and worker services: ```csharp filename="Program.cs" WorkflowRuntime.RegisterLicense("YOUR-LICENSE-KEY-STRING"); var runtime = new WorkflowRuntime() .WithPersistenceProvider(persistenceProvider) .AsSingleServer() .Start(); ``` The key must be registered **before** calling `.Start()`. The key is validated at startup; an invalid or expired key throws `InvalidLicenseException` or `LicenseExpiredException`. ## Store the key in configuration Do not hard-code the key in source code. Read it from application configuration: ```json filename="appsettings.json" { "WorkflowEngine": { "LicenseKey": "YOUR-LICENSE-KEY-STRING" } } ``` ```csharp filename="Program.cs" string licenseKey = builder.Configuration["WorkflowEngine:LicenseKey"] ?? throw new InvalidOperationException("Workflow Engine license key is not configured."); WorkflowRuntime.RegisterLicense(licenseKey); var runtime = new WorkflowRuntime() .WithPersistenceProvider(persistenceProvider) .AsSingleServer() .Start(); ``` For production, store the key in a secrets manager (Azure Key Vault, AWS Secrets Manager, Kubernetes secrets) and inject it into configuration at runtime. ## Workflow Engine Free limits Without a license key, or with an expired key, Workflow Engine runs in Workflow Engine Free mode: Table: Workflow Engine Free limits | Limit | Workflow Engine Free | |----------------------------------------------|------------------------------------------------------| | Unique schemes in the process cache | 10 | | Concurrent runtime operations (semaphore) | 4 | | Activities, transitions, commands per scheme | Unlimited | | Designer | Fully functional with Workflow Engine Free indicator | Workflow Engine Free is suitable for development, evaluation, and small projects. It does not watermark or disable functionality - the scheme and concurrency limits are the only restrictions. ## Verify the license at startup Check the current license restrictions by reading `WorkflowRuntime.GetLicenseRestrictions()`: ```csharp var restrictions = WorkflowRuntime.GetLicenseRestrictions(); if (restrictions.MaxNumberOfSchemes <= 10) { logger.LogWarning( "Workflow Engine is running in Workflow Engine Free mode. " + "Scheme and thread limits apply."); } ``` This method always returns the active restrictions - Workflow Engine Free defaults if no license key is registered, or the upgraded values if a valid key was applied. ## Handle license expiry Most license keys have an expiry date - typically 12 months from purchase. When a key expires, the runtime switches to Workflow Engine Free mode - the same limits as running without a license. Perpetual keys do not expire. Once registered, they remain valid indefinitely. To restore full functionality after expiry, replace the expired key with a new one in your configuration and restart the application. No database changes are needed. For trial keys, visit trial.workflowengine.io before the current key expires. ## Deploy licenses across multiple servers In a multi-server deployment, each runtime node reads the license key from its own configuration. All nodes must use the same key. There is no per-node or per-seat licensing - the key covers the entire deployment. ## See also ## Frequently asked questions **Where do I buy or renew a license key?** Contact OptimaJet at optimajet.com or sales@optimajet.com. Licenses are available for annual subscription or perpetual purchase. **Where do I get a trial key?** Visit trial.workflowengine.io, enter your company name and business email, and click **Get Trial Key**. The key is sent to your email. You can also request one programmatically via `POST /api/trial/llm` - see trial.workflowengine.io/llms.txt for the API reference. The trial key has the `TRIAL-` prefix and works the same way as a commercial key for 30 days. **Can I use one license key in both staging and production?** Yes. You only need a paid license key for production environments. The same key works in staging and development - those environments do not require additional purchases. **What happens if I restart the application after the license expires?** The runtime switches to Workflow Engine Free mode - processes continue to run and new instances can be created, but the Workflow Engine Free limits apply. Renew the key and restart to restore full functionality. **Is the key validated online?** No. License validation is performed locally - the key string encodes your entitlements and is verified without a network call. The runtime does not phone home. **How is the key structured?** License keys contain a customer identifier prefix and base64-encoded license data. The prefix matches the customer identifier encoded in the data to prevent key sharing. The data includes expiry date, feature restrictions, and a cryptographic signature. --- # Get Started Workflow Engine is a .NET library for building stateful business process automation into your application. The Get Started section walks you through the entire setup: installing the NuGet packages, configuring a persistence provider, creating your first workflow scheme, and running it as a process instance. Whether you are building an ASP\.NET Core application or a console-based service, these guides cover the integration path that fits your project. --- # Get Started with Workflow Engine URL: https://docs.workflowengine.io/get-started/get-started-with-workflow-engine/ # Get Started with Workflow Engine Workflow Engine .NET is a library that automates business processes. You define a process - approvals, reviews, escalations, any multi-step workflow - as a **scheme**, and Workflow Engine runs it as a **process instance**. The library handles state persistence, timer scheduling, rule evaluation, and action execution. There is no separate service to deploy: you add NuGet packages to your application and configure Workflow Engine to connect it to your database. ## Why use a workflow engine Every application eventually needs to manage multi-step processes. An order goes through validation, payment processing, fulfillment, and shipping. A document goes through drafting, review, approval, and publication. A support ticket goes through triage, investigation, resolution, and customer confirmation. Without a workflow engine, you build this yourself: database tables for state tracking, background jobs for timeouts, permission checks scattered across controllers, and a growing tangle of conditional logic that is hard to test and harder to change. A workflow engine replaces all of that with a single abstraction. Separating workflow logic from application code makes the process visible and changeable. The scheme is a standalone document that anyone can read - developers, analysts, testers. Changing the process means editing the scheme, not rewriting C# methods scattered across controllers and services. A process change that would take a week of code changes takes a scheme edit and a migration call. ## What Workflow Engine .NET solves Workflow Engine .NET replaces ad-hoc state management with three building blocks: - **Scheme** - a declarative definition of the process: the steps (activities), the paths between them (transitions), and what triggers each path (commands, timers, automatic conditions). - **Runtime** - the engine that loads the scheme, creates process instances, evaluates conditions, runs actions, and persists every state change to the database. - **Process Instance** - one running execution of a scheme. Each instance tracks its own current activity, parameters, and history independently. Once a process is defined as a scheme, you can: - **Persist state automatically** - the runtime saves every transition, parameter change, and timer to the database. No manual state tracking. - **Enforce timing rules** - timers fire transitions on deadlines, SLAs, and schedules. Missed timers catch up when the application restarts. - **Control access** - rules and restrictions determine which users can execute which commands at each step. - **Extend with custom code** - actions run your C# methods when a transition fires: send emails, call APIs, update business data. - **Keep an audit trail** - every transition is recorded. You can replay a process instance's history end-to-end. ## Frequently asked questions **What is Workflow Engine .NET?** Workflow Engine .NET is a .NET library that automates business processes. You define a process as an XML scheme, and the library runs it as a process instance with automatic persistence, timers, rules, and actions. There is no separate service to deploy. **Why do I need a workflow engine instead of writing my own state machine?** A hand-written state machine works for simple cases, but every new requirement - persistence, timers, audit trails, access control - adds complexity. Workflow Engine .NET provides all of these out of the box. You define the process flow, the library handles the infrastructure. **What databases does Workflow Engine .NET support?** SQL Server, PostgreSQL, MySQL, Oracle, MongoDB, and SQLite. Each has a separate NuGet provider package. **Do I need a license to start?** No. Workflow Engine Free requires no license key and is suitable for evaluation and small projects. When you are ready for production, pass a commercial license key to unlock clustering, scheme inlining, and unlimited process instances. ## Next steps --- # Install Workflow Engine URL: https://docs.workflowengine.io/get-started/install-workflow-engine/ # Install Workflow Engine This page walks through adding Workflow Engine to an ASP.NET Core application using the Web API packages. This is the recommended integration path for ASP\.NET Core projects. For console apps, worker services, and non-ASP\.NET hosts, see Framework-Agnostic Install. For the full reference covering every option, see Web API Setup. **Prerequisites:** - .NET SDK 10.0 - A running database (SQL Server, PostgreSQL, MySQL, Oracle, MongoDB) - SQLite uses a local file and does not require a running server ## Step 1: Create a new ASP\.NET project Create a web API project using the .NET SDK: ```bash dotnet new webapi -n WorkflowApi --framework net10.0 cd WorkflowApi ``` ## Step 2: Install packages and register services Choose your database provider. Each tab shows the NuGet packages to install and the corresponding `Program.cs` setup. ```bash dotnet add package OptimaJet.Workflow.Api dotnet add package OptimaJet.Workflow.Api.Sqlite dotnet add package Swashbuckle.AspNetCore ``` Configure Program.cs (copy and paste): ```csharp filename="Program.cs" using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OptimaJet.Workflow.Api; using OptimaJet.Workflow.Api.Options; using OptimaJet.Workflow.Api.Sqlite; using OptimaJet.Workflow.Core.Runtime; var builder = WebApplication.CreateBuilder(args); // Configure JSON options for enum serialization. builder.Services.AddControllers().AddJsonOptions(ConfigureJson); // Configure Workflow API services. builder.Services.AddWorkflowApiSqlite(); builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore); builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Enable middleware to serve generated OpenAPI as a JSON endpoint and the Swagger UI. app.UseSwagger(); app.UseSwaggerUI(); // Configure routing and map the Workflow API endpoints. app.UseRouting(); app.UseCors(); app.MapWorkflowApi(); app.Run(); void ConfigureJson(JsonOptions options) { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } void SetupWorkflowApiCore(WorkflowApiCoreOptions options) { // options.LicenseKey = "PUT YOUR LICENSE KEY HERE"; } void SetupWorkflowRuntime(WorkflowTenantCreationOptions options) { options.ConnectionString = "Data Source=workflow_engine.db"; options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime => { runtime.AsSingleServer(); }; } ``` ```bash dotnet add package OptimaJet.Workflow.Api dotnet add package OptimaJet.Workflow.Api.Mssql dotnet add package Swashbuckle.AspNetCore ``` Configure Program.cs (copy and paste): ```csharp filename="Program.cs" using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OptimaJet.Workflow.Api; using OptimaJet.Workflow.Api.Options; using OptimaJet.Workflow.Api.Mssql; using OptimaJet.Workflow.Core.Runtime; var builder = WebApplication.CreateBuilder(args); // Configure JSON options for enum serialization. builder.Services.AddControllers().AddJsonOptions(ConfigureJson); // Configure Workflow API services. builder.Services.AddWorkflowApiMssql(); builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore); builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Enable middleware to serve generated OpenAPI as a JSON endpoint and the Swagger UI. app.UseSwagger(); app.UseSwaggerUI(); // Configure routing and map the Workflow API endpoints. app.UseRouting(); app.UseCors(); app.MapWorkflowApi(); app.Run(); void ConfigureJson(JsonOptions options) { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } void SetupWorkflowApiCore(WorkflowApiCoreOptions options) { // options.LicenseKey = "PUT YOUR LICENSE KEY HERE"; } void SetupWorkflowRuntime(WorkflowTenantCreationOptions options) { options.ConnectionString = "PUT YOUR CONNECTION STRING HERE"; options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime => { runtime.AsSingleServer(); }; } ``` ```bash dotnet add package OptimaJet.Workflow.Api dotnet add package OptimaJet.Workflow.Api.Mysql dotnet add package Swashbuckle.AspNetCore ``` Configure Program.cs (copy and paste): ```csharp filename="Program.cs" using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OptimaJet.Workflow.Api; using OptimaJet.Workflow.Api.Options; using OptimaJet.Workflow.Api.Mysql; using OptimaJet.Workflow.Core.Runtime; var builder = WebApplication.CreateBuilder(args); // Configure JSON options for enum serialization. builder.Services.AddControllers().AddJsonOptions(ConfigureJson); // Configure Workflow API services. builder.Services.AddWorkflowApiMysql(); builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore); builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Enable middleware to serve generated OpenAPI as a JSON endpoint and the Swagger UI. app.UseSwagger(); app.UseSwaggerUI(); // Configure routing and map the Workflow API endpoints. app.UseRouting(); app.UseCors(); app.MapWorkflowApi(); app.Run(); void ConfigureJson(JsonOptions options) { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } void SetupWorkflowApiCore(WorkflowApiCoreOptions options) { // options.LicenseKey = "PUT YOUR LICENSE KEY HERE"; } void SetupWorkflowRuntime(WorkflowTenantCreationOptions options) { options.ConnectionString = "PUT YOUR CONNECTION STRING HERE"; options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime => { runtime.AsSingleServer(); }; } ``` ```bash dotnet add package OptimaJet.Workflow.Api dotnet add package OptimaJet.Workflow.Api.Postgres dotnet add package Swashbuckle.AspNetCore ``` Configure Program.cs (copy and paste): ```csharp filename="Program.cs" using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OptimaJet.Workflow.Api; using OptimaJet.Workflow.Api.Options; using OptimaJet.Workflow.Api.Postgres; using OptimaJet.Workflow.Core.Runtime; var builder = WebApplication.CreateBuilder(args); // Configure JSON options for enum serialization. builder.Services.AddControllers().AddJsonOptions(ConfigureJson); // Configure Workflow API services. builder.Services.AddWorkflowApiPostgres(); builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore); builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Enable middleware to serve generated OpenAPI as a JSON endpoint and the Swagger UI. app.UseSwagger(); app.UseSwaggerUI(); // Configure routing and map the Workflow API endpoints. app.UseRouting(); app.UseCors(); app.MapWorkflowApi(); app.Run(); void ConfigureJson(JsonOptions options) { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } void SetupWorkflowApiCore(WorkflowApiCoreOptions options) { // options.LicenseKey = "PUT YOUR LICENSE KEY HERE"; } void SetupWorkflowRuntime(WorkflowTenantCreationOptions options) { options.ConnectionString = "PUT YOUR CONNECTION STRING HERE"; options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime => { runtime.AsSingleServer(); }; } ``` ```bash dotnet add package OptimaJet.Workflow.Api dotnet add package OptimaJet.Workflow.Api.Oracle dotnet add package Swashbuckle.AspNetCore ``` Configure Program.cs (copy and paste): ```csharp filename="Program.cs" using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OptimaJet.Workflow.Api; using OptimaJet.Workflow.Api.Options; using OptimaJet.Workflow.Api.Oracle; using OptimaJet.Workflow.Core.Runtime; var builder = WebApplication.CreateBuilder(args); // Configure JSON options for enum serialization. builder.Services.AddControllers().AddJsonOptions(ConfigureJson); // Configure Workflow API services. builder.Services.AddWorkflowApiOracle(); builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore); builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Enable middleware to serve generated OpenAPI as a JSON endpoint and the Swagger UI. app.UseSwagger(); app.UseSwaggerUI(); // Configure routing and map the Workflow API endpoints. app.UseRouting(); app.UseCors(); app.MapWorkflowApi(); app.Run(); void ConfigureJson(JsonOptions options) { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } void SetupWorkflowApiCore(WorkflowApiCoreOptions options) { // options.LicenseKey = "PUT YOUR LICENSE KEY HERE"; } void SetupWorkflowRuntime(WorkflowTenantCreationOptions options) { options.ConnectionString = "PUT YOUR CONNECTION STRING HERE"; options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime => { runtime.AsSingleServer(); }; } ``` ```bash dotnet add package OptimaJet.Workflow.Api dotnet add package OptimaJet.Workflow.Api.Mongo dotnet add package Swashbuckle.AspNetCore ``` Configure Program.cs (copy and paste): ```csharp filename="Program.cs" using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; using OptimaJet.Workflow.Api; using OptimaJet.Workflow.Api.Options; using OptimaJet.Workflow.Api.Mongo; using OptimaJet.Workflow.Core.Runtime; var builder = WebApplication.CreateBuilder(args); // Configure JSON options for enum serialization. builder.Services.AddControllers().AddJsonOptions(ConfigureJson); // Configure Workflow API services. builder.Services.AddWorkflowApiMongo(); builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore); builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // Enable middleware to serve generated OpenAPI as a JSON endpoint and the Swagger UI. app.UseSwagger(); app.UseSwaggerUI(); // Configure routing and map the Workflow API endpoints. app.UseRouting(); app.UseCors(); app.MapWorkflowApi(); app.Run(); void ConfigureJson(JsonOptions options) { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } void SetupWorkflowApiCore(WorkflowApiCoreOptions options) { // options.LicenseKey = "PUT YOUR LICENSE KEY HERE"; } void SetupWorkflowRuntime(WorkflowTenantCreationOptions options) { options.ConnectionString = "PUT YOUR CONNECTION STRING HERE"; options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime => { runtime.AsSingleServer(); }; } ``` The `ConnectionString` value in `SetupWorkflowRuntime` needs to point to your database. For SQLite, the default `Data Source=workflow_engine.db` creates a local file - update the path to match your project. For other providers, replace `PUT YOUR CONNECTION STRING HERE` with your actual connection string. The `LicenseKey` line in `SetupWorkflowApiCore` is commented out by default. If you have a license key, uncomment the line and replace `PUT YOUR LICENSE KEY HERE` with your actual key. To obtain a trial key, visit trial.workflowengine.io or use the API at /api/trial/llm. See the License Key page for details. Without a license key, the Web API still serves the Workflow Designer (`POST /designer`), readiness probes (`GET /readiness`, `GET /tenant-readiness`), and liveness checks (`GET /liveness`). All other endpoints require a valid license key. ## Step 3: Run and verify Start the application: ```bash dotnet run -- --urls "http://localhost:5274/" ``` Open the Swagger UI at to explore and test the API endpoints. ## Optional: Run the Designer standalone You can launch the Workflow Designer as a standalone web application without embedding it into your own frontend. Run it via npx (no installation required): ```bash npx @optimajet/workflow-designer http://localhost:5274/workflow-api/designer MyWorkflowScheme ``` - `http://localhost:5274/workflow-api/designer` - the URL of the Designer endpoint exposed by the Web API - `MyWorkflowScheme` - the scheme code to open in the Designer on startup This opens a browser tab with the full Designer UI connected to your local Web API. Use it to create and edit workflow schemes interactively. ## GitHub sample A complete sample project covering all database providers, JWT security, API client generation, and integration tests is available on GitHub: https://github.com/optimajet/WorkflowEngineApi.NET ## Next steps - Create Your First Workflow - save a scheme and create a process instance - Integrate the Designer - run standalone or embed in your app - Web API Setup - full configuration reference: options tables, DI registries, troubleshooting - Web API Security - add JWT authentication before production ## Frequently asked questions **Do I need a license key?** Yes. The Web API requires a license key. Request one from our sales team at sales@optimajet.com or get a trial key at trial.workflowengine.io. Set the key in the `SetupWorkflowApiCore` method. Without a license key, the following endpoints still work: - `POST /designer` - Workflow Designer operations - `GET /readiness` - readiness probe - `GET /tenant-readiness` - tenant readiness probe - `GET /liveness` - liveness probe **Which database providers are supported?** MSSQL, PostgreSQL, MySQL, Oracle, MongoDB, and SQLite. Each has a corresponding `OptimaJet.Workflow.Api.*` package. Install one and call the matching extension method. **Can I disable authentication during development?** The Web API uses ASP\.NET Core security. Configure it through the standard ASP\.NET Core middleware. See Web API Security. **Can I use Workflow Engine without ASP\.NET Core?** Yes. For console applications, worker services, and any non-ASP\.NET host, use the direct `WorkflowRuntime` setup. See Framework-Agnostic Install. --- # Create Your First Workflow URL: https://docs.workflowengine.io/get-started/create-your-first-workflow/ # Create Your First Workflow In this guide you will create a two-step approval workflow, save it to the database, and start a process instance - all through the Web API. The workflow models a document that starts in `Draft` and moves to `Approved` when the `Submit` command is executed. A scheme defines the activities, transitions, and commands of a process. You will write one as an XML file and upload it to the running Web API, which stores schemes in the database and uses them as blueprints when creating process instances. **Prerequisites:** - Workflow Engine installed and running ## Step 1: Define the scheme XML A scheme contains three sections: `` (the triggers), `` (the states), and `` (the movements between states). The scheme below models a document that starts in `Draft` and moves to `Approved` when the `Submit` command is executed. Save this as `Approval.xml`: !Approval scheme: Draft transitions to Approved via Submit command ```xml filename="Approval.xml" ``` An activity with `IsInitial="true"` is where the process starts. An activity with `IsFinal="true"` ends the process when reached. A trigger with `Type="Command"` fires the transition when the matching command executes. A condition with `Type="Always"` means the transition has no guard - it fires unconditionally. You can also create schemes visually using the standalone Designer. No installation needed - run it via npx: ```bash npx @optimajet/workflow-designer http://localhost:5274/workflow-api/designer Approval ``` This opens a browser tab with the full Designer UI connected to your Web API. You can draw activities and transitions visually and save them, or load an existing XML file through the Designer's menu - the scheme is stored in the database just like the XML approach above. ## Step 2: Upload and save the scheme The Designer API accepts scheme files in two steps: parse the XML into JSON, then save it to the database. Use a temporary file to pass the JSON between the two calls: ```bash # Step 1: Parse the XML file into JSON curl -s -X POST http://localhost:5274/workflow-api/designer \ -F "operation=uploadscheme" \ -F "schemecode=Approval" \ -F "schemeXml=@Approval.xml" > scheme.json # Step 2: Save the parsed scheme to the database curl -s -X POST http://localhost:5274/workflow-api/designer \ -F "operation=save" \ -F "schemecode=Approval" \ -F "data= WorkflowEngine Designer
``` Save this as `process.html` and serve it through a local HTTP server (opening directly from `file://` may not work due to browser security): ```bash npx serve . ``` Then open in your browser. The Designer loads the process instance and highlights its current activity. ## Step 4: Check available commands Before executing a command, you can check which commands are available for the current activity and user: ```bash curl -X POST http://localhost:5274/workflow-api/rpc/get-available-commands \ -H "Content-Type: application/json" \ -d '{ "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c", "identityIds": ["user-42"] }' ``` Response: ```json { "availableCommands": [ { "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c", "name": "Submit", "localizedName": "Submit", "parameters": [], "validForActivityName": "Draft", "validForStateName": "Draft", "isForSubprocess": false, "classifier": "Direct", "identities": [ "user-42" ] } ] } ``` The runtime returns only the commands whose conditions and rules permit the given identity. In this case, `Submit` is available because the process is at `Draft` and the transition has `Type="Always"` - no restrictions. ## Step 5: Execute a command Advance the process from `Draft` to `Approved` by executing the `Submit` command. The command is valid only while the process is at `Draft`: ```bash curl -X POST http://localhost:5274/workflow-api/rpc/execute-command \ -H "Content-Type: application/json" \ -d '{ "command": { "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c", "name": "Submit", "validForActivityName": "Draft" } }' ``` Verify the new state: ```bash curl -X POST http://localhost:5274/workflow-api/rpc/get-process-instance \ -H "Content-Type: application/json" \ -d '{ "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c" }' ``` Response: ```json { "processInstance": { "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c", "currentActivityName": "Approved", "currentState": "Approved", "schemeCode": "Approval" } } ``` The process moved to `Approved` - the workflow ran to completion. ## Step 6: Set state directly (optional) You can move a process instance to any activity directly, bypassing transitions and conditions. This is useful for error recovery or testing: ```bash curl -X POST http://localhost:5274/workflow-api/rpc/set-state-without-execution \ -H "Content-Type: application/json" \ -d '{ "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c", "stateName": "Draft" }' ``` The process is now back at `Draft` without executing any transitions. This can be useful for testing or recovering a process that reached an incorrect state. ## Step 7: Delete the process instance (optional) Delete the process instance and all its data when it is no longer needed: ```bash curl -X POST http://localhost:5274/workflow-api/rpc/delete-instance \ -H "Content-Type: application/json" \ -d '{ "processId": "a3f5b2c1-4d6e-7f8a-9b0c-1d2e3f4a5b6c" }' ``` This removes the process instance and all its subprocesses from the database. ## What you built - Defined a scheme with `Draft` and `Approved` activities, a `Submit` command, and a transition between them. - Uploaded the scheme XML to the Web API and saved it to the database. - Created a process instance and verified it was positioned at `Draft`. - Viewed the process instance in the Designer UI. - Checked which commands are available for a user. - Executed the `Submit` command and the process advanced to `Approved`. - Moved the process back to `Draft` using direct state control. - Deleted the process instance when it was no longer needed. ## Next steps ## Frequently asked questions **What is the minimum scheme XML needed to define a workflow?** A minimal scheme needs at least one command, an initial activity, a final activity, and a transition connecting them. The example on this page, a two-activity approval with Draft, Approved, and a single Submit command, is a complete working scheme. **How do I save a scheme through the Web API?** Send the XML file to the `designer` endpoint with `operation=uploadscheme` to parse it into JSON, then send the JSON back with `operation=save` to store it in the database. Both calls use the same endpoint - see Step 2 above for the exact commands. **How do I know my scheme was saved correctly?** Create a process instance through the Web API and check the current activity. If `POST /rpc/get-process-instance` returns the initial activity name in `currentActivityName`, the scheme was saved correctly. **How do I check which commands are available?** Use `POST /rpc/get-available-commands` with the `processId` and `identityIds` array. The runtime evaluates conditions and rules on each outgoing transition and returns only the commands the user is allowed to execute. **How do I advance a process from one activity to another?** Execute a command that triggers a transition. Use `POST /rpc/execute-command` with the command name and the activity where the command is valid. The command must match a `Command` element in the scheme, and the transition must have a trigger referencing that command. **How do I move a process to a specific activity without executing a transition?** Use `POST /rpc/set-state-without-execution` with the process ID and the target state name. The target activity must have `IsForSetState="true"` and a `State` attribute in the scheme. **How do I delete a process instance?** Use `POST /rpc/delete-instance` with the process ID. This removes the instance and all its subprocesses from the database. **Can I view a process instance in the Designer?** Yes. Create an HTML file that loads the WorkflowDesigner and call `designer.load()` with both `schemecode` and `processid` - see the example after Step 3 above. Serve the file through a local HTTP server. --- # Framework-Agnostic Install URL: https://docs.workflowengine.io/get-started/framework-agnostic-install/ # Framework-Agnostic Install This page shows how to install Workflow Engine in a console application, worker service, or any non-ASP\.NET Core host. For ASP\.NET Core projects, use the Web API install instead - it provides HTTP endpoints, built-in security, and multitenancy. The example below uses a console application, but the same steps apply to any .NET host. **Prerequisites:** - .NET 10 SDK - A running database (SQL Server, PostgreSQL, MySQL, Oracle, or MongoDB) - SQLite uses a local file and does not require a running server ## Step 1: Create a new console project Start by creating a console application with the .NET SDK: ```bash dotnet new console -n WorkflowConsole --framework net10.0 cd WorkflowConsole ``` ## Step 2: Install packages and write Program.cs Choose your database provider. Each tab shows the NuGet packages to install and the corresponding `Program.cs` code. ```bash dotnet add package WorkflowEngine.NETCore-Core dotnet add package WorkflowEngine.NETCore-ProviderForSQLite ``` Copy and paste the following code to replace the contents of `Program.cs`: ```csharp filename="Program.cs" using System.Xml.Linq; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Parser; using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.Migrator; using OptimaJet.Workflow.SQLite; var connectionString = "Data Source=workflow.db"; var provider = new SqliteProvider(connectionString); var workflowBuilder = new WorkflowBuilder( provider, new XmlWorkflowParser(), provider ).WithDefaultCache(); var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(provider) .RunMigrations() .Start(); Console.WriteLine("Workflow Runtime started. Press any key to stop."); Console.ReadKey(); await runtime.ShutdownAsync(); Console.WriteLine("Workflow Runtime stopped."); ``` ```bash dotnet add package WorkflowEngine.NETCore-Core dotnet add package WorkflowEngine.NETCore-ProviderForMSSQL ``` Copy and paste the following code to replace the contents of `Program.cs`: ```csharp filename="Program.cs" using System.Xml.Linq; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Parser; using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.Migrator; using OptimaJet.Workflow.DbPersistence; var connectionString = "PUT YOUR CONNECTION STRING HERE"; var provider = new MSSQLProvider(connectionString); var workflowBuilder = new WorkflowBuilder( provider, new XmlWorkflowParser(), provider ).WithDefaultCache(); var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(provider) .RunMigrations() .Start(); Console.WriteLine("Workflow Runtime started. Press any key to stop."); Console.ReadKey(); await runtime.ShutdownAsync(); Console.WriteLine("Workflow Runtime stopped."); ``` ```bash dotnet add package WorkflowEngine.NETCore-Core dotnet add package WorkflowEngine.NETCore-ProviderForMySQL ``` Copy and paste the following code to replace the contents of `Program.cs`: ```csharp filename="Program.cs" using System.Xml.Linq; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Parser; using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.Migrator; using OptimaJet.Workflow.MySQL; var connectionString = "PUT YOUR CONNECTION STRING HERE"; var provider = new MySQLProvider(connectionString); var workflowBuilder = new WorkflowBuilder( provider, new XmlWorkflowParser(), provider ).WithDefaultCache(); var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(provider) .RunMigrations() .Start(); Console.WriteLine("Workflow Runtime started. Press any key to stop."); Console.ReadKey(); await runtime.ShutdownAsync(); Console.WriteLine("Workflow Runtime stopped."); ``` ```bash dotnet add package WorkflowEngine.NETCore-Core dotnet add package WorkflowEngine.NETCore-ProviderForPostgreSQL ``` Copy and paste the following code to replace the contents of `Program.cs`: ```csharp filename="Program.cs" using System.Xml.Linq; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Parser; using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.Migrator; using OptimaJet.Workflow.PostgreSQL; var connectionString = "PUT YOUR CONNECTION STRING HERE"; var provider = new PostgreSQLProvider(connectionString); var workflowBuilder = new WorkflowBuilder( provider, new XmlWorkflowParser(), provider ).WithDefaultCache(); var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(provider) .RunMigrations() .Start(); Console.WriteLine("Workflow Runtime started. Press any key to stop."); Console.ReadKey(); await runtime.ShutdownAsync(); Console.WriteLine("Workflow Runtime stopped."); ``` ```bash dotnet add package WorkflowEngine.NETCore-Core dotnet add package WorkflowEngine.NETCore-ProviderForOracle ``` Copy and paste the following code to replace the contents of `Program.cs`: ```csharp filename="Program.cs" using System.Xml.Linq; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Parser; using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.Migrator; using OptimaJet.Workflow.Oracle; var connectionString = "PUT YOUR CONNECTION STRING HERE"; var provider = new OracleProvider(connectionString); var workflowBuilder = new WorkflowBuilder( provider, new XmlWorkflowParser(), provider ).WithDefaultCache(); var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(provider) .RunMigrations() .Start(); Console.WriteLine("Workflow Runtime started. Press any key to stop."); Console.ReadKey(); await runtime.ShutdownAsync(); Console.WriteLine("Workflow Runtime stopped."); ``` ```bash dotnet add package WorkflowEngine.NETCore-Core dotnet add package WorkflowEngine.NETCore-ProviderForMongoDB ``` Copy and paste the following code to replace the contents of `Program.cs`: ```csharp filename="Program.cs" using System.Xml.Linq; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Parser; using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.MongoDB; var connectionString = "PUT YOUR CONNECTION STRING HERE"; var provider = new MongoDBProvider(connectionString); var workflowBuilder = new WorkflowBuilder( provider, new XmlWorkflowParser(), provider ).WithDefaultCache(); var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(provider) .Start(); Console.WriteLine("Workflow Runtime started. Press any key to stop."); Console.ReadKey(); await runtime.ShutdownAsync(); Console.WriteLine("Workflow Runtime stopped."); ``` The `connectionString` value needs to point to your database. For SQLite, the default `Data Source=workflow.db` creates a local file - update the path to match your project. For other providers, replace `PUT YOUR CONNECTION STRING HERE` with your actual connection string. ## Step 4: Run and verify Build and run the project to check that the runtime starts correctly: ```bash dotnet run ``` If the runtime starts without exceptions, Workflow Engine creates all required tables in your database automatically. ## Next steps ## Frequently asked questions **Which NuGet packages do I need for a non-ASP\.NET Core project?** Install `WorkflowEngine.NETCore-Core` and the persistence provider package matching your database - for example, `WorkflowEngine.NETCore-ProviderForSQLite` for SQLite or `WorkflowEngine.NETCore-ProviderForMSSQL` for SQL Server. The provider transitively pulls in `WorkflowEngine.NETCore-Migrator` (schema setup). **How do I initialize WorkflowRuntime without ASP\.NET Core?** Create a persistence provider instance with your connection string, pass it to `WorkflowBuilder`, call `RunMigrations()` to create the required database tables, then call `Start()`. No DI container or web host is needed. **Does Workflow Engine create its own database tables automatically?** For SQL providers, calling `RunMigrations()` during initialization creates all required tables in the target database if they do not exist, and applies any pending schema updates. You do not need to run SQL scripts manually. For MongoDB, `RunMigrations()` is not applicable - collections and indexes are created automatically by the provider at runtime, no explicit migration call is needed. **Can I use Workflow Engine without a database at runtime?** No. Workflow Engine persists process state, scheme definitions, timers, and history to a database. A persistence provider is required. --- # Integrate the Designer URL: https://docs.workflowengine.io/get-started/integrate-the-designer/ # Integrate the Designer The Workflow Designer is a visual editor for creating and modifying workflow schemes. You can run it standalone - no installation required - or embed it as a component in your application. Both approaches connect to the same DesignerAPI endpoint on your backend. ## Standalone via npx Run the Designer in a single command. It starts a local web server and opens a browser tab with the full Designer UI connected to your backend: ```bash npx @optimajet/workflow-designer ``` Table: Designer standalone npx parameters | Parameter | Description | |----------------|---------------------------------------------------------------------------------------------| | `` | URL of your DesignerAPI endpoint, for example `http://localhost:5274/workflow-api/designer` | | `` | The code of the scheme to open for editing, for example `MyWorkflowScheme` | Example for a locally running Web API: ```bash npx @optimajet/workflow-designer http://localhost:5274/workflow-api/designer MyWorkflowScheme ``` The Designer opens in your default browser. You can draw activities and transitions, add commands, and save the scheme - it is stored in the database through the API endpoint. ## Embed in your application For production use, embed the Designer as a frontend component directly into your application UI. The component connects to the same DesignerAPI endpoint and loads, saves, and validates schemes from your backend: - **Embed Designer in React** - use @optimajet/workflow-designer-react with full component API, refs, and callbacks. - **Embed Designer in Angular** - use @optimajet/workflow-designer-angular with Angular component bindings. - **Embed Designer in Blazor** - use @optimajet/workflow-designer with JavaScript interop in Blazor WebAssembly or Server. - **Embed Designer with Vanilla JS** - load the Designer from the CDN and initialize it with a plain `new WorkflowDesigner(...)` call, no framework required. ## Frequently asked questions **Do I need to install anything for the standalone Designer?** No. `npx @optimajet/workflow-designer` downloads and runs the Designer on the fly. The only requirement is Node.js `>=18` installed on your machine. **Where does the standalone Designer store schemes?** Schemes are not stored locally. The standalone Designer is a frontend-only tool - it sends all data to the DesignerAPI endpoint, which persists it to your database. When you click Save, the scheme goes to your backend just like it would with an embedded Designer. **What is the DesignerAPI endpoint?** The DesignerAPI endpoint is part of Workflow Engine's backend. It is available at `/workflow-api/designer` when using the Web API, or through `WorkflowRuntime.DesignerAPI()` in framework-agnostic setups. The Designer component (standalone or embedded) sends HTTP requests to this endpoint to load, save, and validate schemes. **Can I use the standalone Designer for production?** The standalone Designer is designed for development, testing, and quick edits. For production, embed the Designer as a component in your application - this gives you control over authentication, styling, and the user experience. ## Next steps --- # Workflow Engine Concepts URL: https://docs.workflowengine.io/get-started/concepts/workflow-engine-concepts/ # Workflow Engine Concepts Workflow Engine provides seventeen concepts organised into two groups. The seven core concepts cover everything you need to configure, run, and persist a workflow. The ten additional concepts control what happens when a transition fires, handle advanced scenarios, or extend the workflow with logging, localization, and scheduling. Use this page to find the concept you need and jump to its full description. ## Core Concepts These seven concepts are required for every Workflow Engine integration. ## Additional Concepts How transitions get triggered, gated, enriched with code, and extended with metadata, logging, localization, and scheduling. ## Frequently asked questions **Do I need to understand all concepts before building my first workflow?** No. The seven core concepts are enough to build and run a workflow. Behaviour concepts can be added as your workflows need conditions, custom code, or automatic triggers. **Which concepts are required for a minimal workflow?** Activity, Transition, Command, Scheme, Process Instance, Workflow Runtime, and Persistence. Create a scheme with at least two activities and one transition, configure the runtime with a persistence provider, then create an instance and execute a command. **Can I use Workflow Engine without a database?** No. The runtime requires a persistence provider to store schemes, instances, and history. SQLite is the lightest option for local development. **What is the difference between a Condition and a Rule?** A condition guards whether a transition fires - it evaluates data or state. A rule guards who can fire a transition - it evaluates the user's identity. Both run during transition evaluation but serve different purposes. ## See also --- # Activity URL: https://docs.workflowengine.io/get-started/concepts/activity/ # Activity An activity is a stable state where a process instance waits. Activities are defined in a scheme. Every scheme has exactly one initial activity where new process instances start and can have zero or more activities marked as final. A process instance leaves an activity when a command executes, a timer fires, or an automatic transition fires. A process instance is always positioned at exactly one activity at a time. During normal execution, when a process instance enters an activity, the activity can execute useful work through actions. Actions are blocks of code that run automatically upon entry, such as sending notifications, updating business data, or calling external services. Actions are attached to the activity via the `Implementation` property. ## Activity properties Each activity has a set of properties that define its identity, behavior, and position in the scheme. ### Core properties These are the most commonly configured properties: Table: Core activity properties | Property | Type | Description | |----------------|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------| | Name | `string` | Case-sensitive, unique identifier for the activity within the scheme | | State | `string` | The business state name associated with this activity; non-unique or absent | | IsInitial | `bool` | Only one activity per scheme may be initial | | IsFinal | `bool` | Marks a final activity; optional in regular schemes and required in schemes used for inlining | | IsForSetState | `bool` | Marks this activity as the target when moving a process to a state by name. Only one activity per state value should have this enabled | | Implementation | `list` of `ActionDefinitionReference` | Actions that execute when a process instance enters this activity | ### Scenario-specific properties These properties are not always needed. Some are used in specialized scenarios such as disabling database persistence, setting timeouts, or handling errors. Others are metadata helpers: Table: Scenario-specific activity properties | Property | Type | Description | |---------------------------------|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| | IsAutoSchemeUpdate | `bool` | Allows the process instance to be updated to a new scheme version when it reaches this activity | | UserComment | `string` | Optional custom comment attached to the activity | | Annotations | `list` of `Annotation` | Key-value metadata attached to the activity; see Annotations | | DisablePersistState | `bool` | Skips saving the current state of the process instance when entering this activity | | DisablePersistTransitionHistory | `bool` | Skips saving the transition history entry when passing through this activity | | DisablePersistParameters | `bool` | Skips saving the parameter values when entering this activity | | ExecutionTimeout | `ActivityTimeout` | Limits activity execution time and cancels it when the interval expires; asynchronous actions must use the provided `CancellationToken` | | IdleTimeout | `ActivityTimeout` | Limits how long the process instance may sit in this activity without any transition | | ExceptionsHandlers | `list` of `ActivityExceptionsHandler` | Configures what should happen if an action throws an exception | | PreExecutionImplementation | `list` of `ActionDefinitionReference` | Actions executed only during pre-execution simulation | ### Exception handling When an action on an activity throws an exception, you can add exception handlers to the activity. If no handler processes the exception, it reaches the normal workflow error handling. Each handler has two parts: which exceptions to catch and what to do when one occurs. The runtime evaluates handlers by ascending `Order` and uses the first matching handler. To catch a specific exception, use its full type name such as `System.InvalidOperationException`. To catch any exception, use `*`, and place that handler after more specific handlers. Four actions are available when an exception is caught: - **SetActivity** - move the process instance to another activity. You specify which activity by name. - **SetState** - move the process instance to a business state. The runtime finds the activity that matches that state. - **Retry** - run the activity implementation again. You specify how many times to retry before giving up. - **Ignore** - suppress the exception and continue workflow execution. ```xml ``` In this example, `InvalidOperationException` reaches the handler with `Order="1"`, so the process moves to the `Failed` activity. Any other exception reaches the wildcard handler with `Order="2"` and retries the activity implementation up to three times. ### Execution and idle timeouts An activity can have two timeouts that limit how long things can take. - **ExecutionTimeout** - requests cancellation when activity execution exceeds the configured interval, then applies the configured result. - **IdleTimeout** - limits how long the process instance can sit at this activity without any transition firing. If the timer expires, the runtime takes the configured action. `ExecutionTimeout` cancels execution through the `CancellationToken` passed to asynchronous actions and custom activity execution. It cannot stop synchronous code or asynchronous code that ignores the token; the runtime waits for that code to finish before handling the timeout. Each timeout defines an interval and what to do when it expires. `ExecutionTimeout` supports `SetActivity`, `SetState`, and `Retry`. `IdleTimeout` supports `SetActivity` and `SetState`. `IdleTimeout` is implemented as an interval timer and supports work calendars when its value uses calendar-aware units such as `2wd` or `4 whours`. `ExecutionTimeout` parses a regular elapsed-time interval directly and does not use work calendars. ```xml ``` In this example, if the process stays at the `Review` activity for more than 2 days without advancing, the runtime moves it to the `Escalated` state. ### Disabling persistence By default, the runtime saves the process state, its parameters, and the transition history to the database through the persistence provider every time the process moves. For high-throughput scenarios where every millisecond counts, you can skip some of these saves on a per-activity basis: - **`DisablePersistState`** - skip saving the current state (activity and status) for this activity. - **`DisablePersistParameters`** - skip saving the parameter values for this activity. - **`DisablePersistTransitionHistory`** - skip saving the transition history entry when moving through this activity. These flags do not affect reads - the runtime still loads state and parameters from the database when needed. They only disable the write that happens after each transition. Use them on high-traffic activities where you are willing to lose the latest state data in exchange for faster execution. ### Pre-execution implementation An activity can have two sets of actions: the regular `Implementation` that runs during normal execution and `PreExecutionImplementation` that runs only during pre-execution simulation. During simulation, the runtime uses `PreExecutionImplementation` instead of `Implementation` for a visited activity. Pre-execution actions should implement simulation-specific behavior. They can still call external systems, so adding an action to `PreExecutionImplementation` does not make that action free of side effects. ```xml ``` ### What is State The `State` property represents the **business state**, which is the human-readable status such as "Draft", "Under Review", "Approved". An `Activity` is the **physical state** that the runtime tracks internally. One `State` can be shared across multiple activities; for example, "Under Review" can be the state for both a `ManagerReview` activity and a `DirectorReview` activity. ## Declaring activities in a scheme Activities are listed in the `` section of a scheme. Each `` element defines one waiting point in the process. Activities are connected by transitions, which determine the paths a process instance can take from one activity to another. The activity itself does not execute logic. It is a named waiting point that holds the process instance until an outgoing transition fires. Actions run when the process instance arrives at an activity. Each activity can be configured to perform specific work, such as sending notifications, changing data, or calling an external service. This work is defined by the activity's actions. ```xml ``` `IsInitial="true"` marks the activity where Workflow Engine positions new instances. `IsFinal="true"` is a marker: if execution stops in that activity, the runtime sets the process status to `Finalized`. The marker does not prohibit outgoing transitions. A regular scheme may have no final activities, but a scheme used for inlining must have at least one so the inlined fragment can connect back to its parent scheme. ## Managing activities from code You can move a process instance to a specific activity or state programmatically without going through a normal transition. These methods are useful for error recovery, manual overrides, and testing. ### SetState method `SetStateAsync` selects an activity whose `State` matches the requested name and whose `IsForSetState` property is `true`. By default, the method executes that activity's `Implementation` and follows its automatic transitions, bypassing normal incoming transition conditions: ```csharp var setStateParams = new SetStateParams(processId, "Approved"); await runtime.SetStateAsync(setStateParams); ``` ### SetActivity method Moves the process to a specific activity directly, without requiring its `State` or `IsForSetState` properties. You have two options: - `SetActivityWithExecutionAsync` - moves and runs the activity's actions. - `SetActivityWithoutExecutionAsync` - moves without running actions. ```csharp var scheme = processInstance.ProcessScheme; var activity = scheme.FindActivity("Review"); await runtime.SetActivityWithExecutionAsync( "user-42", null, null, activity, processInstance); ``` ### Resume process `ResumeAsync` continues from the current activity, or from the activity named in `ResumeParams`, without executing that starting activity's `Implementation` again. It evaluates the activity's outgoing automatic transitions and follows one when its conditions pass. Unlike `SetStateAsync`, `ResumeAsync` does not select a target by business state or execute the selected starting activity: ```csharp var result = await runtime.ResumeAsync(processId); ``` ### Finding activities in a scheme Every process instance holds a reference to its scheme. Use it to look up activities by name or find the starting point: ```csharp var scheme = processInstance.ProcessScheme; // Find the initial activity var start = scheme.InitialActivity; // Find a specific activity by name var review = scheme.FindActivity("Review"); // List all activities List allActivities = scheme.Activities; ``` `FindActivity` throws an exception if no activity with that name exists. ## See also ## Frequently asked questions **What is an activity in Workflow Engine?** An activity is a named stable state in a scheme where a process instance waits until a command, timer, or automatic transition moves it forward. Every scheme has exactly one initial activity and may have zero or more activities marked as final. **Can multiple activities share the same state?** Yes. Multiple activities can have the same `State` value. For example, both a `ManagerReview` and a `DirectorReview` activity can use the "Under Review" state. The activity name identifies the physical waiting point, while the state is the business status. **Where are activities declared in a scheme?** Activities are declared in the `` section of the scheme XML. Each `` element defines one waiting point, and activities are connected by transitions that determine how a process instance moves from one to another. --- # Transition URL: https://docs.workflowengine.io/get-started/concepts/transition/ # Transition A transition connects two activities in a scheme. Every transition has a source activity where the transition starts, and a target activity where the process instance moves after the transition fires. What causes a transition to fire depends on its trigger. A command trigger fires when someone calls a named command. An auto trigger fires after the process instance enters the source activity and its implementation completes. A timer trigger fires when a configured timer expires. A transition can also have conditions that select a route based on process data, and restrictions that control actor access to a command transition. A condition answers whether the route can run now; a restriction answers whether the actor may run it. When a transition fires, the process instance moves to the target activity and that activity's actions run. ## Transition properties Each transition has a set of properties that define its source, target, trigger, and behavior. ### Core properties These are the most commonly configured properties: Table: Core transition properties | Property | Type | Description | |--------------|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| | Name | `string` | Unique identifier for the transition within the scheme | | From | `ActivityDefinition` | Source activity where the process instance must be for this transition to fire | | To | `ActivityDefinition` | Target activity where the process instance moves when the transition fires | | Classifier | `enum` (`NotSpecified`, `Direct`, `Reverse`) | Classifies the transition direction. `Direct` for forward transitions, `Reverse` for backward or corrective, `NotSpecified` when not classified | | Trigger | `trigger` (`Command`, `Auto`, `Timer`) | What causes the transition to fire. `Command` fires for a matching command, `Auto` after the source implementation, and `Timer` on expiry | | Conditions | `list` of conditions | Determines whether the transition is eligible to fire. Multiple action or expression conditions are combined by `ConditionsConcatenationType` | | Restrictions | `list` of restrictions | Allow and restrict actor rules that control access to a command transition | ### Scenario-specific properties These properties are needed in specific cases such as combining rules, subprocess integration, or metadata: Table: Scenario-specific transition properties | Property | Type | Description | |-----------------------------|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| | ConditionsConcatenationType | `enum` (`And`, `Or`) | How multiple conditions are combined. `And` means all must pass, `Or` means any one is enough | | AllowConcatenationType | `enum` (`And`, `Or`) | How multiple allow restrictions are combined | | RestrictConcatenationType | `enum` (`And`, `Or`) | How multiple restrict restrictions are combined | | UserComment | `string` | Optional custom comment attached to the transition | | Annotations | `list` of `Annotation` | Key-value metadata attached to the transition; see Annotations | | IsFork | `bool` | Marks this transition as the start or end of a subprocess branch | | SubprocessInOutDefinition | `enum` (`Auto`, `Start`, `Finalize`) | Defines how the transition relates to a subprocess | | MergeViaSetState | `bool` | Subprocess only. On a finalizing transition, selects a forced parent state change instead of evaluating auto transitions from the target activity | | DisableParentStateControl | `bool` | Subprocess only. On a starting transition, disables automatic subprocess deletion based on the parent activity | ### Transition and subprocess A transition can start a subprocess or merge it back into the parent. To make a transition do either, add `IsFork="true"` to the transition and set `SubprocessInOutDefinition`: - `SubprocessInOutDefinition="Start"` - this transition creates a new subprocess when it fires. - `SubprocessInOutDefinition="Finalize"` - this transition merges the subprocess back when it fires. - `SubprocessInOutDefinition="Auto"` - the runtime decides based on where the transition sits in the scheme. `MergeViaSetState` applies to a transition that finalizes a subprocess. With `false`, the runtime merges the parameters, deletes the subprocess, and evaluates auto transitions that leave the finalizing transition's target activity. If none fires, the parent remains in its current activity and becomes idle. With `true`, the runtime instead forcibly moves the parent to the target activity and runs that activity's implementation. `DisableParentStateControl` applies to a transition that starts a subprocess. With `false`, the runtime calculates the parent activities where the subprocess may exist and deletes it when the parent leaves that set. With `true`, parent activity changes do not delete the subprocess automatically; its own completion or merge behavior still applies. For a full explanation with examples, see the Subprocess concept page. ### What is Classifier The `Classifier` property classifies the direction of a transition. It has three possible values: - **NotSpecified**. No classification assigned. This is the default value. - **Direct**. A forward transition in the normal workflow flow. - **Reverse**. A backward or corrective transition used to return the process instance to an earlier activity. The classifier does not select or trigger a transition; triggers and conditions do. The runtime uses the classifier to limit pre-execution to direct transitions, filter direct and reverse actor queries, and record the transition direction. ### What is Trigger The `Trigger` property defines what causes the transition to fire. - **Command**. Fires when someone executes a matching command. The command must be declared separately in the scheme's `` section. - **Auto**. Fires automatically after the process instance enters the source activity and its implementation completes. No external call is needed. Useful for routing logic that should run immediately. - **Timer**. Fires when a configured timer expires. Timer transitions do not need a command call. ## How transitions connect activities in a scheme Transitions are declared in the `` section of a scheme. Each `` element specifies the source and target activities, the classifier, and nested elements for triggers, conditions, and restrictions. Activities are declared in the `` section and referenced by name in the transitions. !Transition from Draft to Review ```xml filename="DocumentReview.xml" {11-23} ``` The trigger is defined as a nested `` element, not as an attribute on the transition. In the example above, the trigger type is `Auto`, which means the transition fires automatically when the process instance arrives at the source activity `Draft`, without any command call. Conditions are defined as a nested `` element. Each condition can be of type `Always` (always passes), `Action` (a named C# method), `Expression`, or `Otherwise` (a fallback). Without at least one condition, the transition never fires. Even an auto-triggered transition needs a condition to pass. ## Accessing transitions from code You can inspect transitions programmatically from the scheme definition. A `ProcessInstance` holds a reference to its scheme through `ProcessScheme`, which provides methods to find specific transitions: ```csharp var scheme = processInstance.ProcessScheme; // Find a transition by name var approveTransition = scheme.FindTransition("ApproveReview"); // Find all transitions between two activities var transitions = scheme.FindTransitions( fromActivity, toActivity); // Get all transitions that leave the current activity var outgoing = scheme.GetPossibleTransitionsForActivity( currentActivity); ``` Each method returns a `TransitionDefinition` object or a list of them, which you can then inspect for properties like trigger type, conditions, and restrictions. ## See also ## Frequently asked questions **What is a transition in Workflow Engine?** A transition connects two activities in a scheme and defines what causes a process instance to move between them. Every transition has a source activity, a target activity, and a trigger that determines when it fires. **What is the difference between Direct and NotSpecified transitions?** `Direct` and `NotSpecified` are classifier values that describe the direction of a transition, not how it fires. A `Direct` transition is a forward transition in the normal workflow flow. `NotSpecified` means no direction is assigned. How a transition fires (by command, automatically, or by timer) is determined by its `Trigger` property, not the classifier. **What happens if multiple transitions match the same command?** For non-fork transitions, the runtime first selects an `Always` transition, then evaluates action or expression conditions in scheme order and uses the first passing transition. If none passes, it uses an `Otherwise` transition. Fork transitions are handled separately and can create multiple subprocesses. **How do AllowConcatenationType and RestrictConcatenationType work?** For allow restrictions, `And` requires every allow rule to match, while `Or` requires any one. For restrict restrictions, `And` blocks an actor only when every restrict rule matches, while `Or` blocks an actor when any one matches. Both properties default to `And`. --- # Command URL: https://docs.workflowengine.io/get-started/concepts/command/ # Command A command is a named trigger that moves a process instance from its current activity to the next one by firing a matching transition. Commands represent user actions or system events, such as "Submit", "Approve", or "Reject". Commands are declared in a scheme separately from transitions. A transition references a command through its trigger. Executing a command makes the runtime evaluate matching transitions from the current activity. A command is available to an identity when the restrictions on at least one matching transition allow that identity. Restrictions belong to transitions, not to the command itself. A command can also carry input parameters, additional data the caller provides when executing it, such as a comment or a selected value. ## Command properties Each command in the scheme has a set of properties that define its identity and input parameters. ### Core properties Table: Core command properties | Property | Type | Description | |-----------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------| | Name | `string` | Case-sensitive name used to reference the command in transitions and when executing it | | InputParameters | `list` of `ParameterDefinitionReference` | Parameters the command accepts. Each parameter has a name, a linked scheme parameter, and optional default value | | Comment | `string` | Optional description | ### Input parameter properties Each input parameter in `InputParameters` has the following properties: Table: Command input parameter properties | Property | Type | Description | |--------------|-----------------------|-----------------------------------------------------------------------| | Name | `string` | Name used to access and localize the parameter | | Parameter | `ParameterDefinition` | Links to a scheme-level parameter definition that determines the type | | IsRequired | `bool` | Whether a value must be provided before executing | | DefaultValue | `string` | JSON-serialized value applied by the parameter default methods | | Comment | `string` | Optional description | ## How commands connect to transitions in a scheme Commands are declared in the `` section of a scheme. Each `` element defines one named trigger. Transitions reference commands through their trigger element. ```xml filename="DocumentReview.xml" {2-9} ``` !DocumentReview scheme with Submit and Approve commands A command is referenced by a transition's trigger through the `NameRef` attribute. Multiple transitions can reference the same command. When someone executes that command, the runtime finds matching transitions from the current activity and evaluates their conditions to select which transition or transitions to execute. ## Working with commands When you call `GetAvailableCommandsAsync`, the runtime returns a list of `WorkflowCommand` objects. It filters command transitions from the current activities of the process and its child subprocesses by their restrictions for the supplied identity. Use `GetAvailableCommandsWithConditionCheckAsync` when failed transition conditions must also exclude commands from the list. Each `WorkflowCommand` contains the command's resolved parameters, localization, and allowed identities. ```csharp var commands = await runtime.GetAvailableCommandsAsync(processId, identityId); foreach (var command in commands) { // System and localized names string name = command.CommandName; string localizedName = command.LocalizedName; // The activity and state this command is valid from string fromActivity = command.ValidForActivityName; string fromState = command.ValidForStateName; bool isForSubprocess = command.IsForSubprocess; TransitionClassifier classifier = command.Classifier; // Users who can execute this command IEnumerable identities = command.Identities; } ``` ### Command parameters Each `WorkflowCommand` carries a list of `CommandParameter` objects. Every parameter exposes its metadata and current value: Table: WorkflowCommand parameter properties | Property | Description | |-----------------|---------------------------------------------| | `ParameterName` | System name of the parameter | | `LocalizedName` | Localized name of the parameter | | `Type` | CLR type of the parameter value | | `IsRequired` | Whether a value must be provided | | `DefaultValue` | Default value from the scheme | | `Value` | The current value, set before execution | | `IsPersistent` | If true, the value is saved to the database | Set parameter values on the command object before executing it: ```csharp var command = commands.First(); // Set a temporary parameter command.SetParameter("Comment", "Approved"); // Set a persistent parameter (saved to database) command.SetParameter("ApprovedAmount", 1500.00m, persist: true); // Validate required fields and types if (command.Validate(out string errorMessage)) { await runtime.ExecuteCommandAsync(command, identityId, impersonatedIdentityId: null); } ``` ### Implicit parameters A command does not need to declare every parameter in its `` section. `SetParameter` updates a declared command parameter when the name matches. Otherwise, it adds a new parameter with the value's runtime type. The `persist` argument determines whether the value is persistent or temporary; the runtime does not infer this from a same-named scheme parameter. During execution, a declared command parameter name is mapped to its referenced scheme parameter name. An undeclared parameter keeps the name passed to `SetParameter`: ```csharp // TrackingNote is temporary because persist defaults to false. command.SetParameter("TrackingNote", "Routed to manager"); ``` WorkflowRuntime also adds the system parameters `CurrentCommand`, `IdentityId`, and `ImpersonatedIdentityId` during command execution. These parameters do not need to be declared in ``. ## See also ## Frequently asked questions **What is a command in Workflow Engine?** A command is a named trigger defined in the scheme that moves a process instance from one activity to another by firing a matching transition. Commands represent user actions or system events, like "Submit", "Approve", or "Reject". **How does a transition reference a command?** A transition references a command through its nested `` element. Multiple transitions can reference the same command. When that command is executed, the runtime evaluates matching transitions from the current activity. **What happens if a user tries to execute a command they are not allowed to use?** `GetAvailableCommandsAsync` filters command transitions by their restrictions. `ExecuteCommandAsync` does not repeat that check by default, so the application should present only returned commands or use `ExecuteCommandWithRestrictionCheckAsync` when restrictions must be checked again during execution. **How do I set parameter values on a command before executing it?** Use `SetParameter(name, value)` or `SetParameter(name, value, persist: true)` on the `WorkflowCommand` object returned by `GetAvailableCommandsAsync`. Call `SetAllParametersToDefault()` to reset all parameters, then run `Validate()` to check that required fields have values and types match. --- # Scheme URL: https://docs.workflowengine.io/get-started/concepts/scheme/ # Scheme A scheme is a workflow blueprint written in XML that Workflow Engine parses and builds into the `ProcessDefinition` used to create and execute process instances. It declares each activity, transition, command, and user-defined parameter, together with other workflow configuration. You define a scheme once; Workflow Engine creates as many process instances from its stored versions as needed, with each instance tracking its own state independently. ## What a scheme is A scheme is an XML document whose top-level sections describe the building blocks of a workflow. The most common sections are commands, activities, and transitions, but a scheme can also include parameters, timers, code actions, and other elements depending on the process requirements. ```xml filename="DocumentApproval.xml" ``` !DocumentApproval scheme with Submit, Approve, and Reject transitions `Commands` lists the named command triggers in the process. `Activities` lists the workflow steps, marking which is initial and which are final. `Transitions` connects activities and defines their triggers, conditions, and restrictions. In addition to these core sections, a scheme can declare parameters, timers, actors, code actions, localization, and subprocess configuration. A minimal executable scheme can contain a single initial activity; transitions are needed only when the process must move to another activity. ## How a scheme works `SchemeDefinition` carries an unparsed scheme together with its code and stored version metadata. `WorkflowBuilder` looks up an active, non-obsolete version by scheme code. If no such version exists, the configured generator supplies the scheme, the parser creates a `ProcessDefinition`, build steps transform it, and the persistence provider saves the built version under a unique scheme ID. When a process instance is created, it receives the scheme ID and `ProcessDefinition` for that stored version. Execution starts from the definition's initial activity. Multiple process instances can use the same scheme version while keeping their current activities and parameters independent. ## Scheme and process instance A scheme is a workflow blueprint; a process instance is one execution of a stored version of that blueprint. Many process instances can use the same scheme version at the same time, each tracking its own current activity and parameters independently. A process instance does not store a private snapshot of the scheme XML. It stores a `SchemeId` that identifies its built scheme version, and `WorkflowBuilder` loads the corresponding `ProcessDefinition` when the instance is loaded. After stored versions are marked obsolete, new instances use an active non-obsolete version or cause a new version to be generated and built. Existing instances remain linked to their old version until they are updated. Automatic updates require the runtime setting and `IsAutoSchemeUpdate` on the current activity; an explicit update overload can ignore the activity flag. ## See also ## Frequently asked questions **What is a scheme in Workflow Engine?** A scheme is an XML document that serves as the workflow blueprint. `WorkflowBuilder` parses the scheme into a `ProcessDefinition`, applies configured build steps, and stores the built version for process instances. A configured parsed-process cache can reuse that `ProcessDefinition` in memory. **How does Workflow Engine handle scheme updates without breaking running processes?** A process instance keeps the `SchemeId` of the built scheme version it started with. Marking that version obsolete makes new instances use an active version, while running instances can keep the old version or be updated. Automatic updates require `IsAutoSchemeUpdate` on the current activity, but an explicit update can ignore that flag. **Do I have to write XML to create a scheme in Workflow Engine?** No. The visual designer creates scheme XML, and application code can create a `ProcessDefinition` with `ProcessDefinitionBuilder`. A configured generator can also supply the unparsed scheme to `WorkflowBuilder`. --- # Process Instance URL: https://docs.workflowengine.io/get-started/concepts/process-instance/ # Process Instance A process instance is one execution of a scheme. Workflow Engine creates a process instance for a specific scheme, assigns it a unique identifier, positions it at the initial activity, and persists its state to the database. Each process instance can carry its own parameters and business data through the workflow. ## What a process instance is Each process instance that Workflow Engine manages holds three pieces of state: the current activity name, the parameter values attached to this execution, and the transition history. Workflow Engine reads and writes this state through the persistence provider during execution. Transition history is recorded by default and can be disabled per activity. A process instance moves through several lifecycle states that Workflow Engine manages automatically: ```mermaid stateDiagram-v2 Initialized --> Running Running --> Idled Idled --> Running Running --> Finalized Running --> Terminated Running --> Error Error --> Running Finalized --> Running Terminated --> Running ``` Table: Process instance lifecycle states | State | What it means | |-------------|--------------------------------------------------------------------------------| | Initialized | Instance has just been created and positioned at the initial activity | | Running | Instance is being executed | | Idled | Instance is not executing and is waiting for external interaction | | Finalized | Instance has reached a final activity and is not executing | | Terminated | Instance was terminated after an error; commands and timers cannot continue it | | Error | Instance stopped after an error; commands and timers can continue it | A process instance reaches the **Finalized** state when it enters an activity marked as final (`IsFinal="true"`). Until then, the instance cycles between **Idled** (waiting) and **Running** (executing). By default, an unhandled execution error sets **Error** when a command or active timer exit remains; otherwise it sets **Terminated**. The **Finalized**, **Terminated**, and **Error** statuses can all return to **Running** through `SetStateAsync`, activity-setting methods, or `ResumeAsync`. The **Error** status also accepts commands and timers, while **Finalized** and **Terminated** do not. ## How a process instance works Workflow Engine creates a process instance when you call `CreateInstanceAsync` with a scheme code and a unique identifier: ```csharp var processId = Guid.NewGuid(); var createParams = new CreateInstanceParams("InvoiceApproval", processId) .AddPersistentParameter("Amount", 1500.00m) .AddPersistentParameter("OwnerId", "user-42"); await runtime.CreateInstanceAsync(createParams); ``` Once created, the process instance moves from one activity to the next when a transition fires. A transition can be triggered by a command, by a timer, or automatically when the process instance arrives at the activity. Workflow Engine finds the matching transition, evaluates any conditions on it, and runs the configured actions. If the trigger does not apply - for example a command is not available at the current activity, or the rule for that command does not permit the given identity - Workflow Engine does not fire the transition. ## Process instance properties A `ProcessInstance` object exposes properties that reveal the current state, the execution context, and the identity of the user who triggered the transition. These properties are accessible inside actions, conditions, and rules through the `processInstance` parameter. ### Identification properties These properties identify the process, its scheme, and its place in the process tree: Table: Process instance identification properties | Property | Type | Description | |-------------------|----------|-----------------------------------------------------------------------------------------------------------------| | `ProcessId` | `Guid` | Unique identifier of this process instance | | `SchemeId` | `Guid` | Identifier of the scheme version this instance runs on | | `SchemeCode` | `string` | The scheme code (name) that was used to create this instance | | `RootProcessId` | `Guid` | Root process identifier. Equals `ProcessId` for top-level processes; points to the root parent for subprocesses | | `ParentProcessId` | `Guid?` | Parent process identifier. `null` for top-level processes | | `TenantId` | `string` | Optional tenant identifier assigned when the process instance is created | `TenantId` is supplied through `CreateInstanceParams` when the process instance is created and is read-only afterward. In tenant-scoped contexts, Workflow Engine validates it before loading or changing the process instance. ### Current state properties These properties report where the process currently is and where it has been: Table: Process instance current state properties | Property | Type | Description | |----------------------------------|----------------------|--------------------------------------------------------------------| | `CurrentActivityName` | `string` | Name of the activity the process is currently at | | `CurrentActivity` | `ActivityDefinition` | The activity definition object the process is currently at | | `CurrentState` | `string` | Current business state name (from the activity's `State` property) | | `PreviousState` | `string` | Previous business state name recorded before the last transition | | `PreviousStateForDirect` | `string` | Previous state recorded during a Direct-classified transition | | `PreviousStateForReverse` | `string` | Previous state recorded during a Reverse-classified transition | | `PreviousActivityName` | `string` | Name of the previous activity | | `PreviousActivityForDirectName` | `string` | Name of the previous activity during a Direct transition | | `PreviousActivityForReverseName` | `string` | Name of the previous activity during a Reverse transition | ### Execution context properties These properties provide context about what is currently executing and why: Table: Process instance execution context properties | Property | Type | Description | |---------------------------------|------------------------|----------------------------------------------------------------------------------------| | `ExecutedActivity` | `ActivityDefinition` | The activity whose actions are currently being executed | | `ExecutedActivityState` | `string` | The state name of the currently executing activity | | `ExecutedTransition` | `TransitionDefinition` | The transition that is currently being executed | | `CurrentCommand` | `string` | Name of the command that triggered the current transition | | `ExecutedTimer` | `string` | Name of the timer that triggered the current transition | | `IsPreExecution` | `bool` | `true` if the action is running in Pre-Execution mode | | `IdentityIds` | `List` | Identities of users who could potentially execute a transition (Pre-Execution only) | | `IdentityIdsForCurrentActivity` | `List` | Identities who can execute a transition from the current activity (Pre-Execution only) | ### Identity properties These properties track who triggered the current execution: Table: Process instance identity properties | Property | Type | Description | |--------------------------|----------|-------------------------------------------------------------------------------------------| | `IdentityId` | `string` | Identifier of the user who executed the command or set the state | | `ImpersonatedIdentityId` | `string` | Identifier of the impersonated user, if using Impersonation | ### Reading properties from code Inside an action, condition, or rule, access these properties directly from the `processInstance` parameter: ```csharp public async Task ExecuteActionAsync(string name, ProcessInstance processInstance, WorkflowRuntime runtime, string parameter, CancellationToken token) { // Identification var processId = processInstance.ProcessId; var schemeCode = processInstance.SchemeCode; var tenantId = processInstance.TenantId; // Current position var currentActivity = processInstance.CurrentActivityName; var currentState = processInstance.CurrentState; // Execution context var executingActivity = processInstance.ExecutedActivity?.Name; var executingTransition = processInstance.ExecutedTransition?.Name; var triggeredBy = processInstance.ExecutedTimer ?? processInstance.CurrentCommand; // Identity var userId = processInstance.IdentityId; Console.WriteLine( $"Process {processId} ({schemeCode}, tenant {tenantId}): at {currentActivity}," + $" state {currentState}, triggered by {triggeredBy}"); } ``` To read properties from application code outside actions, load the process instance through `WorkflowRuntime`: ```csharp var processInstance = await runtime.GetProcessInstanceAndFillProcessParametersAsync(processId); string activityName = processInstance.CurrentActivityName; string stateName = processInstance.CurrentState; ``` ## Process instance and scheme A scheme is the blueprint; a process instance is an execution based on that blueprint. Workflow Engine creates many process instances from the same scheme, each tracking its own state independently. When the scheme is updated, existing instances continue running against the scheme version they started with, and can be migrated to a new version on demand. ### Accessing the scheme from a process instance Every process instance holds a reference to its scheme definition. Use this to inspect the scheme's activities, transitions, and other elements at runtime: ```csharp // Access the scheme from the process instance var scheme = processInstance.ProcessScheme; // List all activities List activities = scheme.Activities; // Find the initial activity ActivityDefinition initial = scheme.InitialActivity; // Find a specific activity by name ActivityDefinition review = scheme.FindActivity("Review"); // List all transitions List transitions = scheme.Transitions; // Find a specific transition TransitionDefinition transition = scheme.FindTransition("ApproveReview"); ``` ### Accessing current state through WorkflowRuntime Without loading the full process instance, query the current state directly: ```csharp // Quick queries without loading the full instance string activityName = await runtime.GetCurrentActivityNameAsync(processId); string stateName = await runtime.GetCurrentStateNameAsync(processId); ProcessStatus status = await runtime.GetProcessStatusAsync(processId); ``` ## See also ## Frequently asked questions **What is a process instance in Workflow Engine?** A process instance is one execution of a scheme, created by Workflow Engine for a specific business process. Each instance has its own current activity, parameter values, and transition history, and is persisted independently in the database. **How do I create a process instance in Workflow Engine?** Create a process instance by calling `CreateInstanceAsync` with a scheme code and a unique identifier. Workflow Engine positions the new instance at the initial activity defined in the scheme and persists it to the database. You can also provide initial parameter values to set business data at creation time. **Can a process instance move backwards to a previous activity?** A process instance can move backwards only if a transition to that previous activity exists in the scheme. If no transition leads back to the target activity, the instance cannot move there through normal execution. You can, however, set the process instance to a specific state if the target activity is configured to accept direct state changes. **How do I get the current activity of a process instance from application code?** Call `runtime.GetCurrentActivityNameAsync(processId)` for the activity name, or load the full instance with `runtime.GetProcessInstanceAndFillProcessParametersAsync(processId)` and read `processInstance.CurrentActivity` or `processInstance.CurrentState`. **What is the difference between CurrentState and ExecutedActivityState?** `CurrentState` is the last recorded business state. It remains unchanged until the process completes a transition to an activity with a different state. `ExecutedActivityState` is the state of the activity whose actions are currently executing. During action execution, these can differ. --- # Workflow Runtime URL: https://docs.workflowengine.io/get-started/concepts/workflow-runtime/ # Workflow Runtime Workflow Runtime is the main API access point for Workflow Engine and a facade over the structures that execute and manage processes. Application code configures a `WorkflowRuntime` with a workflow builder and persistence provider, then uses it to create and control process instances. ## What Workflow Runtime coordinates Workflow Runtime connects the public process API to the engine components responsible for scheme management, execution, persistence, and background services. Its main responsibilities are: - **Scheme management** - the workflow builder generates, parses, and caches schemes used by process instances. - **Process operations** - the runtime API creates and deletes process instances, returns available commands, executes commands, and reads or changes process state. - **Process execution** - internal execution components evaluate transition conditions, run actions, and move a process to the next activity. - **Persistence** - the configured persistence provider reads and writes process status, parameters, transition history, and timers. - **Background services** - the configured service runner and timer manager restore interrupted processes and execute due timers. For example, when application code executes a command, Workflow Runtime obtains the process instance and its scheme, delegates the transition execution, and uses the persistence provider to save the result. ## Runtime lifecycle Configure Workflow Runtime with `WithBuilder(...)` and `WithPersistenceProvider(...)` before calling `StartAsync()`. Action providers, rule providers, plugins, calendars, logging, and other extensions are optional configuration points. The following code illustrates the full Workflow Runtime lifecycle for direct integration: it accepts database-specific components, starts a single-server runtime, runs application work, and guarantees shutdown. Most ASP.NET Core applications do not need to manage this lifecycle explicitly. The Workflow Engine Web API integration registers Workflow Runtime in the standard .NET dependency injection container and uses the host lifecycle to start and stop it. ```csharp filename="RuntimeLifecycle.cs" using System; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Builder; using OptimaJet.Workflow.Core.Persistence; using OptimaJet.Workflow.Core.Runtime; public static class RuntimeLifecycle { public static async Task RunAsync( IWorkflowBuilder workflowBuilder, IPersistenceProvider persistenceProvider, Func runApplicationAsync) { var runtime = new WorkflowRuntime() .WithBuilder(workflowBuilder) .WithPersistenceProvider(persistenceProvider) .AsSingleServer(); await runtime.StartAsync(); try { await runApplicationAsync(runtime); } finally { await runtime.ShutdownAsync(); } } } ``` `AsSingleServer()` selects the single-server service runner and timer manager. `AsMultiServer()` selects their multi-server implementations. After configuration, `StartAsync()` starts runtime services and compiles global code actions. `ShutdownAsync()` switches off the runtime API, waits for active API calls, and stops timers. Table: Common runtime configuration methods | Method | Role | |-----------------------------------------|-------------------------------------------------------------------------| | `WithBuilder(...)` | Connects the workflow builder and adds the default scheme build steps. | | `WithPersistenceProvider(...)` | Connects and initializes the process persistence provider. | | `WithActionProvider(...)` | Registers an action provider. | | `WithActionProvider(provider, schemes)` | Registers an action provider for specified schemes. | | `WithRuleProvider(...)` | Registers a rule provider for actor checks. | | `WithRuleProvider(provider, schemes)` | Registers a rule provider for specified schemes. | | `WithCalendars(...)` | Registers work calendars by name. | | `SetDefaultCalendar(name)` | Selects one registered work calendar as the default. | | `WithLocalizationProvider(...)` | Registers an external localization provider. | | `WithProcessLogger(...)` | Registers a process log provider. | | `WithExternalParametersProvider(...)` | Registers an external parameter provider. | | `WithPlugin(...)` | Registers a plugin with Workflow Runtime. | | `WithPlugins(schemes, plugins)` | Registers several plugins for specified schemes. | | `AsSingleServer()` | Selects the single-server service runner and timer manager. | | `AsMultiServer()` | Selects the multi-server service runner and timer manager. | | `RegisterAssemblyForCodeActions(...)` | Registers an assembly for code actions and, by default, Designer types. | | `CodeActionsDebugOn()` | Enables code action debug mode. | | `StartAsync()` | Starts runtime services and compiles global code actions. | | `ShutdownAsync()` | Stops the API and timers after active API calls finish. | ## Use the process API Workflow Runtime provides the application-facing operations for a process instance. A common sequence creates the instance, obtains commands available to an identity, and executes one of those commands: ```csharp filename="ProcessOperations.cs" using System; using System.Linq; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; public static class ProcessOperations { public static async Task CreateAndExecuteAsync( WorkflowRuntime runtime, string schemeCode, string identityId) { Guid processId = Guid.NewGuid(); await runtime.CreateInstanceAsync(schemeCode, processId); var commands = await runtime.GetAvailableCommandsAsync(processId, identityId); WorkflowCommand command = commands.FirstOrDefault(); if (command is not null) { await runtime.ExecuteCommandAsync( command, identityId, identityId); } return processId; } } ``` The facade groups related operations on the same `WorkflowRuntime` instance instead of exposing its internal executor, scheme updater, or subprocess manager. Table: Common process methods | Method | Role | |----------------------------------------------------|-----------------------------------------------------------------------------| | `CreateInstanceAsync(schemeCode, processId)` | Creates and initializes a process instance. | | `GetAvailableCommandsAsync(processId, identityId)` | Returns commands available to an identity. | | `ExecuteCommandAsync(command, identityId, ...)` | Executes the selected command. | | `GetProcessStatusAsync(processId)` | Returns the persisted process status. | | `GetCurrentStateNameAsync(processId)` | Returns the current state name. | | `GetCurrentActivityNameAsync(processId)` | Returns the current activity name. | | `SetStateAsync(SetStateParams)` | Sets an allowed target state using the supplied options. | | `SetPersistentProcessParameterAsync(...)` | Stores one persistent process parameter. | | `ResumeAsync(...)` | Continues a process without executing the activity used as the start point. | | `DeleteInstanceAsync(processId)` | Deletes the process instance and its child subprocesses. | For direct state changes and pre-execution, use the dedicated guides. They explain the options and execution rules that do not belong in this introductory concept. ## Observe runtime events Workflow Runtime raises events when process execution or the runtime lifecycle changes. The synchronous events below have asynchronous counterparts with the `Async` suffix. ```csharp filename="RuntimeEvents.cs" using System; using OptimaJet.Workflow.Core.Runtime; public static class RuntimeEvents { public static void Subscribe(WorkflowRuntime runtime) { runtime.OnProcessActivityChanged += (_, args) => { Console.WriteLine( $"{args.ProcessId}: {args.CurrentActivityName}"); }; runtime.OnWorkflowError += (_, args) => { Console.Error.WriteLine( $"{args.ProcessInstance.ProcessId}: {args.Exception.Message}"); }; } } ``` Table: Common runtime events | Event | Raised when | |----------------------------|------------------------------------------------| | `OnWorkflowError` | A workflow execution error occurs. | | `OnProcessActivityChanged` | The current activity of a process changes. | | `OnProcessStatusChanged` | The persisted process status changes. | | `OnSchemaWasChanged` | The scheme of a process changes. | | `OnRuntimeStart` | Workflow Runtime has started. | | `OnRuntimeShutdown` | Workflow Runtime begins shutting down its API. | For all configuration options and API groups, use the WorkflowRuntime guide and Runtime Settings reference. These pages keep detailed API material out of the introductory concepts section. ## Workflow Runtime and scheme A scheme is a process definition that describes activities, transitions, commands, and other workflow elements. Workflow Runtime uses the parsed scheme when it creates a process instance or executes that process. The scheme provides the definition; Workflow Runtime provides the API and coordinates the components that execute it. ## See also ## Frequently asked questions **Is Workflow Runtime a separate service?** No. `WorkflowRuntime` is a class in Workflow Engine Core. Application code creates and configures an instance, then calls its public API directly. **What must be configured before Workflow Runtime starts?** Workflow Runtime requires a workflow builder and a persistence provider before `StartAsync()`. Other providers and plugins are added only when the process definitions or integration require them. **What does starting Workflow Runtime do?** `StartAsync()` starts the configured runtime services and compiles global code actions. Call `ShutdownAsync()` during application shutdown so the runtime can stop accepting API calls, wait for active calls, and stop timers. **Which Workflow Runtime server mode should I use?** Use `AsSingleServer()` for a single-server environment and `AsMultiServer()` for a multi-server environment. Each method configures the matching service runner and timer manager. **How do I react to a workflow execution error?** Subscribe to `OnWorkflowError`. If the handler needs to perform asynchronous work, subscribe to `OnWorkflowErrorAsync` instead. The example in Observe runtime events shows the synchronous event. --- # Persistence URL: https://docs.workflowengine.io/get-started/concepts/persistence/ # Persistence Persistence connects Workflow Engine to a database through separate contracts for runtime process data and workflow scheme data. Workflow Runtime uses `IPersistenceProvider` for process instances, statuses, persisted parameters, timers, transition history, global parameters, and runtime coordination records. The workflow builder uses `ISchemePersistenceProvider` to load and save scheme definitions. Workflow Engine ships providers for SQL Server, PostgreSQL, MySQL, Oracle, MongoDB, and SQLite. ## What Persistence is A persistence provider is a database adapter that implements the storage contracts required by Workflow Engine. `IPersistenceProvider` covers runtime records, while `ISchemePersistenceProvider` covers source and built schemes used by the workflow builder. The built-in providers implement `IWorkflowProvider`, which combines both persistence contracts with XML scheme generation. The persistence contracts cover these relationships: - The workflow builder loads and saves source and built schemes through the scheme persistence contract. - Process instances are initialized, read, updated, and deleted through the runtime persistence contract. - Persisted parameters are loaded and saved per process instance. - Timers are registered, queried, and removed through the runtime persistence contract. - Transition history can be recorded through the provider when history persistence is enabled. ## How Persistence works Each supported database has a separate provider project and NuGet package. You choose the provider that matches the database and configure it for both the workflow builder and Workflow Runtime. The shipped `IWorkflowProvider` implementations support both persistence roles. Relational database schema setup is separate from provider configuration. SQL Server, PostgreSQL, MySQL, Oracle, and SQLite providers integrate with `OptimaJet.Workflow.Migrator`, whose `RunMigrations()` method applies pending migrations. Applications can apply the provider SQL scripts separately instead. Configuring a provider does not run migrations automatically. ## Persistence and Workflow Runtime Workflow Runtime executes and manages workflows, while Persistence stores the data required across runtime calls and application restarts. Runtime operations use `IPersistenceProvider` to initialize, load, update, and delete process data. `WithPersistenceProvider(...)` assigns the runtime provider and initializes it with the current Workflow Runtime. The workflow builder receives its scheme persistence provider separately. A built-in provider object can fill both roles because it implements `IWorkflowProvider`. ## See also ## Frequently asked questions **Which databases does Workflow Engine support?** SQL Server, PostgreSQL, MySQL, Oracle, MongoDB, and SQLite. Each has a separate NuGet provider package. **Can I switch databases after I have running process instances?** Configuring Workflow Runtime and the workflow builder with a different provider changes the database they use but does not transfer existing records. Moving to another database requires migrating the relevant schemes and process data before the application uses the new provider. **Does Workflow Engine create its database tables automatically?** No. Configuring a persistence provider does not create or update relational tables. Run `RunMigrations()` from `OptimaJet.Workflow.Migrator` or apply the provider SQL scripts separately. --- # Action URL: https://docs.workflowengine.io/get-started/concepts/action/ # Action An action is named C# business logic that Workflow Engine executes from an activity implementation while it runs a process instance. Actions connect a scheme to application behavior such as sending emails, updating database records, or calling external services. Workflow Engine uses the `NameRef` value from an action reference to find an action with the same name in an action provider or among Code Actions. ## How actions are defined in the scheme Actions are referenced by name in the scheme XML. The `Implementation` section on an activity lists the actions that Workflow Engine executes during normal activity execution. !DocumentApproval scheme with Submit command and LogActivityEntry action ```xml filename="DocumentApproval.xml" {11-13} ``` Each `ActionRef` identifies an action by `NameRef`. Workflow Engine executes the references by ascending `Order` when it runs the activity's normal `Implementation`. Actions can carry a parameter, configured as a child element of the action reference. The value is a string and often contains JSON for structured data. Use this to pass data to the action method without writing separate action implementations: ```xml reviewers@example.com ``` ## How an action works You can implement an action provider and register it with Workflow Engine at startup. The provider implements `IWorkflowActionProvider`; its execution methods receive the action name, the current process instance, the runtime, and an optional string parameter from the scheme. ```csharp filename="EmailActionProvider.cs" using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; public interface IEmailService { Task SendAsync(string to, string body, CancellationToken token); } public class EmailActionProvider : IWorkflowActionProvider { private readonly IEmailService _email; public EmailActionProvider(IEmailService email) { _email = email; } public async Task ExecuteActionAsync( string name, ProcessInstance process, WorkflowRuntime runtime, string parameter, CancellationToken token) { if (name == "NotifyReviewers") { var reviewers = process.GetParameter("ReviewerEmails"); await _email.SendAsync(reviewers, "Review required", token); } if (name == "SendEmail") { await _email.SendAsync(parameter, "Action required", token); } } public void ExecuteAction( string name, ProcessInstance process, WorkflowRuntime runtime, string parameter) { } public Task ExecuteConditionAsync( string name, ProcessInstance process, WorkflowRuntime runtime, string parameter, CancellationToken token) => Task.FromResult(false); public bool ExecuteCondition( string name, ProcessInstance process, WorkflowRuntime runtime, string parameter) => false; public bool IsActionAsync(string name, string schemeCode) => true; public bool IsConditionAsync(string name, string schemeCode) => false; public List GetActions(string schemeCode, NamesSearchType searchType) => ["NotifyReviewers", "SendEmail"]; public List GetConditions(string schemeCode, NamesSearchType searchType) => new(); } ``` `GetActions` returns the names supplied by the provider. For a matching name, `IsActionAsync` tells Workflow Engine whether to call `ExecuteAction` or `ExecuteActionAsync`. The same interface also contains `GetConditions`, `IsConditionAsync`, `ExecuteCondition`, and `ExecuteConditionAsync` because an action provider can supply conditions as well. Register the provider when configuring Workflow Engine: ```csharp runtime.WithActionProvider(new EmailActionProvider(emailService)); ``` Without a scheme list, the provider is available to every scheme managed by the runtime. You can also call `WithActionProvider` more than once and pass a list of scheme codes to scope a provider. See Call Custom Assembly Code from Actions for a full provider setup. ## When an action runs Normal actions belong to an activity, not to a transition. When a transition passes its conditions, Workflow Engine selects the target activity and executes the actions in that activity's `Implementation`. The initial activity's implementation can also run when a new process starts, without an incoming transition. An activity can separately define `PreExecutionImplementation`. During pre-execution simulation, Workflow Engine executes that list instead of the normal `Implementation`. Both lists contain ordered action references, and neither makes an action free of external side effects. ## Code Actions as a scheme-specific alternative In addition to implementing an action provider, you can define C# actions directly in the scheme's `CodeActions` section. A local Code Action belongs to that scheme and can be referenced by name from an activity implementation. Code Action and expression compilation are enabled by default. Disable both when the application must not execute scheme-defined C# code: ```csharp runtime.DisableCodeActions(); ``` Call `EnableCodeActions()` to enable both compilers again. Workflow Engine compiles Code Actions at runtime. Register an application assembly only when a Code Action needs types from that assembly; the framework-agnostic setup guide shows that configuration. Code Actions run with access to the host process, so only trusted users should be able to edit them. Marking a Code Action as global stores it in Workflow Engine global parameters instead of one scheme and makes it available across schemes. ### Search priority for actions The runtime can resolve an action name from a local Code Action, a global Code Action, or a registered provider. The default `LocalGlobalProvider` order checks those sources in that sequence. `SetExecutionSearchOrder` can select any of the six orders defined by `ExecutionSearchOrder` when duplicate names require a different priority. ## See also ## Frequently asked questions **What is an action in Workflow Engine?** An action is named C# business logic referenced from an activity implementation. Workflow Engine resolves the name to a provider action or Code Action and executes it when it runs that activity. **How do I register an action provider with Workflow Engine?** Implement the `IWorkflowActionProvider` interface and register an instance via `runtime.WithActionProvider(provider)` in the builder chain at startup. Route by action name inside the implementation. **Can I have multiple action providers for different schemes?** Yes. Call `runtime.WithActionProvider(provider, [schemeCode1, schemeCode2])` to register a provider for specific schemes. A provider registered without a scheme list is considered for every scheme. **How does the runtime choose which action to use when the same name exists in multiple places?** The runtime searches local Code Actions, global Code Actions, and the registered provider in `LocalGlobalProvider` order by default. Use `runtime.SetExecutionSearchOrder()` to select a different order. --- # Parameter URL: https://docs.workflowengine.io/get-started/concepts/parameter/ # Parameter A parameter is a variable that belongs to a process instance: a typed key-value pair that WorkflowRuntime manages throughout the process lifecycle. Parameters carry business data - document IDs, assignee names, approval decisions, computed values. Actions read and write parameters; conditions evaluate them; the scheme can use them to route transitions. Think of a parameter as a labeled field on a paper form that moves from desk to desk. When someone submits a vacation request, the `ApproverName` parameter stores who must sign it. When an invoice reaches the Accounting step, `InvoiceAmount` holds the amount to pay. Each step reads and updates these fields - they travel with the process from start to finish. ## What a parameter is A parameter is a named value that a process instance owns. Its name is a string - for example, `ApproverName` or `DocumentId` - and its value has a .NET type such as `int`, `string`, or `DateTime`. The process reads it, writes it, and passes it between steps. `WorkflowRuntime` keeps parameters in memory while the process executes and saves only `Persistence` parameters through the configured persistence provider. Inside actions, you read and write them using `GetParameter` and `SetParameter`. ```csharp filename="Reading and writing a parameter inside an action" // Read a parameter set by a previous action string assignee = processInstance.GetParameter("ApproverName"); int docId = processInstance.GetParameter("DocumentId"); // Write a value for the next step processInstance.SetParameter("Decision", "Approved"); ``` You can also pass initial parameters when creating the instance: ```csharp filename="Creating a process instance with initial parameters" var createInstanceParams = new CreateInstanceParams( schemeCode: "DocumentApproval", processId: Guid.NewGuid() ); createInstanceParams.AddPersistentParameter("DocumentId", 1042); createInstanceParams.AddPersistentParameter("OwnerId", "user-42"); await runtime.CreateInstanceAsync(createInstanceParams); ``` The values are available during instance creation. `Persistence` values are saved through the configured persistence provider. `ParameterPurpose` controls how Workflow Engine handles a parameter: Table: Parameter purpose types | Purpose | Behavior | |---------------|----------------------------------------------------------------------| | `Persistence` | Saved through the persistence provider and available in later runs | | `Temporary` | Kept in memory until the current process execution finishes | | `System` | Maintained by Workflow Engine from process and execution information | Use `Persistence` for data needed after the process becomes idle. Use `Temporary` for values needed only by the execution started by a creation, command, or state change. Parameters are also classified by how they are declared: Table: Parameter definition types | Classification | Description | |----------------|-------------------------------------------------------------------------| | `Explicit` | Declared in the scheme with a known type, purpose, and optional default | | `Implicit` | Created at runtime when a value is first passed or set through code | Most parameters are implicit. You pass them through `SetParameter`, `AddPersistentParameter`, or `AddTemporaryParameter` without declaring them in the scheme first. The engine creates the parameter automatically when the first value arrives. Explicit parameters are declared in the scheme ahead of time with a type, purpose, and optional default value. They are rarely needed and can be useful for auto-generated forms. Only a persistent explicit parameter can have an initial value. ## How a parameter works When an execution starts, `WorkflowRuntime` loads the process instance and fills its persistence parameters from the database into memory. Actions and conditions run against this in-memory copy - `GetParameter` and `SetParameter` calls do not read or write the database directly. After the current execution finishes successfully, `WorkflowRuntime` saves changed `Persistence` parameters. `Temporary` parameters are not saved and are unavailable when the process starts its next execution. The parameter lifecycle for a successful process execution is: You can also pass them with `WorkflowCommand.SetParameter` or `SetStateParams`. A command parameter is temporary by default; pass `persist: true` when the value must be saved. The command execution guide shows the complete flow. ```csharp filename="Passing parameters with a command" var command = (await runtime.GetAvailableCommandsAsync(processId, identityId)).First(); command.SetParameter("Comment", "Needs review"); command.SetParameter("ApprovedAmount", 1500.00m, persist: true); await runtime.ExecuteCommandAsync(command, identityId, identityId); ``` `Comment` is available only during this command execution. `ApprovedAmount` is saved for later executions. The example passes the same identity twice because it does not use impersonation. Calling `SetParameter` without a purpose creates a missing parameter as `Temporary`. Passing `ParameterPurpose.Persistence` creates a persistent implicit parameter. If an existing parameter or its scheme definition is already `Persistence`, setting it without the purpose does not downgrade it to `Temporary`. ## Parameter and process instance A process instance is the owner of every process parameter. Actions and conditions attached to the same instance read and write the same parameter set. Two instances created from the same scheme have separate parameter values. `WorkflowRuntime` also maintains system parameters from process and execution information. Examples include `ProcessId`, `SchemeCode`, `CurrentState`, `CurrentActivity`, `IdentityId`, and `ExecutedTransition`. They are available inside actions and conditions without being declared explicitly in the scheme. ## Parameters in conditions Scheme conditions can read process parameters through a code action defined in the scheme. The code action runs during transition evaluation and returns `true` or `false`: ```csharp filename="Reading a parameter in a condition" return processInstance.GetParameter("IsUrgent"); ``` Here, the condition reads `IsUrgent` from the process instance. The scheme connects the condition to a transition. An `Expression` condition can read the same parameter directly with the `@` prefix. For example, `@IsUrgent` returns the Boolean value of the `IsUrgent` parameter without a separate code action. For syntax and examples, see Condition. ## Advanced parameter scenarios Parameters support advanced scenarios when the type is unknown at design time, the value is nested inside a complex object, or the data lives outside the workflow database. When the parameter type is not known at design time, read it as `DynamicParameter` and access its properties by name: ```csharp filename="Reading a dynamic parameter" var document = processInstance.GetParameter("Document"); string title = (string)document["Title"]; ``` The same parameter can be read through a C# `dynamic` property: ```csharp filename="Reading a DynamicParameter through dynamic" dynamic document = processInstance.GetParameter("Document"); string title = document.Title; ``` For parameters that hold multi-level objects, dot notation reads or updates one property without replacing the whole object: ```csharp filename="Reading and updating a partial parameter" decimal amount = processInstance.GetParameter("Document.Amount"); processInstance.SetParameter("Document.Amount", amount + 100m); ``` An external parameter is stored outside Workflow Engine but is accessed through the normal `ProcessInstance` parameter API. An `IWorkflowExternalParametersProvider` tells the engine which names it handles and supplies synchronous or asynchronous getters and setters. This minimal provider handles names that start with `External_` and stores values in memory by process ID. A production provider would use the application's data store instead: ```csharp filename="External parameter provider" using System; using System.Collections.Concurrent; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; public sealed class InMemoryExternalParameterProvider : IWorkflowExternalParametersProvider { private readonly ConcurrentDictionary<(Guid ProcessId, string Name), object> _values = new(); public bool HasExternalParameter( string parameterName, string schemeCode, ProcessInstance processInstance) { return parameterName.StartsWith("External_", StringComparison.Ordinal); } public bool IsGetExternalParameterAsync( string parameterName, string schemeCode, ProcessInstance processInstance) { return true; } public bool IsSetExternalParameterAsync( string parameterName, string schemeCode, ProcessInstance processInstance) { return true; } public object GetExternalParameter( string parameterName, ProcessInstance processInstance) { _values.TryGetValue((processInstance.ProcessId, parameterName), out object value); return value; } public Task GetExternalParameterAsync( string parameterName, ProcessInstance processInstance) { return Task.FromResult(GetExternalParameter(parameterName, processInstance)); } public void SetExternalParameter( string parameterName, object parameterValue, ProcessInstance processInstance) { _values[(processInstance.ProcessId, parameterName)] = parameterValue; } public Task SetExternalParameterAsync( string parameterName, object parameterValue, ProcessInstance processInstance) { SetExternalParameter(parameterName, parameterValue, processInstance); return Task.CompletedTask; } } ``` Register the provider during runtime setup: ```csharp filename="External parameter provider registration" runtime.WithExternalParametersProvider(new InMemoryExternalParameterProvider()); ``` After registration, workflow code reads `External_Customer` through the normal parameter API. The value remains in the provider's data store rather than being saved as a process parameter: ```csharp filename="Reading an external parameter" dynamic customer = await processInstance.GetParameterAsync("External_Customer"); string name = customer.Name; ``` Dynamic, partial, and external parameters are advanced integration options. They do not change the basic choice between temporary execution data and persistence data owned by the process instance. ## See also ## Frequently asked questions **What is a parameter in Workflow Engine?** A parameter is a variable that belongs to a process instance - a typed key-value pair that `WorkflowRuntime` manages throughout the process lifecycle. Actions read and write parameters; conditions evaluate them to route transitions. **What is the difference between implicit and explicit parameters in Workflow Engine?** An implicit parameter has no scheme definition and is created when code first passes or sets its value. An explicit parameter is declared in the scheme with its CLR type and purpose; a persistent explicit parameter can also define an initial value. **How do I pass initial data to a process instance in Workflow Engine?** Add values to `CreateInstanceParams` with `AddPersistentParameter` or `AddTemporaryParameter` before calling `CreateInstanceAsync`. The values are available during instance creation, and persistent values are saved through the configured provider. **What is the difference between Temporary and Persistence parameters?** A `Temporary` parameter is available only during the current process execution and is not saved. A `Persistence` parameter is saved through the persistence provider and can be loaded for later executions. **What is an external parameter in Workflow Engine?** An external parameter is read and written through an `IWorkflowExternalParametersProvider`. Its value stays in an external data store while workflow code uses the normal `ProcessInstance` parameter API. --- # Condition URL: https://docs.workflowengine.io/get-started/concepts/condition/ # Condition A condition is a Boolean guard evaluated before Workflow Engine selects and fires a transition. It determines whether a process instance can move from the current activity to the transition's target activity. Use an `Always` condition when a transition must be unconditional. ## What a condition is A condition is a rule attached to a transition. When a command, timer, or auto trigger activates several outgoing transition candidates, the runtime checks their conditions. A failed condition skips that transition; another candidate can still pass. The process stays at the current activity only when no candidate is selected. A transition can use one `Always` or `Otherwise` condition, or one or more `Action` and `Expression` conditions. `ConditionsConcatenationType` combines multiple conditions on the same transition with `And` or `Or` logic. The simplest condition type is `Always`, which always returns `true`. A transition with an `Always` condition fires whenever its trigger activates, without any checks. The most flexible type is an expression condition - a C# expression that the runtime compiles and evaluates at the moment the transition fires (described below in the types table). Several outgoing transitions from any activity can form an ordered conditional choice. For candidates activated by the same trigger, Workflow Engine checks an `Always` transition first, then checks `Action` and `Expression` transitions in scheme order, and uses an `Otherwise` transition only if none of the earlier candidates is selected. This behaves like an `if` / `else if` / `else` branch without requiring a separate gateway element. ## How a condition works When a trigger activates a transition, `WorkflowRuntime` evaluates its conditions using the transition's `ConditionsConcatenationType`. If the combined result passes, the transition fires. Otherwise, the runtime continues with another outgoing candidate. The scheme below defines a single transition from `Review` to `Approved`. The transition has an expression condition that checks whether the `Amount` parameter exceeds 1000: !Scheme diagram showing Review transitioning to Approved with an expression condition ```xml filename="ConditionExample.xml" {21-25} @Amount > 1000 ``` When the user executes the `Approve` command on the `Review` activity, the runtime evaluates the expression `@Amount > 1000`. The `@Amount` is replaced with the parameter's value at evaluation time. If the `Amount` parameter is greater than 1000, the condition passes and the process moves to `Approved`. Otherwise, the transition is blocked. ## Condition types Conditions are classified by how they determine the result: Table: Condition types | Type | Description | Supports inversion | Priority | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------|--------------------|----------| | `Always` | Selects the transition without a Boolean check when its trigger matches. | No | Highest | | `Expression` | Evaluates a compiled C# expression with `@`-syntax for parameter access. | Yes | Normal | | `Action` | Calls a named condition from a Code Action or `IWorkflowActionProvider`; the provider can also supply actions. | Yes | Normal | | `Otherwise` | Selects the fallback after no `Always`, `Action`, or `Expression` candidate passes. | No | Lowest | A condition of type `Expression` stores a C# expression as a string. Workflow Engine compiles expressions when it prepares the process definition and reuses the compiled delegates. An `Action` condition resolves a named implementation from a Code Action or a registered provider. ### Inverting a condition result By default, a condition allows the transition when it returns `true`. You can invert this behavior: when inversion is turned on, a condition that would return `true` now blocks the transition, and a condition that would return `false` now allows it. Use inversion when you have a condition method that checks the opposite of what you need. Instead of writing two separate conditions, you write one and invert it on the transition that needs the opposite check. ```xml ``` Inversion works with both `Action` and `Expression` condition types. ### Pre-execution mode Pre-execution is Workflow Engine's simulation mode, which predicts a path through the scheme without executing the real transition flow. During simulation, `ProcessInstance.IsPreExecution` is `true`, and the runtime evaluates transition conditions while using activity pre-execution implementations. `ResultOnPreExecution` supplies a fixed condition result during simulation. Outside pre-execution, the `Action` or `Expression` condition evaluates normally. ```xml @Amount > 10000 ``` In the example above, simulation treats the condition as `true`, while normal execution still evaluates whether `@Amount > 10000`. ## Expression syntax Expression conditions use a special syntax for accessing process parameters. Any reference to a parameter starts with the `@` character: Table: Expression condition syntax | Syntax | Description | Example | |---------------------------|------------------------------------------------------------------------------------------|---------------------------------------------| | `@ParameterName` | Substitutes the parameter value. The type matches the parameter type. | `@Amount > 100` | | `@Object.Property` | Accesses a nested property of a complex parameter (dot notation). | `@Document.Amount > 100` | | `@(Parameter)` | Wraps the parameter in parentheses to call methods on it. | `@(Document.CreationDate).ToString("yyyy")` | | `@ParameterName:format` | Applies a .NET format string to the parameter value using its `ToString(format)` method. | `@CreationDate:yyyy-MM-dd` | | `@(ParameterName:format)` | Same format string, but with parentheses for complex expressions. | `@(Amount:N2)` | | `NOT @Parameter` | Negates a boolean parameter. | `NOT @Document.IsSigned` | The engine transforms the `@`-syntax into a compiled C# lambda: ```csharp // Expression: @Amount > 1000 // Compiled to: async (processInstance) => (await processInstance.GetParameterAsync("Amount")) > 1000 // Expression: @Document.Amount > 100 AND @Document.CreationDate <= DateTime.Now.AddDays(-1) // Compiled to: async (processInstance) => (await processInstance.GetParameterAsync("Document.Amount")) > 100 && (await processInstance.GetParameterAsync( "Document.CreationDate")) <= DateTime.Now.AddDays(-1) ``` The expression supports C# operators and method calls: - Comparison: `=`, `==`, `<>`, `!=`, `>`, `<`, `>=`, `<=` - Logic: `AND`, `OR`, `NOT`, `&&`, `||`, `!` - Arithmetic: `+`, `-`, `*`, `/` - Method calls: `DateTime.Now`, `string.IsNullOrEmpty(@Name)`, `@(Amount).ToString()` Workflow Engine compiles the lambda while preparing the process definition and reuses it for subsequent evaluations of that scheme. ## Defining action conditions An `Action` condition references a name implemented by a Code Action or an `IWorkflowActionProvider`. The provider exposes four condition-specific methods: Table: Action provider condition methods | Method | Role | |-------------------------|-------------------------------------------------------------| | `GetConditions` | Returns condition names for a scheme and `NamesSearchType`. | | `IsConditionAsync` | Selects the synchronous or asynchronous execution method. | | `ExecuteCondition` | Runs a synchronous condition and returns `bool`. | | `ExecuteConditionAsync` | Runs an asynchronous condition and returns `Task`. | Register the provider with `WorkflowRuntime.WithActionProvider`. The `schemeCode` argument to `GetConditions` lets one provider expose different names per scheme. The Designer asks for `All` and `NotExcluded` names, while runtime execution resolves conditions from `All`. This example exposes one synchronous `IsHighValue` condition: ```csharp filename="OrderConditionProvider.cs" using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; var runtime = new WorkflowRuntime() .WithActionProvider(new OrderConditionProvider()); public sealed class OrderConditionProvider : IWorkflowActionProvider { public List GetConditions( string schemeCode, NamesSearchType namesSearchType) { return new List { "IsHighValue" }; } public bool IsConditionAsync(string name, string schemeCode) { return false; } public bool ExecuteCondition( string name, ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter) { return name switch { "IsHighValue" => processInstance.GetParameter("Amount") > 10000m, _ => throw new NotImplementedException( $"Condition '{name}' is not registered.") }; } public Task ExecuteConditionAsync( string name, ProcessInstance processInstance, WorkflowRuntime runtime, string actionParameter, CancellationToken token) { return Task.FromResult(false); } // Action-specific IWorkflowActionProvider members are omitted from this example. } ``` Alternatively, define condition code directly in the scheme's `` section. Use `Type="Condition"` and set `IsAsync` to `true` or `false`: ```xml ("Amount"); return amount > 10000; ]]> ``` Reference either implementation from an `Action`-type condition: ```xml ``` The `Invert` and `ResultOnPreExecution` attributes belong to the condition reference and work whether its implementation comes from a provider or a Code Action. ## Condition and transition A condition is always attached to a transition. The transition property `ConditionsConcatenationType` specifies how multiple `Action` and `Expression` conditions are combined. In XML, set `ConditionsConcatenationType="And"` or `ConditionsConcatenationType="Or"` on the `` element. When a transition has several conditions, they can work in two ways: - **All conditions must pass**: every condition must return `true` for the transition to fire. If one fails, the transition is blocked. - **Any one condition is sufficient**: the transition fires as soon as one condition returns `true`. The rest are skipped. This setting combines conditions within one transition; it does not combine separate outgoing transitions. Use an `Always` condition for an unconditional transition rather than leaving the condition list empty. ## See also ## Frequently asked questions **What is the difference between an Always condition and an Otherwise condition?** `Always` selects its transition first when the trigger matches. `Otherwise` is the fallback used only when no `Always`, `Action`, or `Expression` transition is selected. **Can a transition have multiple conditions?** Yes. Set the transition's `ConditionsConcatenationType` to `And` when all conditions must pass, or `Or` when any one condition is sufficient. **Can I write a condition as a C# expression without registering an action provider?** Yes. Use an expression condition. Workflow Engine compiles the expression when it prepares the process definition and reuses it without an external provider. **What does the Invert attribute do on a condition?** `Invert="true"` flips the condition result. A condition that normally returns `true` to allow the transition now returns `false` to block it, and vice versa. This is useful when you want to reuse a condition method for the opposite check. **How do I format a parameter value inside an expression?** Use the colon syntax: `@ParameterName:format`. For example, `@CreationDate:yyyy-MM-dd` formats a date parameter. The format string follows .NET format conventions and is passed to the parameter's `ToString(format)` method. --- # Rule URL: https://docs.workflowengine.io/get-started/concepts/rule/ # Rule A rule is a named check that WorkflowRuntime evaluates to decide whether a specific user is allowed to execute a command at the current activity. Rules control who can move a process forward - only users who pass the rule check can proceed to the next step. ## What a rule is A rule is a permission check used by a transition restriction. The restriction references an actor, and the actor stores the rule name and an optional value. The rule's `Check` or `CheckAsync` method receives an identity and returns `true` (allowed) or `false` (blocked). Rule implementations can also provide `GetIdentities` or `GetIdentitiesAsync` to return the identities that match the rule. ## How a rule works Actors are declared in the scheme's `` section. Each actor references a rule by name. Restrictions on transitions reference these actors, and the runtime resolves the rule through the registered provider or the scheme's `CodeActions`. The scheme below defines two actors (`IsApprover` and `IsOwner`) and restricts the `Approve` transition to the `IsApprover` actor: !Scheme diagram showing Review transitioning to Approved with an actor restriction ```xml filename="RuleExample.xml" {28-30} ``` When `GetAvailableCommandsAsync` is called for `"user-42"`, `WorkflowRuntime` loads the scheme, finds the `Approve` transition, reads its restrictions, resolves the `IsApprover` actor, and calls the `IsApprover` rule method with `"user-42"` as the identity. If the rule returns `false`, the `Approve` command is excluded from the result. ## Rule and actor An **Actor** is the link between a scheme restriction and a rule implementation. Each actor in the scheme has three parts: - **Name**: a unique identifier used in `` - **Rule**: the rule name passed to `CheckAsync` and `GetIdentitiesAsync` as the `ruleName` parameter - **Value**: an optional string parameter passed to both methods as the `parameter` argument Multiple transitions can reference the same actor. Multiple actors can reference the same rule with different value parameters. ### Restrictions and their combination A transition can have multiple restrictions. Each restriction has a type (`Allow` or `Restrict`) and references an actor. The `AllowConcatenationType` and `RestrictConcatenationType` attributes on the transition control how restrictions of the same type are combined. The runtime combines the `Allow` results, combines the `Restrict` results, and requires both parts to pass. The table describes the result for one identity checked through `Check` or `CheckAsync`: Table: Restriction combination rules | Allow combination | Restrict combination | Effect | |-------------------|----------------------|-----------------------------------------------------------------------------------------------| | - | - | The identity is allowed. | | `And` (default) | - | The identity must match all `Allow` restrictions. | | `Or` | - | The identity must match at least one `Allow` restriction. | | - | `And` (default) | The identity must not match all `Restrict` restrictions. | | - | `Or` | The identity must match none of the `Restrict` restrictions. | | `And` | `And` | The identity must match all `Allow` restrictions and not all `Restrict` restrictions. | | `And` | `Or` | The identity must match all `Allow` restrictions and no `Restrict` restrictions. | | `Or` | `And` | The identity must match at least one `Allow` restriction and not all `Restrict` restrictions. | | `Or` | `Or` | The identity must match at least one `Allow` restriction and no `Restrict` restrictions. | Actor-list APIs start with the identities returned for `Allow` restrictions. Consequently, a transition with only `Restrict` restrictions can validate an identity supplied to `GetAvailableCommandsAsync`, but `GetAllActorsForCommandTransitionsAsync` returns no identities for it because the API derives its candidates only from `Allow` restrictions. ## Defining rules Rules can be defined in two places. Defining them inside the scheme via CodeActions keeps the logic local to that process. Defining them through `IWorkflowRuleProvider` shares the logic across multiple schemes. ### Defining rules in the scheme with CodeActions Rules can be defined directly in the scheme's `` section. Use `Type="RuleGet"` for a method that returns the list of permitted identity IDs, or `Type="RuleCheck"` for a method that checks a single identity: ```xml ("Department"); if (department == "Engineering") return new[] { "alice", "bob" }; return new[] { "charlie" }; ]]> ``` If you define only `RuleGet`, the runtime generates a `RuleCheck` that compares the supplied `identityId` with the returned list. If you define only `RuleCheck`, the generated `RuleGet` returns an empty list. If you define both, the runtime uses each implementation for its corresponding operation. ### Implementing rules through IWorkflowRuleProvider For rules shared across schemes or backed by external services, implement `IWorkflowRuleProvider` and register it with `WithRuleProvider`. Its `Check` methods validate a supplied identity, while its `GetIdentities` methods return identities for actor-list APIs. `GetRules` reports the supported rule names, and the `IsCheckAsync` and `IsGetIdentitiesAsync` flags select the synchronous or asynchronous implementations. ```csharp filename="Owner rule provider" using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; public sealed class OwnerRuleProvider : IWorkflowRuleProvider { public List GetRules( string schemeCode, NamesSearchType namesSearchType) => ["IsOwner"]; public bool Check( ProcessInstance processInstance, WorkflowRuntime runtime, string identityId, string ruleName, string parameter) => processInstance.GetParameter("OwnerId") == identityId; public Task CheckAsync( ProcessInstance processInstance, WorkflowRuntime runtime, string identityId, string ruleName, string parameter, CancellationToken token) => Task.FromResult(false); public IEnumerable GetIdentities( ProcessInstance processInstance, WorkflowRuntime runtime, string ruleName, string parameter) => [processInstance.GetParameter("OwnerId")]; public Task> GetIdentitiesAsync( ProcessInstance processInstance, WorkflowRuntime runtime, string ruleName, string parameter, CancellationToken token) => Task.FromResult>([]); public bool IsCheckAsync(string ruleName, string schemeCode) => false; public bool IsGetIdentitiesAsync(string ruleName, string schemeCode) => false; } ``` Register the provider with `runtime.WithRuleProvider(new OwnerRuleProvider())`. If several providers or plugins are registered, `WorkflowRuntime` combines them through `AggregatingRuleProvider` and selects the provider that reports the requested rule name for the current scheme. For an implementation example, see Get available actors. ## Impersonation and delegated execution Impersonation allows one identity to execute a command on behalf of another. `GetAvailableCommandsAsync` can check several supplied identities and records the matching ones in `WorkflowCommand.Identities`. During `ExecuteCommandAsync`, restrictions are checked against the impersonated identity. Transition history stores the real identity as `ExecutorIdentityId` and the impersonated identity as `ActorIdentityId`. See Execute commands for the call pattern and identity parameters. ```csharp filename="Executing a command with impersonation" using System; using System.Linq; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Runtime; public static class DelegatedExecution { public static async Task ExecuteAsync( WorkflowRuntime runtime, Guid processId, string commandName, string identityId, string impersonatedIdentityId) { var commands = await runtime.GetAvailableCommandsAsync( processId, [identityId, impersonatedIdentityId]); var command = commands.First( item => item.CommandName == commandName && item.Identities.Contains(impersonatedIdentityId)); await runtime.ExecuteCommandAsync( command, identityId, impersonatedIdentityId); } } ``` ## Accessing identity information inside actions During command execution, `ProcessInstance.IdentityId` identifies the user who called `ExecuteCommandAsync`. `ProcessInstance.ImpersonatedIdentityId` identifies the user on whose behalf the command runs. Actions can read both system parameters: ```csharp filename="Reading identities in an action" using System.Threading; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; public static class IdentityActions { public static Task ExecuteActionAsync( string name, ProcessInstance processInstance, WorkflowRuntime runtime, string parameter, CancellationToken token) { var actualUser = processInstance.IdentityId; var onBehalfOf = processInstance.ImpersonatedIdentityId; return Task.CompletedTask; } } ``` ## Search priority for rules When the same rule name exists in several places, `WorkflowRuntime` needs to decide which one to use. It looks for rules in three places: 1. **Local Code Actions** - rules defined inside the current scheme 2. **Global Code Actions** - rules marked as `IsGlobal="true"`, shared across all schemes 3. **Registered provider** - your `IWorkflowRuleProvider` class external to the scheme By default, the runtime checks these sources in that order: the scheme's own rules first, then global rules, then the provider. If a rule named `IsApprover` exists in both the scheme and the provider, the scheme's version wins by default. This order is configurable with `SetExecutionSearchOrder`, which accepts the six orders defined by `ExecutionSearchOrder`. This matches the same priority system used for actions. ## See also ## Frequently asked questions **What is a rule in Workflow Engine?** A rule is a named check that WorkflowRuntime evaluates to decide whether a specific identity is permitted to execute a command at the current activity. Rules are defined as Code Actions or implemented in an `IWorkflowRuleProvider`, and actors reference them in the scheme. **How do I restrict a command to certain users?** Define an actor with the rule name and add a `Restriction` to the transition that references the actor. Implement the rule as a Code Action or in `Check` or `CheckAsync` of an `IWorkflowRuleProvider`. The runtime then filters the command list. **What is the difference between a rule and a condition?** A rule checks who can execute a command (permission). A condition checks whether the process state allows the transition to fire (business logic). `GetAvailableCommandsAsync` checks rules, while conditions are checked during execution. Use `GetAvailableCommandsWithConditionCheckAsync` when command discovery must also check conditions. **How do I get a list of all users who can execute the next command?** Call `runtime.GetAllActorsForCommandTransitionsAsync(processId)` to build the result from identity lists returned for `Allow` restrictions on outgoing command transitions. This is useful for building notification lists or inbox views. Filter by classifier to limit results to only direct or only reverse transitions. See Get available actors for the API examples. --- # Timer URL: https://docs.workflowengine.io/get-started/concepts/timer/ # Timer A timer fires a transition automatically when its persisted execution time becomes due. Developers use timers to model deadlines, timeouts, reminders, and scheduled tasks in their workflow processes. This page covers timer types, registration, execution, and calendar integration. ## What a timer is A timer is a named schedule defined in the scheme. Unlike a command, which requires a user or system to call `ExecuteCommandAsync`, a timer is processed by Workflow Runtime. The runtime calculates and stores its execution time, and the timer manager polls for due records. A transition can have at most one timer. Multiple transitions can reference the same timer. Each timer has a name and three configuration fields: Table: Timer field properties | Field | Description | |-------------------------|------------------------------------------------------------------------------------------------| | **Name** | Unique identifier used by transitions to reference this timer via `NameRef` | | **Type** | Determines how the runtime interprets the schedule value | | **Value** | The schedule itself: a duration, time of day, date, or expression | | **NotOverrideIfExists** | Keeps the execution time when a timer with the same name is already registered for the process | The five timer types are: Table: Timer types | Timer type | What it means | |---------------|----------------------------------------------------------------------------| | `Interval` | Fire after a delay, such as `"2d 5h 24m"` or `3600000` milliseconds | | `Time` | Fire at a time of day (`HH:mm:ss`); registration selects today or tomorrow | | `Date` | Fire at midnight on a specific calendar date | | `DateAndTime` | Fire at an exact date and time | | `Expression` | Evaluate a C# expression that returns a date, time span, or interval | ## How timers work in a scheme Timers are declared in the `` section of a scheme XML. Each `` element defines one named schedule. Transitions reference timers through their trigger element. The following scheme models a service desk ticket. Two timers enforce different kinds of deadlines: an **Interval** timer fires four hours after the ticket enters the `InProgress` activity (SLA breach), and an **Expression** timer fires at midnight on January 1st of the following year, computed dynamically. ```xml filename="ServiceDeskTicket.xml" {2-6,46,55} ``` !ServiceDeskTicket scheme showing two timer-triggered transitions: an Interval SLA timer from InProgress to Breached, and an Expression deadline timer from Open to Breached Two transitions are timer-triggered: - The `DeadlineBreach` transition from `Open` to `Breached` is triggered by `HardDeadline`. The runtime registers this timer as soon as the process instance enters the `Open` activity. If the ticket is still in `Open` when the deadline passes, the timer fires and the process moves to `Breached`. - The `SLABreach` transition from `InProgress` to `Breached` is triggered by `SLA`. The runtime registers this timer when the ticket is assigned and enters `InProgress`. If no one resolves the ticket within four hours, the timer fires and the process moves to `Breached`. The `HardDeadline` timer is registered when the process first enters `Open` (the initial activity). If the ticket is assigned before the deadline, the `Assign` command moves it to `InProgress`. The process then clears the `Open`-activity timers and registers the `InProgress` -activity timers (`SLA`). A scheme can declare any number of timers. Each timer is defined once and can be referenced by multiple transitions. ## How timers connect to the runtime A timer relates a transition to the process instance's timeline. ### Timer registration When a process instance enters an activity, `WorkflowRuntime` registers timers associated with outgoing timer-triggered transitions. The sequence is: 1. Find every timer-triggered transition from the current activity. 2. Clear previous timers, except same-name timers preserved by `NotOverrideIfExists`. 3. Calculate the execution time from the timer type and process context. 4. Register the timer in the persistence store. An overridable same-name timer is updated. ### Timer execution The runtime's `TimerManager` periodically queries the persistence store for due timers. For each timer, it loads the process instance and evaluates conditions on transitions from the current activity that reference the timer name: - If a condition passes, the transition fires and the process moves to the target activity. The new activity triggers a fresh registration cycle. - If no condition passes, the normal clear-and-register cycle runs for the current activity. With the default `NotOverrideIfExists="false"`, this replaces the fired timer and calculates its execution time again. ### Recovery after downtime Timer records remain in persistence while the runtime is stopped. After `WorkflowRuntime.StartAsync()` starts the timer manager, a poll selects records whose execution time has passed. Single-server mode polls immediately on startup; multi-server mode begins after its configured timer interval. ### Custom expression values Workflow Runtime compiles an `Expression` timer against the process instance. A `DateTime` result is used directly. A `TimeSpan`, integer number of milliseconds, or interval string is added to the runtime's current time. If the result cannot be converted, the optional `GetCustomTimerValueAsync` delegate can supply an execution time. An expression reads a process parameter with the `@` prefix. For a `DateTime` parameter named `Deadline`, the expression `(@Deadline).AddHours(1)` schedules the timer one hour after that parameter value. Register a custom resolver when an expression value uses an application-specific format: ```csharp filename="Custom timer value resolver" using System; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Runtime; public static class TimerConfiguration { public static void Configure(WorkflowRuntime runtime) { runtime.GetCustomTimerValueAsync = (value, timerName) => { if (timerName == "EndOfBusinessDay") { return Task.FromResult( DateTime.Today.AddHours(18)); } return Task.FromResult(null); }; } } ``` ### Hosting environment considerations Call `WorkflowRuntime.StartAsync()` after configuration to start the timer manager. See Configure Timers for hosting guidance and Runtime Settings for single-server and multi-server setup. ### Calendar-aware timers An `Interval` timer can use the work calendar resolved for the process instance. A value such as `"8 whours"` is converted to elapsed time according to that calendar's workdays and working hours. ## When timers matter - **SLA enforcement**: escalate a ticket if nobody reviews it within 24 hours - **Deadline**: reject an approval if not decided by end of day - **Reminder**: trigger a notification path after a delay - **Scheduled transition**: move a waiting process at a configured time ## See also ## Frequently asked questions **What timer types does Workflow Engine support?** Five types: `Interval` (delay), `Time` (time of day), `Date` (calendar date at midnight), `DateAndTime` (exact moment), and `Expression` (a C# expression that resolves an execution time). **Can a timer coexist with a command on the same transition?** No. A transition can have exactly one trigger: either a command or a timer. If you need both user-triggered and time-triggered paths from the same activity, create two transitions. **What happens when a timer fires but conditions on the transition fail?** With the default `NotOverrideIfExists="false"`, the runtime clears and registers the current activity's timers again. `Interval` starts a new delay from the current time, `Time` selects today or tomorrow again, `Expression` is reevaluated, and `Date` or `DateAndTime` resolves to the same configured moment. With `NotOverrideIfExists="true"`, the fired same-name timer is not replaced and is deleted after the execution attempt. **What is the `NotOverrideIfExists` property for?** When `NotOverrideIfExists` is `true`, the runtime keeps an existing timer with the same name when that timer still applies after an activity change. This prevents an active countdown from restarting when the process loops back or moves to another activity that uses the same timer. **What happens to timers after the hosting environment restarts?** Persisted timers that became due while Workflow Runtime was stopped are selected after the timer manager starts polling. Single-server mode performs its first poll immediately; multi-server mode waits for its configured timer interval. --- # Annotation URL: https://docs.workflowengine.io/get-started/concepts/annotation/ # Annotation An annotation is a named string value attached to an activity or transition in a scheme. It is custom metadata that application code can read from the scheme model. Use annotations for configuration that belongs to a scheme element rather than data that changes for each process instance. Annotations are part of the scheme definition. Changing an annotation means changing the scheme. A running process instance continues to use its assigned scheme version until it is updated to another version. ## What an Annotation is An annotation is a name-value record on an activity or a transition. Both fields are strings in the scheme model. The value can contain plain text or JSON that application code deserializes when reading it. Activities and transitions can carry multiple annotations. In the Designer, annotations are available in Expert mode in the activity or transition properties. Annotation names are required and must be unique within that scheme element. Workflow Engine does not interpret a regular annotation as activity execution or transition routing logic. Application code reads annotations through the `ProcessDefinition`, `ActivityDefinition`, or `TransitionDefinition` object and decides how to use the values. ## How Annotations work Annotations are stored in the scheme alongside activities and transitions in nested `` elements. Each `` element has a `Name` attribute and a string value inside a `` section. The following scheme defines two annotations on the `Review` activity and one on the `ApproveReview` transition: ```xml filename="DocumentReview.xml" {13-18,43-45} ``` The `Review` activity carries two annotations: a plain string `EmailTemplate` and a JSON object `ReviewConfig`. The `ApproveReview` transition carries one annotation `NotifyRoles`. When `WorkflowRuntime` loads the scheme, annotations are available through the scheme's object model. Read an annotation from an activity or transition by name from the scheme definition: !DocumentReview scheme with annotations on Review activity and ApproveReview transition ```csharp filename="Read annotations from a process scheme" using OptimaJet.Workflow.Core.Model; public sealed record ReviewConfig(string EmailTemplate, string[] Recipients); public static class AnnotationReader { public static (string EmailTemplate, string NotifyRoles) Read( ProcessInstance processInstance) { var processScheme = processInstance.ProcessScheme; return ( processScheme.GetActivityAnnotation("Review", "EmailTemplate"), processScheme.GetTransitionAnnotation("ApproveReview", "NotifyRoles")); } public static ReviewConfig ReadReviewConfig(ProcessInstance processInstance) { return processInstance.ProcessScheme.GetActivityAnnotation( "Review", "ReviewConfig"); } } ``` When an annotation contains valid JSON, the generic `GetActivityAnnotation`, `GetTransitionAnnotation`, and `GetAnnotation` overloads deserialize it to the requested type. During action execution, `processInstance.ExecutedActivity` and `processInstance.ExecutedTransition` provide the same `GetAnnotation` methods. Custom activities are a special case. The Designer can store their configured fields as activity annotations. Before a custom activity runs, Workflow Engine substitutes process parameter references in those values and passes the results to the custom activity without changing the stored scheme annotations. ## Annotation and Parameter Annotations are sometimes confused with parameters. The difference is straightforward: - An **annotation** is metadata stored on an activity or transition in a scheme version. Read it from the scheme definition. - A **parameter** is data stored on a process instance. Read and write it through the process instance as the workflow runs. Use annotations for values that belong to the workflow design, such as an email template name. Use parameters for values that belong to one process run, such as a document ID or approval decision. A parameter reference in a regular annotation remains text when read with `GetAnnotation`; automatic substitution applies when Workflow Engine prepares annotation values for a custom activity. ## See also ## Frequently asked questions **What is an annotation in Workflow Engine?** An annotation is a named string value attached to an activity or transition in a scheme. It stores metadata that code can read, but Workflow Engine does not treat a regular annotation as process execution logic. **How do I read an annotation from code?** Use `GetActivityAnnotation()` or `GetTransitionAnnotation()` on the `ProcessDefinition` object. Activity and transition definitions also expose `GetAnnotation()`, and their generic overloads deserialize valid JSON values. **What is the difference between an annotation and a parameter?** An annotation belongs to an activity or transition in a scheme version. A parameter belongs to a process instance and can change during that process run. **Can I store complex objects in an annotation?** Yes. Store valid JSON as the annotation value, then read it with a generic `GetAnnotation()` overload. The annotation value stored in the scheme remains a string. --- # Process Logs URL: https://docs.workflowengine.io/get-started/concepts/process-logs/ # Process Logs ## What Process Logs are Process logs help you investigate what happened while a process instance was running. They can record action and condition calls, transition selection, activity and state changes, timeouts, execution duration, and errors. Process logging is optional and is disabled by default. Enable it for a scheme with `LogEnabled`, or override that setting for a particular process instance. When logging is enabled, WorkflowRuntime writes `ProcessLogEntry` records through its `IProcessLogProvider`. Set `LogEnabled="true"` on the root `Process` element to enable logging for instances created from a scheme: ```xml ``` To enable logging for an existing process that currently has it disabled, check the instance setting before calling the runtime toggle: ```csharp using System; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Runtime; public static class ProcessLogSwitchExample { public static async Task EnableAsync( WorkflowRuntime runtime, Guid processId) { var processInstance = await runtime.GetProcessInstanceAndFillProcessParametersAsync(processId); if (!processInstance.LogEnabled) { await runtime.ChangeProcessLogEnabledAsync(processId); } } } ``` `ChangeProcessLogEnabledAsync` toggles the current value and returns the new value. The change also applies to subprocesses that belong to the process. Each entry identifies the process and includes a timestamp, message, structured properties, and an optional exception. When subprocesses are involved, their entries are grouped under the root process log and include subprocess details in the entry properties. ## Where Process Logs are stored By default, `WorkflowRuntime` uses `MemoryProcessLogger`, which keeps a bounded set of log entries in application memory. The runtime configures it for up to 100 processes and 100 entries per process. These records are lost when the application stops. For durable or centralized storage, implement `IProcessLogProvider` and register it with `WithProcessLogger`. The provider controls how entries are written and read; Workflow Engine does not automatically persist process logs to the workflow database. See Process Logs Reference for provider setup, the complete `ProcessLogEntry` model, and the available read methods. ## Activity execution event types The `EventType` value in an entry's `Properties` identifies what produced the process log entry. Common values from `ActivityExecutionEventType` include: Table: Activity execution event types | Event type | What it records | |--------------------------|-------------------------------------------------------| | `InitializeProcess` | Initialization of a root process | | `InitializeSubProcess` | Initialization of a subprocess | | `StartExecution` | Start of an execution cycle from the current activity | | `EndExecution` | End of an execution cycle at the current activity | | `CallAction` | Invocation of a Code Action | | `CallCondition` | Evaluation of an expression condition | | `ActivityExecutionError` | An error during activity execution | | `ExecuteTransition` | Execution of a selected transition | | `SetState` | A change to the current state | The enum also contains values for provider calls, condition selection, retries, timeouts, and direct activity changes. See Process Logs Reference for the complete list. ## Logging examples The following examples show process, runtime, and event-based logging. ### Process logging with SQLite The following example stores process logs in a SQLite database. It creates a `WorkflowProcessLog` table, writes entries with parameterized queries, and reads them back through the provider interface. Logging must also be enabled for the scheme or process instance. ```csharp filename="SqliteProcessLogger.cs" using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Data.Sqlite; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OptimaJet.Workflow.Core.Logging; public class SqliteProcessLogger : IProcessLogProvider { private readonly string _connectionString; public SqliteProcessLogger(string connectionString) { _connectionString = connectionString; EnsureTable(); } private void EnsureTable() { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = @" CREATE TABLE IF NOT EXISTS WorkflowProcessLog ( Id INTEGER PRIMARY KEY AUTOINCREMENT, ProcessId TEXT NOT NULL, CreatedOn TEXT NOT NULL, Timestamp TEXT NOT NULL, Message TEXT, Properties TEXT, ExceptionMessage TEXT, ExceptionType TEXT ); CREATE INDEX IF NOT EXISTS IX_WorkflowProcessLog_ProcessId ON WorkflowProcessLog(ProcessId); CREATE INDEX IF NOT EXISTS IX_WorkflowProcessLog_CreatedOn ON WorkflowProcessLog(CreatedOn);"; command.ExecuteNonQuery(); } public void Write(ProcessLogEntry entry) { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO WorkflowProcessLog (ProcessId, CreatedOn, Timestamp, Message, Properties, ExceptionMessage, ExceptionType) VALUES ($processId, $createdOn, $timestamp, $message, $properties, $exceptionMessage, $exceptionType)"; command.Parameters.AddWithValue("$processId", entry.ProcessId.ToString()); command.Parameters.AddWithValue("$createdOn", entry.CreatedOn.ToString("O")); command.Parameters.AddWithValue("$timestamp", entry.Timestamp.ToString("O")); command.Parameters.AddWithValue( "$message", (object?)entry.Message ?? DBNull.Value); command.Parameters.AddWithValue( "$properties", entry.Properties?.ToString(Formatting.None) ?? (object)DBNull.Value); command.Parameters.AddWithValue( "$exceptionMessage", entry.Exception?.Message ?? (object)DBNull.Value); command.Parameters.AddWithValue( "$exceptionType", entry.Exception?.GetType().FullName ?? (object)DBNull.Value); command.ExecuteNonQuery(); } public async Task ReadAllAsync(Guid processId) { return await ReadAsync( processId, "ORDER BY CreatedOn", command => { }); } public async Task ReadLastAsync( Guid processId, DateTime time) { return await ReadAsync( processId, "AND CreatedOn > $time ORDER BY CreatedOn", command => command.Parameters.AddWithValue("$time", time.ToString("O"))); } public async Task ReadLastAsync( Guid processId, int count) { var entries = await ReadAsync( processId, "ORDER BY CreatedOn DESC LIMIT $count", command => command.Parameters.AddWithValue("$count", count)); var results = new List(entries); results.Reverse(); return results; } public async Task ReadEarlyAsync( Guid processId, DateTime time, int count) { var entries = await ReadAsync( processId, "AND CreatedOn < $time ORDER BY CreatedOn DESC LIMIT $count", command => { command.Parameters.AddWithValue("$time", time.ToString("O")); command.Parameters.AddWithValue("$count", count); }); var results = new List(entries); results.Reverse(); return results; } private async Task ReadAsync( Guid processId, string filter, Action addParameters) { var results = new List(); await using var connection = new SqliteConnection(_connectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = $@" SELECT ProcessId, CreatedOn, Timestamp, Message, Properties, ExceptionMessage, ExceptionType FROM WorkflowProcessLog WHERE ProcessId = $processId {filter}"; command.Parameters.AddWithValue("$processId", processId.ToString()); addParameters(command); await using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { results.Add(MapEntry(reader)); } return results; } private static ProcessLogEntry MapEntry(SqliteDataReader reader) { var entry = new ProcessLogEntry { ProcessId = Guid.Parse(reader.GetString(0)), CreatedOn = DateTime.Parse(reader.GetString(1)), Timestamp = DateTime.Parse(reader.GetString(2)), Message = reader.IsDBNull(3) ? null : reader.GetString(3), }; if (!reader.IsDBNull(4)) { entry.Properties = JObject.Parse(reader.GetString(4)); } if (!reader.IsDBNull(5)) { entry.Exception = new Exception( $"{reader.GetString(6)}: {reader.GetString(5)}"); } return entry; } } ``` ```csharp filename="Program.cs" using OptimaJet.Workflow.Core.Runtime; using OptimaJet.Workflow.SQLite; var runtime = new WorkflowRuntime() .WithPersistenceProvider(new SqliteProvider("Data Source=workflow.db")) .WithProcessLogger(new SqliteProcessLogger("Data Source=logs.db")) .AsSingleServer(); ``` ### Runtime logging with ILogger In addition to per-process logs, `WorkflowRuntime` sends its own operational diagnostics through `ILogger`. These messages cover runtime services such as startup, shutdown, timer processing, and multi-server recovery. The logger is optional; when it is not assigned, these diagnostic calls are skipped. Assign an application implementation to `WorkflowRuntime.Logger`: ```csharp using OptimaJet.Workflow.Core.Logging; using OptimaJet.Workflow.Core.Runtime; public static class RuntimeLoggingExample { public static void Configure(WorkflowRuntime runtime, ILogger applicationLogger) { runtime.Logger = applicationLogger; } } ``` `ILogger` receives `Debug`, `Info`, and `Error` messages. Workflow Engine Core defines the interface but does not include a concrete logger implementation. A `ConsoleLogger` example is available in sample and test projects; applications should provide an adapter for their logging system. ### Custom logging with runtime events For selected lifecycle changes and workflow errors, subscribe to runtime events and forward their data to an application's logging or monitoring system: ```csharp using System; using OptimaJet.Workflow.Core.Runtime; public static class RuntimeEventLogExample { public static void Subscribe(WorkflowRuntime runtime) { runtime.OnWorkflowError += (_, args) => Console.Error.WriteLine( $"Process {args.ProcessInstance.ProcessId}: {args.Exception.Message}"); runtime.OnProcessActivityChanged += (_, args) => Console.WriteLine( $"Process {args.ProcessId}: {args.PreviousActivityName} -> " + args.CurrentActivityName); runtime.OnProcessStatusChanged += (_, args) => Console.WriteLine( $"Process {args.ProcessId}: {args.OldStatus} -> {args.NewStatus}"); } } ``` Asynchronous counterparts such as `OnWorkflowErrorAsync` are available when a handler must perform asynchronous work. Events do not store records by themselves and do not contain the detailed action and condition trace provided by process logs. ## Process Logs, Runtime Logs, and Transition History Workflow Engine exposes four related but separate diagnostics mechanisms: Table: Logging and history mechanisms | Mechanism | Purpose | Destination | |--------------------|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------| | Process logs | Trace execution details for a process | `IProcessLogProvider`; in memory by default | | Runtime logs | Report operational diagnostics from `WorkflowRuntime`, such as timer and runtime service activity | The optional `ILogger` assigned to `WorkflowRuntime.Logger` | | Runtime events | Forward selected lifecycle changes and workflow errors to application logging or monitoring | Event handlers subscribed to `WorkflowRuntime` | | Transition history | Record how a process moved between activities and states | The configured persistence provider | `LogEnabled` controls process logs only. It does not enable or disable transition history. Transition history is normally persisted as the process moves, but it can be skipped for an activity with `DisablePersistTransitionHistory`. ## When to use each mechanism - Use process logs while diagnosing the execution of a particular process. - Use runtime logs to monitor engine services and infrastructure. - Use transition history when you need the persisted path of a process through its activities and states. - Use runtime events when another application component must react immediately to a lifecycle change or workflow error. ## See also ## Frequently asked questions **What are process logs in Workflow Engine?** Process logs record execution details for a process instance, including action and condition calls, transitions, state changes, timeouts, and errors. They are written through `IProcessLogProvider`. **How do I enable process instance logging?** Set `LogEnabled="true"` on the scheme's root `Process` element. For an existing instance, check `ProcessInstance.LogEnabled` and call `ChangeProcessLogEnabledAsync` when the value must be toggled. **What is the difference between process logs and runtime logs?** Process logs trace the execution of a process instance through `IProcessLogProvider`. Runtime logs report operational diagnostics from `WorkflowRuntime` through its optional `ILogger`. **How do I store process logs in a database?** Implement `IProcessLogProvider` for the required storage and register it with `runtime.WithProcessLogger(provider)`. The SQLite example above implements `Write`, `ReadAllAsync`, both `ReadLastAsync` overloads, and `ReadEarlyAsync`. **Does Workflow Engine log transition history automatically?** Transition history is written by the persistence provider as a process moves between activities. It is separate from process logging and can be skipped for an activity with `DisablePersistTransitionHistory`. **Are process logs stored in the workflow database?** Not by default. `MemoryProcessLogger` stores them in application memory. Use a custom `IProcessLogProvider` when the records must survive an application restart or be sent to an external logging system. **Are process logs the same as transition history?** No. Process logs are optional diagnostic records about execution. Transition history is a separate persisted record of movement between activities and states. **Do runtime events replace process logs?** No. Runtime events notify subscribers about selected lifecycle changes and errors, but they do not store data and do not provide the full execution trace. --- # Localization URL: https://docs.workflowengine.io/get-started/concepts/localization/ # Localization Localization in Workflow Engine maps internal names for commands, states, command parameters, actions, conditions, schemes, and comments to culture-specific text. Applications can display these translated values while Workflow Engine continues to use the internal names. WorkflowRuntime, ProcessInstance, and `ProcessDefinition` expose a `Localizer`, but each one starts with different translation sources. A lookup uses the requested culture, `CultureInfo.CurrentCulture` when no culture is supplied, a default translation when available, and finally the original internal name. ## What Localization is Localization in Workflow Engine is a set of translation entries stored alongside a scheme or provided through an external source. Each entry maps an object name - for example, a command called `Approve` - to a translated string for a specific culture, such as `Approuver` for French or `Genehmigen` for German. Every translation entry has these properties: Table: Localization entry properties | Property | Description | |----------------|--------------------------------------------------------------------------------------------------------| | **ObjectName** | The internal name of the scheme object being translated | | **Type** | The type of the object: `Command`, `State`, `Parameter`, `Action`, `Condition`, `Scheme`, or `Comment` | | **Culture** | The `CultureInfo` name used for the translation, for example `en-US`, `fr-FR`, or `de-DE` | | **IsDefault** | When `true`, this translation is used when no translation is found for the requested culture | | **Value** | The translated string for the specified culture | The built-in `Localization.Get` method selects an exact culture match before a default entry. When a `Localizer` has several sources, it returns the first non-default candidate in source order. If no such candidate exists, it returns the first default supplied by those sources. If neither is available, the `Localizer` returns the original internal name. ## How Localization works Localization entries are stored in the scheme. They can be accessed through the `Localizer` property on `WorkflowRuntime`, `ProcessInstance`, or `ProcessDefinition` (the scheme object). Each source has a different search scope: - `WorkflowRuntime.Localizer` - searches only external localization providers registered at runtime. - `ProcessInstance.Localizer` - binds the scheme as the first source and the runtime's external provider as the second. - `ProcessDefinition.Localizer` - searches only the scheme's own localization. Localization entries are embedded directly in the scheme XML inside a `` section. The following scheme declares French translations for the `Review` state and the `Approve` command: ```xml filename="DocumentReview.xml" {38-43} ``` !DocumentReview scheme with French localization for Review state and Approve command Access a scheme translation through the process instance's `Localizer`: ```csharp filename="Localize a process state" using System.Globalization; using OptimaJet.Workflow.Core.Model; static string GetFrenchStateName(ProcessInstance processInstance) { return processInstance.Localizer[ "Review", LocalizeType.State, CultureInfo.GetCultureInfo("fr-FR")]; } ``` ### Adding translations with an external provider For shared translations, register an `ILocalizationProvider` with `WorkflowRuntime`. The built-in `Localization` collection already implements this interface: ```csharp filename="Register shared translations" using OptimaJet.Workflow.Core.Model; using OptimaJet.Workflow.Core.Runtime; static WorkflowRuntime AddSharedTranslations(WorkflowRuntime runtime) { return runtime.WithLocalizationProvider(new Localization { new Translation( "Approve", "Approuver", LocalizeType.Command, "fr-FR") }); } ``` For database, resource-file, or service-backed translations, implement `ILocalizationProvider.Get` for lookups and `GetAll` for predefined translations shown in the Designer. See Localization configuration for the provider and programmatic scheme configuration APIs. ### Localizing commands in GetAvailableCommands When you retrieve available commands for a user, pass the culture to populate `WorkflowCommand.LocalizedName` and each command parameter's `LocalizedName`: ```csharp filename="Read localized available commands" using System; using System.Globalization; using System.Threading.Tasks; using OptimaJet.Workflow.Core.Runtime; static async Task ShowCommandsAsync( WorkflowRuntime runtime, Guid processId, string identityId) { var culture = CultureInfo.GetCultureInfo("fr-FR"); var commands = await runtime.GetAvailableCommandsAsync( processId, identityId, culture); foreach (var command in commands) { Console.WriteLine(command.LocalizedName); } } ``` If no culture is specified, `CultureInfo.CurrentCulture` is used. The available-command path creates each `WorkflowCommand` from its `ProcessDefinition`, so it uses the translations embedded in the scheme and does not query the runtime's external provider. ## Localization and Scheme A scheme stores its localization entries in `ProcessDefinition.Localization`. `ProcessDefinition.Localizer` uses only those entries, while `ProcessInstance.Localizer` binds the scheme source first and the runtime's external provider second. A culture-specific external entry can therefore replace a default scheme entry, but not a culture-specific scheme entry. Use scheme-local translations for workflow-specific names and for names returned by available-command APIs. Use an external `ILocalizationProvider` for shared translations resolved through `WorkflowRuntime.Localizer` or a localizer bound to a process or scheme. ## See also ## Frequently asked questions **What is localization in Workflow Engine?** Localization maps commands, states, command parameters, actions, conditions, scheme names, and comments to culture-specific text. A `Localizer` returns the original internal name when neither a culture-specific nor a default translation is available. **How do I add translations to a scheme?** Translations can be added directly to the scheme's localization section. Each entry specifies the object name, `LocalizeType`, culture name, value, and whether it is a default for lookups without a culture-specific match. **How do I add external translations without modifying the scheme?** Implement `ILocalizationProvider` and register it through `runtime.WithLocalizationProvider(provider)`. `WorkflowRuntime.Localizer` uses that external source, while `ProcessInstance.Localizer` binds its scheme before the provider. **How do I get localized command names for a user?** Call `GetAvailableCommandsAsync(processId, identityId, culture)` and read `command.LocalizedName`. Available commands use scheme-local command and parameter translations; when culture is omitted, they use `CultureInfo.CurrentCulture`. --- # Subprocess URL: https://docs.workflowengine.io/get-started/concepts/subprocess/ # Subprocess A subprocess is a child process instance that runs as part of a parent process's execution. The parent creates it through a special transition, and the child runs independently. A finishing transition can copy selected parameters back and continue the parent. ## What a subprocess is A subprocess is defined in the same scheme XML as its parent. `WorkflowRuntime` builds the child process definition from the activities and transitions between two subprocess boundaries: - A **ForkStart** transition goes from a parent activity to the subprocess's first activity. When its trigger fires, `WorkflowRuntime` creates a child `ProcessInstance` with `IsSubprocess = true` and `ParentProcessId` pointing to the parent. The parent stays in place while the child runs. - A **ForkEnd** transition goes from the subprocess's last activity to a parent-level merge activity. It finalizes the child, merges parameters according to the transition's strategy, and continues the parent according to the selected merge mode. The following scheme models an estimate that needs two independent approvals - one from a manager and one from a financial director: ```xml filename="EstimateApproval.xml" ``` !Estimate approval scheme with two parallel subprocesses that merge before the final decision Eight transitions exist in this scheme. Two are ForkStart, two are ForkEnd: - `StartManagerReview` and `StartFinDirectorReview` are Auto-triggered ForkStart transitions. When the process enters `AwaitingApprovals`, the runtime creates one child at `ManagerSignOff` and another at `FinDirectorSignOff`. - `ManagerSigned` and `FinDirectorSigned` are command-triggered ForkEnd transitions with `MergeViaSetState="false"`. Each completed child is removed after its parameters are merged. The runtime then evaluates Auto transitions from `CollectingSignOffs` without forcing the parent into that activity. - `AllApprovalsCollected` uses the Basic Plugin condition `CheckAllSubprocessesCompleted`. It cannot move the parent to `ReadyForDecision` while either child still exists. After the second approval finishes, the condition passes and `Approve` and `Reject` become available to the parent. One subprocess can therefore complete without deciding the parent's outcome. The parent reaches `ReadyForDecision` only after both approvals finish. Two or more subprocesses can run in parallel from the same parent activity. Each is a fully independent process instance with its own process ID. ### Merge modes and parent state control `MergeViaSetState` is configured on a ForkEnd transition: - With `MergeViaSetState="false"` (the default), the runtime merges parameters, removes the completed child, and evaluates Auto transitions outgoing from the ForkEnd target activity. The parent changes activity only if one of those transitions executes. This mode supports merge conditions such as `CheckAllSubprocessesCompleted`. - With `MergeViaSetState="true"`, the runtime merges parameters, removes the completed child, and directly sets the parent to the ForkEnd target activity. It does not wait for sibling subprocesses, so use it only when that child is allowed to determine the parent's next activity. `DisableParentStateControl` is a separate ForkStart setting. With its default value, `false`, the runtime calculates which parent activities allow the child to exist and removes the child if the parent leaves them. Setting it to `true` disables that automatic cleanup, so changing the parent activity does not remove the child. It does not change how the child merges. ## How subprocesses connect A subprocess relates a parent process instance to one or more child instances. Each child has its own process ID, which you use with methods such as `GetAvailableCommandsAsync` and `ExecuteCommandAsync`. The lifecycle: 1. The parent fires a ForkStart transition (auto, command, or timer). 2. `WorkflowRuntime` creates a child `ProcessInstance` and copies parameters according to the ForkStart strategy. 3. The child runs independently. Users or systems interact with it via its own process ID. 4. A ForkEnd transition finalizes the child and merges parameters according to the ForkEnd strategy. 5. The runtime either evaluates an Auto transition for the parent or sets its activity directly, depending on `MergeViaSetState`. A child does not need a ForkEnd transition. Without one, it remains active until it reaches an activity marked `IsFinal`, when the runtime removes it without merging parameters into the parent. It can also be removed earlier when the parent leaves its allowed activities, unless `DisableParentStateControl="true"` disables that cleanup. Each child has `ParentProcessId` (the immediate parent's ID) and `RootProcessId` (the top-level root process ID, useful when subprocesses are nested). ## When subprocesses matter - **Parallel approval**: multiple approvers each get their own subprocess, each runs independently, the parent waits for all to complete. - **Delegated task**: a manager delegates a review to a subordinate; the subordinate's work runs as a subprocess. - **Nested workflows**: a top-level order process spawns subprocesses for fulfillment, invoicing, and shipping. ## See also ## Frequently asked questions **Does a subprocess use a separate scheme or share the parent's scheme?** You define the subprocess's activities and transitions in the parent scheme XML. ForkStart and ForkEnd transitions delimit its boundaries, and the runtime derives the child process definition from that region. You do not author a separate subprocess scheme. **Can I nest subprocesses inside other subprocesses?** Yes. A subprocess can have its own ForkStart and ForkEnd transitions, creating additional child instances. Each child tracks `ParentProcessId` and `RootProcessId` for navigation through the tree. **How do parameters flow from a subprocess back to the parent?** The ForkStart parameter copy strategy controls which parent parameters the child receives. When a ForkEnd transition finishes the child, its merge strategy can overwrite all parent values, overwrite only missing or null values, overwrite a specified list, or overwrite all except a specified list. `MergeViaSetState` controls the parent's next activity, not which parameters are copied. **How is a ForkStart transition triggered?** A ForkStart transition can use any trigger type: auto (fires immediately when the activity is entered), command (requires a user or system to execute it), or timer (fires after a delay). Auto-triggered ForkStart transitions are the most common pattern for creating subprocesses without user interaction. **Can a parent activity spawn multiple subprocesses?** Yes. Multiple ForkStart transitions from the same parent activity create independent child instances. Each child receives its own process ID and runs separately. The parent stays in place and can wait for all children to complete before advancing. --- # Work Calendar URL: https://docs.workflowengine.io/get-started/concepts/work-calendar/ # Work Calendar A work calendar defines which days and hours count as working time, so that timers configured with calendar-aware notation fire only during business hours. Use a work calendar when a process must respect your organisation's working schedule - skip weekends, observe holidays, and count delays in business time rather than wall-clock time. This page explains how to create, register, and assign calendars to process instances. WorkflowRuntime supports any number of named calendars. A process instance can be assigned a specific calendar; otherwise, its scheme calendar or the runtime default is used. When an interval timer value uses a `w` working-time unit, `WorkflowRuntime` calculates the delay according to the selected calendar instead of wall-clock time. ## What a Work Calendar is A work calendar is a named configuration that specifies three things. **Weekend days** determine which days of the week are non-working, for example Saturday and Sunday. **Working hours** define the start time and duration of the working day, for example 10:00 to 18:00 with an 8-hour day. **Holidays and exceptions** are specific dates that override the default schedule, such as public holidays, and custom workday settings for specific dates. `WorkflowRuntime` stores all registered calendars in memory. When it registers an interval timer with a calendar-aware value, the runtime calculates the firing time by advancing through only the working hours defined in the calendar, skipping weekends, holidays, and non-working hours. A process instance inherits its calendar through a priority chain: the process-level calendar takes precedence, followed by the scheme-level calendar, then the runtime default calendar. ## How a Work Calendar works Create a calendar with the `Calendar.Create` method. Configure its weekly schedule, then override specific dates for holidays and custom workdays: ```csharp using OptimaJet.Workflow.Core.Runtime.Calendars; // Define a single holiday: December 31, 2023 var holiday = MonthDay.Create(2023, Months.December, 31); // Define a custom workday: December 30, 2023, from 10 AM for 4 hours var customWorkday = Workday.Create( MonthDay.Create(2023, Months.December, 30), TimeSpan.FromHours(10), TimeSpan.FromHours(4)); // Build the year definition with holidays and custom workdays var year2023 = Year.Create(2023) .SetHolidays(holiday) .SetWorkdays(customWorkday); // Shorthand notation for another year var year2024 = Year.Create(2024) .SetHoliday(Months.January, 1) .SetHoliday(Months.December, 31) .SetWorkday(Months.December, 30, TimeSpan.FromHours(10), TimeSpan.FromHours(4)); // Assemble the calendar var calendar = Calendar.Create("MyCalendar") .SetWeekends(DayOfWeek.Saturday, DayOfWeek.Sunday) .SetWorkingHours(TimeSpan.FromHours(8)) .SetStartOfWork(TimeSpan.FromHours(10)) .SetYears(year2023, year2024); ``` Register the calendar with `WorkflowRuntime` and set it as the default: ```csharp runtime.WithCalendars(calendar); runtime.SetDefaultCalendar(calendar.Name); ``` `SetDefaultCalendar` accepts only the name of a calendar already registered with `WithCalendars`. ### Using calendars with timers Once a calendar is registered and selected, use `w` working-time units in an `Interval` timer value to activate calendar-aware calculation. Do not mix working-time units with ordinary interval units in the same value. Table: Calendar-aware timer value examples | Timer value | Meaning | |-------------|-----------------------------------------------------------------| | `2wd` | 2 working days, skipping weekends and holidays | | `8 whours` | 8 working hours - the timer fires after 8 hours of working time | | `1 wd` | 1 working day - the timer fires after one full working day | Without a `w` working-time unit, an interval timer uses wall-clock time regardless of any calendar. A working-time value requires the selected calendar name to resolve to a registered calendar. ### Assigning a calendar to a specific process Pass the calendar name when creating a process instance to override the default: ```csharp var createParams = new CreateInstanceParams("SchemeCode", processId) { CalendarName = "MyCalendar" }; await runtime.CreateInstanceAsync(createParams); ``` ### Multiple calendars for different departments Register multiple calendars and assign them per scheme or per process: ```csharp var itCalendar = Calendar.Create("IT department calendar") .SetWeekends(DayOfWeek.Saturday, DayOfWeek.Sunday) .SetWorkingHours(TimeSpan.FromHours(8)) .SetStartOfWork(TimeSpan.FromHours(10)) .SetYears(Year.Create(2023) .SetHolidays( MonthDay.Create(2023, Months.January, 1), MonthDay.Create(2023, Months.January, 2)) .SetWorkday(Months.December, 31, TimeSpan.FromHours(10), TimeSpan.FromHours(4))); var salesCalendar = Calendar.Create("Sales department calendar") .SetWeekends(DayOfWeek.Saturday, DayOfWeek.Sunday) .SetWorkingHours(TimeSpan.FromHours(8)) .SetStartOfWork(TimeSpan.FromHours(7)) .SetYears(Year.Create(2023) .SetHoliday(Months.January, 1)); runtime.WithCalendars(itCalendar, salesCalendar); runtime.SetDefaultCalendar("IT department calendar"); ``` ### Calendar selection priority When `WorkflowRuntime` calculates an interval timer's firing time, it selects the first configured calendar name in this order: the **process-level calendar** assigned through `CreateInstanceParams.CalendarName`, the **scheme-level calendar**, then the **runtime default calendar** set through `runtime.SetDefaultCalendar()`. The selected name must be registered in `runtime.Calendars`; an unregistered higher-priority name does not fall back to a lower-priority calendar. Without a selected calendar, ordinary interval values use wall-clock time, while `w` working-time values cannot be calculated. ## Work Calendar and Timer A timer that uses a work calendar behaves differently from a wall-clock timer: - A **wall-clock timer** counts real seconds, minutes, and hours. A timer set to `4h` fires exactly 4 hours after it is registered, regardless of whether that time falls outside business hours. - A **calendar-aware timer** counts only working time. A timer set to `4 whours` at 3 PM, with the workday ending at 6 PM, is scheduled for 11 AM on the next working day: three working hours elapse on the first day and one on the next. Use a wall-clock timer for absolute deadlines and technical timeouts. Use a calendar-aware timer for business deadlines that should only count during operational hours. Calendar calculation happens when an interval timer is registered. Timer execution still requires `WorkflowRuntime` to be configured for single-server or multi-server mode and started with `StartAsync()`. ## See also ## Frequently asked questions **What is a work calendar in Workflow Engine?** A work calendar defines which days and hours count as working time. When a timer uses the `w` notation, `WorkflowRuntime` uses the calendar to skip weekends, holidays, and non-working hours. A calendar is configured with weekend days, working hours, and per-date exceptions. **How do I make a timer use a work calendar?** Use a `w` working-time unit in an `Interval` timer value. For example, `2wd` means 2 working days and `8 whours` means 8 working hours. The selected work calendar must be registered with `WorkflowRuntime`. **How does a work calendar handle an interval that crosses non-working time?** The work calendar includes only configured working time in the interval. If a 4-working-hour timer is set at 3 PM on Friday and the workday ends at 6 PM, its firing time is Monday at 11 AM when Monday is the next working day. **Can I have different calendars for different processes?** Yes. Register multiple calendars with `runtime.WithCalendars()` and assign each process instance a specific calendar through `CreateInstanceParams.CalendarName`. When that property is null, the runtime checks the scheme calendar and then the default runtime calendar. **How do I define public holidays in a calendar?** Use `Year.Create(year).SetHoliday(month, day)` for individual holidays or `SetHolidays()` for multiple. Use `SetWorkday()` to override a date that would normally be a weekend. Each year's configuration is independent.