Workflow Engine 22.0.0
July 16, 2026
Overview
Workflow Engine 22.0.0 is a major feature release focused on dynamic Workflow Engine Web API tenant registration, full hybrid multi-tenancy for process data, schemes, Global Parameters, and forms, plus major platform updates for the Forms Plugin and Angular Designer packages.
Why update
- Add and remove Workflow Engine Web API tenants at runtime without restarting the ASP.NET host.
- Get consistent logical tenant isolation for processes, process-related entities, schemes, Global Parameters, and forms
while preserving shared records through
nullor empty tenant values where supported. - Prevent cross-tenant access through Data API and RPC API operations that previously could read or affect processes or related records outside the current tenant.
- Use Forms Plugin packages on Form Engine 10.0.1 and Angular Designer packages aligned with Angular 22.
Key features
- Workflow Engine Web API now uses a dynamic tenant registry with immutable request snapshots, so multi-tenant hosts can start without a complete static tenant list and register tenants later.
- Full hybrid multi-tenancy extends tenant scoping to schemes, Global Parameters, forms, and process-related persistence, including timers, statuses, parameters, transition history, inbox entries, and approval history.
- Data API and RPC API process operations are now tenant-scoped and hide foreign-tenant processes from the current request.
- Forms Plugin API and frontend components now pass tenant context through form operations and apply deterministic tenant-first/shared fallback rules.
- The obsolete Assignment Plugin has been removed.
- Forms Plugin now uses Form Engine 10.0.1.
- Angular and related packages were updated to Angular 22.
Changelog
Update instructions
Mandatory update checklist
- All packages: update Workflow Engine packages to version 22.0.0.
- If you use SQL persistence providers, run Workflow Engine database migrations through
RunMigrations()before starting the updated runtime. The bundled SQL migration scripts are executed automatically by the migration mechanism. This release adds tenant storage and tenant-aware queries for schemes, Global Parameters, process-related entities, and forms. See Database versioning. - If you use the MongoDB provider, manually run the bundled
update_22.0.0.jsupdate script against your MongoDB database before starting the updated runtime. The script adds the new indexes required for tenant-aware queries. - If you use Workflow Engine Web API in multi-tenant mode, review tenant startup and runtime registration code. Static
AddWorkflowTenants(...)configuration remains supported, but tenant providers and registry read helpers were removed. - If you use Workflow Engine Web API Data API or RPC API in multi-tenant mode, test clients against the stricter tenant boundary. Foreign-tenant reads now return not-found style results, and foreign-tenant writes are rejected, no-op, or failed per endpoint instead of crossing tenant boundaries.
- If you use custom
IPersistenceProvider,IWorkflowTenant,IWorkflowTenantFactory, Forms Plugin workflow provider, or custom Web API authorization code, update those implementations before rebuilding. - If you use
AssignmentPlugin, remove the package and replace the integration. The plugin is no longer supported in 22.0.0. - If you use Angular Designer packages, update Angular and related packages to Angular 22.
- If you use Forms Plugin frontend packages, update Form Engine dependencies to 10.0.1 through the corresponding
@react-form-builder/*packages.
Scenario-specific migrations
Use the following sections for the cases that apply to your installation.
Resolving the current runtime, tenant ID, or tenant in HTTP code
The old tenant registry HTTP helper methods were removed. Inject IWorkflowTenantLocator or use the public
IServiceProvider extension methods.
Before:
public sealed class MyController
{
public MyController(IWorkflowTenantRegistry tenantRegistry)
{
_tenantRegistry = tenantRegistry;
}
public WorkflowRuntime GetRuntime()
{
return _tenantRegistry.GetHttpContextWorkflowRuntime();
}
private readonly IWorkflowTenantRegistry _tenantRegistry;
}
After, using IWorkflowTenantLocator:
public sealed class MyController
{
public MyController(IWorkflowTenantLocator tenantLocator)
{
_tenantLocator = tenantLocator;
}
public WorkflowRuntime GetRuntime()
{
return _tenantLocator.GetHttpContextWorkflowRuntime();
}
private readonly IWorkflowTenantLocator _tenantLocator;
}
Replace HTTP helper calls as follows:
tenantRegistry.GetHttpContextTenantId()->tenantLocator.GetHttpContextWorkflowTenantId()orservices.GetHttpContextWorkflowTenantId().tenantRegistry.GetHttpContextTenant()->tenantLocator.GetHttpContextWorkflowTenant()orservices.GetHttpContextWorkflowTenant().- Use
services.GetHttpContextWorkflowTenantSnapshot()when you need the request-scoped immutable tenant snapshot.
Reading tenants outside an HTTP request
If code previously read the registry directly outside an HTTP request, use IWorkflowTenantSnapshot.
public sealed class MyService
{
public MyService(IWorkflowTenantSnapshot tenantSnapshot)
{
var tenantId = "TenantA";
var tenant = tenantSnapshot.GetTenant(tenantId);
var runtime = tenant.WorkflowRuntime;
}
}
Static and dynamic multi-tenant configuration
- If you used
AddWorkflowTenants(options), startup code usually does not need to change. The public signature remains available, and startup tenants are registered through the registry lifecycle. - If you used
AddWorkflowTenantProvider(new ListWorkflowTenantProvider(...)), replace it withAddWorkflowTenants(...)for options-based startup registration, or register tenants after host startup throughIWorkflowTenantRegistry.RegisterTenantsAsync(...). MapWorkflowApi()no longer requires a tenant provider during endpoint mapping. A multi-tenant host can start without the complete static tenant list and accept tenants later through the registry.
Example dynamic tenant registration after host startup:
var tenantRegistry = services.GetRequiredService<IWorkflowTenantRegistry>();
var tenants = await tenantRegistry.RegisterTenantsAsync(new WorkflowTenantCreationOptions
{
TenantIds = ["TenantA"],
PersistenceProviderId = PersistenceProviderId.Mssql,
ConnectionString = connectionString
});
var tenant = tenants.Single();
Example tenant removal:
await tenantRegistry.UnregisterTenantsAsync(tenant);
The registry removes the tenant from new snapshots immediately. For registry-owned tenants, WorkflowRuntime shutdown
is delayed until older snapshots that still reference the tenant are released.
Custom tenant implementations
Custom IWorkflowTenant implementations must now implement the lifecycle contract:
- Add
WorkflowTenantLifecycleOwnership LifecycleOwnership. - Implement
Task StartAsync(). - Implement
Task ShutdownAsync(). - Use
WorkflowTenantLifecycleOwnership.Registrywhen the registry should start and stop the tenant runtime. - Use
WorkflowTenantLifecycleOwnership.Externalwhen your application keeps lifecycle ownership.
The standard WorkflowTenant constructor now requires WorkflowTenantLifecycleOwnership. The
IWorkflowTenantFactory.Create(IEnumerable<string>, WorkflowRuntime, DataProviderCreationOptions) overload also has a
new optional lifecycleOwnership parameter, so custom IWorkflowTenantFactory implementations must update their
signatures.
Web API permission and authorization code
IWorkflowApiPermissions no longer authorizes the current HTTP request. Keep it for permission claim creation and
validation through CreateBuilder(), BuildClaim(string), and BuildClaim(Action<...>).
- Remove direct calls to
IWorkflowApiPermissions.HttpContextHasAccess(...). - For Workflow API endpoints, rely on
AddWorkflowApiSecurity(...)andMapWorkflowApi(). Authorization now runs in the request pipeline using the scoped tenant snapshot. - Do not use
WorkflowApiConstants.SingleTenantIdas a tenant permission target. It remains an empty string, but it is valid only in a single-tenant snapshot.
Process-related persistence and custom providers
Custom IPersistenceProvider implementations must update timer and process existence contracts:
- Update
RegisterTimerAsyncto acceptstring tenantIdbeforebool notOverrideIfExists. - Implement
Task<bool> IsProcessExistsAsync(Guid processId, string tenantId).
Before:
RegisterTimerAsync(processId, rootProcessId, name, nextExecutionDateTime, notOverrideIfExists)
After:
RegisterTimerAsync(processId, rootProcessId, name, nextExecutionDateTime, tenantId, notOverrideIfExists)
Global Parameters and custom providers
Custom IPersistenceProvider implementations must add tenant-scoped Global Parameter methods:
SaveTenantGlobalParameterAsync.LoadTenantGlobalParameterAsync.LoadTenantGlobalParametersWithNamesAsync.LoadTenantGlobalParametersAsync.LoadTenantGlobalParametersWithPagingAsync.DeleteTenantGlobalParametersAsync.
If an external system previously managed Workflow Engine system Global Parameter types through the generic Global
Parameters Data API, move it to specialized APIs or customize ProtectedGlobalParameterTypes. By default, direct
generic get, create, update, and delete requests for protected types return 400; collection, search, and
collection-delete operations hide or skip protected records.
Forms Plugin providers and frontend integrations
Forms Plugin provider methods now receive an optional tenantId. Update custom workflow provider implementations and
custom form handlers to pass or resolve tenant context consistently.
Forms lookup and mutation behavior is now scoped:
GetFormNamesAsync(tenantId): withouttenantId, returns only shared forms whereTenantId = null; withtenantId, returns distinct names from the tenant scope and shared scope.GetFormVersionsAsync(name, tenantId): withouttenantId, returns only shared versions; withtenantId, returns tenant versions when the tenant has any rows for the form name, otherwise falls back to shared versions.GetFormAsync(name, version: null, tenantId): withouttenantId, returns the latest shared version; withtenantId, searches tenant and shared scopes, sorts tenant forms above shared forms, then sorts byVersion DESC.GetFormAsync(name, version, tenantId): withouttenantId, searches only the exact shared version; withtenantId, returns the tenant exact version when present, does not fall back to shared if the tenant has any rows for that form name, and falls back to shared only when the tenant has no rows for that form name.CreateNewFormIfNotExistsAsync(name, defaultDefinition, tenantId): checks existence only in the exact target scope. A shared form does not count as an existing tenant form. If a tenant form is missing, tenantv0is created from the latest shared form when available, otherwise fromdefaultDefinition. After a concurrent duplicate insert, the method rereads the exact target scope.CreateNewFormVersionAsync(name, defaultDefinition, version, tenantId): creates the new version only in the exact target scope. The next version number is calculated from the target scope; an empty target scope createsv0. Definition is copied from the target scope, or from shared scope only as bootstrap when tenant scope is empty. If the requested source version is not found in either allowed scope, the method returnsnull, and the provider-level wrapper throws an error.UpdateFormAsync(...): updates only the exact target scope and exact version with lock checks. Shared fallback is not used.DeleteFormVersionAsync(...): deletes only the exact version in the exact target scope. Shared fallback is not used.DeleteFormAsync(...): deletes all versions only in the exact target scope. Shared fallback is not used.
FormsManager and FormsViewer now include tenantId. If FormsViewer requests are handled by external application code,
you can either pass tenantId through those handlers or resolve it inside the handlers from your own request context.
Passing it explicitly keeps behavior consistent with the rest of the Forms Plugin API.
Breaking changes
Major breaking changes
- The Web API tenant model is no longer a static list created during DI configuration and then read directly from
IWorkflowTenantRegistry. The public tenant API now uses a dynamic registry and immutable request snapshots.IWorkflowTenantRegistry.Tenantswas replaced withIWorkflowTenantSnapshot.Tenants,IWorkflowTenantRegistry.IsSingleTenant()was replaced withIWorkflowTenantSnapshot.IsSingleTenant, andIWorkflowTenantRegistry.Get(string id)was replaced withIWorkflowTenantSnapshot.GetTenant(string id). - HTTP-specific tenant helpers were removed from
IWorkflowTenantRegistry.IWorkflowTenantRegistry.GetHttpContextTenantId(),GetHttpContextTenant(), andGetHttpContextWorkflowRuntime()were replaced withIWorkflowTenantLocatorand publicIServiceProviderextension methods. IWorkflowTenantProvider,ListWorkflowTenantProvider, andAddWorkflowTenantProvider(...)were removed. Tenants are no longer supplied through a provider collection in DI. Static startup configuration remains available throughAddWorkflowRuntime(...)andAddWorkflowTenants(...); runtime registration after application startup is performed throughIWorkflowTenantRegistry.RegisterTenantsAsync(...).IWorkflowTenantnow has a lifecycle contract. All custom implementations must exposeWorkflowTenantLifecycleOwnership LifecycleOwnershipand implementTask StartAsync()andTask ShutdownAsync(). The standardWorkflowTenantconstructor requiresWorkflowTenantLifecycleOwnership, and customIWorkflowTenantFactoryimplementations must update theCreate(...)signature for the new optionallifecycleOwnershipparameter.- Data API process-related entities are now tenant-scoped.
statuses,parameters,timers,transitions,inbox entries,approval history, andprocessesno longer allow cross-tenant reads or changes. Foreign-tenantGETrequests now behave as not found; foreign-tenantupdateanddeleterequests are no-op or rejected depending on the endpoint. - RPC API endpoints under
workflow-api.rpc.*now strictly check the process tenant against the current HTTP tenant ID. A process from another tenant cannot be read, changed, executed, deleted, checked, or otherwise manipulated through RPC by knowing itsprocessId. - Schemes, Global Parameters, and forms now participate in tenant scoping. Existing records with
nullor empty tenant values are treated as shared records and can be visible to all tenants according to each API's fallback rules, but new records created in tenant context are written with tenant information.
Additional breaking changes
MapWorkflowApi()no longer validates or enumerates tenant provider state during endpoint mapping. Tenant composition is validated during registration and removal, not in the request pipeline.WorkflowApiConstants.SingleTenantIdremains an empty string, but it is now explicitly valid only in a single-tenant snapshot and cannot be used in tenant permission targets.- Changing tenant IDs on an already registered tenant is not supported. Unregister the tenant and register a new tenant with the required IDs.
IWorkflowApiPermissionsno longer authorizes the current HTTP request.HttpContextHasAccess(ClaimsPrincipal, string)was removed from the public interface. Workflow API authorization is performed inside the request pipeline with the scoped tenant snapshot.IPersistenceProvider.RegisterTimerAsyncnow requiresstring tenantIdbeforebool notOverrideIfExists.IPersistenceProvidernow requiresTask<bool> IsProcessExistsAsync(Guid processId, string tenantId).- Data API creation operations for
ParametersandTimersnow require the parent process to exist in the current tenant.createandcreate-collectionno longer work when the process is missing or whenprocessIdbelongs to a foreign tenant. This is a breaking change and also closes an unsafe cross-tenant behavior. - Data API
processessearch, collection read, single read, and update operations now work only in the current tenant. Cross-tenant process queries are no longer supported. - The process tenant cannot be changed through Data API.
tenantIdwas removed fromProcessUpdateRequest, soPUT /data/processes/{id}cannot move a process to another tenant. TenantIdwas removed from the process query contract.ProcessField.TenantIdis no longer available, so clients cannot filter, sort, or searchprocessesbyTenantIdthrough the current Data API contract.- For single RPC endpoints, tenant mismatch and
ProcessNotFoundExceptionnow map to404 NotFoundwith the public messageProcess with id {id} not found. Earlier versions could allow some cross-tenant scenarios or return the real not-found case in another form, including500. rpc/is-process-existsnow returnsfalsefor a process in another tenant to hide whether the process exists.- Bulk RPC result behavior changed for process IDs from another tenant. The affected item is returned as
Failedwith a Core exception inBulkTaskResult.Exception;rpc/bulk-is-process-existsis the exception and returnsCompletedwithResult = false. Bulk API can still expose bothProcessNotFoundandProcessTenantMismatchExceptionin item results. - Custom
IPersistenceProviderimplementations must add the tenant-scoped Global Parameter methods:SaveTenantGlobalParameterAsync,LoadTenantGlobalParameterAsync,LoadTenantGlobalParametersWithNamesAsync,LoadTenantGlobalParametersAsync,LoadTenantGlobalParametersWithPagingAsync, andDeleteTenantGlobalParametersAsync. - Generic Workflow Engine Web API access to system Global Parameter types is now blocked by default. Direct generic
get,create,update, anddeleteoperations for protected types return400; collection, search, and delete-collection operations hide or skip those records. - The default protected Global Parameter types are
CodeAction,WFE_CodeActionsSettings,WFE_MultiServerSettings,WFE_SingleServerSettings,Plugin,Security,settings,LoggerSettings,CallbackProvider,LDAPConf,OpenIddictServer, andCorsSettings. - Forms Plugin workflow provider form methods now receive an optional
tenantId. Custom providers must update their method signatures and tenant-aware lookup, create, update, and delete behavior. - Forms Plugin fallback rules are tenant-aware. When a tenant already has rows for a form name, exact-version reads do not fall back to shared rows, and updates/deletes never fall back to shared scope.
- FormsManager and FormsViewer now include
tenantIdin form-related operations. AssignmentPluginwas removed and is no longer supported.
Features
- Web API: added dynamic registration and removal of Workflow API tenants without restarting the application.
- Web API: added public
IWorkflowTenantRegistrymutation APIs:RegisterTenantsAsync(IEnumerable<WorkflowTenantCreationOptions>),RegisterTenantsAsync(params WorkflowTenantCreationOptions[]),RegisterTenantsAsync(IEnumerable<IWorkflowTenant>),RegisterTenantsAsync(params IWorkflowTenant[]),UnregisterTenantsAsync(IEnumerable<IWorkflowTenant>), andUnregisterTenantsAsync(params IWorkflowTenant[]). - Web API: added public
IWorkflowTenantSnapshot, an immutable point-in-time view of tenant instances, tenant IDs, and single-tenant state. - Web API: added public
IWorkflowTenantLocatorfor HTTP-specific resolution of the current tenant ID, tenant instance, tenant snapshot, andWorkflowRuntime. - Web API: added public helper
IServiceProvider.GetHttpContextWorkflowTenantSnapshot(). - Web API: added tenant lifecycle ownership through
WorkflowTenantLifecycleOwnership.RegistryandWorkflowTenantLifecycleOwnership.External. - Web API: registry-managed tenants are started before publication in snapshots and shut down gracefully after removal when no active snapshot still references them.
- Web API: externally managed tenants can be registered without automatic
StartAsync()andShutdownAsync()calls by the registry. - Web API:
AddWorkflowRuntime(...)andAddWorkflowTenants(...)now use the same registration pipeline as runtime dynamic registration. - Web API: the tenant registry is connected to the application lifecycle through a hosted service. Startup tenants are registered and started with the host; registry-managed tenants are shut down gracefully when the application stops.
- Core and providers: added tenant persistence for process-related entities, including timers, process status, persistence parameters, transition history, inbox entries, and approval history.
- Core and providers: added full hybrid multi-tenancy support for
WorkflowSchemaandWorkflowProcessSchema. - Web API Data API: added tenant-scoped behavior for schemes, processes, process-related entities, and Global Parameters.
- Forms Plugin API: added tenant-aware form operations and tenant-specific/shared fallback behavior.
- Forms Plugin frontend: added tenant ID support to FormsManager and FormsViewer.
Enhancements
- Web API: the HTTP request path no longer performs full tenant configuration validation and no longer reads mutable startup state.
- Web API:
MapWorkflowApi()filters endpoints by license restrictions without creating or enumerating tenant runtimes. - Web API: tenant configuration validation is earlier and atomic. Duplicate IDs, overlong IDs, mixing the reserved
single-tenant ID with multi-tenant IDs, missing runtime or data provider, and reuse of one
WorkflowRuntimeby multiple tenant instances are rejected before a new snapshot is published. - Web API:
UnregisterTenantsAsync(...)with an unknown tenant instance is a no-op and does not affect the current snapshot. - Web API: single-tenant mode no longer requires a tenant permission rule in permission claims.
- Web API: permission builder and parser validation now account for tenant ID restrictions.
- Web API: public helper methods
GetHttpContextWorkflowTenantId,GetHttpContextWorkflowTenant, andGetHttpContextWorkflowRuntimeremain available and now work throughIWorkflowTenantLocatorand the scoped request snapshot. - Web API: readiness and tenant-readiness use the scoped snapshot for the current request, so they observe a consistent tenant set during the request.
- Web API: authorization uses the same scoped tenant resolution as the execution pipeline and returns
403 Forbiddenfor missing, invalid, unknown, or forbidden tenant IDs in multi-tenant mode. - Core and providers: new process-related persistence records take tenant ID from
ProcessInstance.TenantId; bulk creation also propagates tenant ID into statuses and timers. - Core and providers: provider-level and runtime tests now cover tenant scoping for process creation, bulk initialization, parameters, statuses, timers, inbox, approval history, and transition history.
- Global Parameters: Web API now protects Workflow Engine system Global Parameter types from generic Data API access by
default through
ProtectedGlobalParameterTypes. - Forms Plugin: FormEngine was updated to version 10.0.1.
- Designer Angular package:
@optimajet/workflow-designer-angularand related Angular packages were updated to Angular 22.
Bug fixes
- Web API Data API: fixed unsafe cross-tenant behavior for process-related entities by requiring
ParametersandTimerscreation to target an existing parent process in the current tenant. - Web API RPC API: fixed cross-tenant process manipulation by enforcing tenant-aware process checks for
workflow-api.rpc.*endpoints. - Web API RPC API: fixed process existence checks so a process from another tenant is reported as missing instead of
revealing cross-tenant existence through
rpc/is-process-exists.
Resources
- Web API core services and tenant registry: Core Services.
- Web API multitenancy: Multitenancy.
- Web API security documentation: Security services.
- Database migrations guide: Database versioning.
- Forms Plugin documentation: Forms.
- Form Engine: formengine.io.