Multitenancy
In Quick Start we covered how to configure the Web API in single-tenant mode. In that mode, the API works
with one tenant whose identifier is WorkflowApiConstants.SingleTenantId (an empty string).
For most scenarios, this is sufficient to run Workflow Engine, and you don’t need to think about tenants or other multi-tenant details. Single-tenant mode simply hides those details from you.
Tenant Model
In the Web API, a tenant is represented by one or more tenant ids, a WorkflowRuntime, and a data provider. A single
database provider factory can create multiple provider instances, and each WorkflowRuntime can handle requests from
multiple logical tenant ids.
Tenants that differ only by tenant id but are served by the same WorkflowRuntime are called logical tenants.
Tenants that are served by different WorkflowRuntime instances are called physical tenants.
Tenant ids are case-sensitive, must be non-null, and can be at most 128 characters long. The empty string
WorkflowApiConstants.SingleTenantId is reserved for single-tenant mode. Named multi-tenant configurations cannot use
it or include it in permission claim tenant targets.
Static Configuration
To enable multi-tenant mode, use AddWorkflowTenants instead of AddWorkflowRuntime.
Provide a list of options that describe the physical tenants to create during application startup. For example:
builder.Services.AddWorkflowTenants(
new WorkflowTenantCreationOptions
{
// First physical tenant creation options.
TenantIds = ["MsSqlTenant1", "MsSqlTenant2"],
PersistenceProviderId = PersistenceProviderId.Mssql,
ConnectionString = "Server=localhost,1433;Database=master;User Id=SA;Password=MyPassword;"
},
new WorkflowTenantCreationOptions
{
// Second physical tenant creation options.
TenantIds = ["PostgresTenant1", "PostgresTenant2"],
PersistenceProviderId = PersistenceProviderId.Postgres,
ConnectionString = "Host=localhost;Port=5432;Database=postgres;User Id=postgres;Password=MyPassword;"
}
);
// Don't forget to register the database providers you use.
builder.Services.AddWorkflowApiMssql();
builder.Services.AddWorkflowApiPostgres();
This example defines two physical tenants. The first serves the logical tenants MsSqlTenant1 and MsSqlTenant2 using
the Microsoft SQL Server provider, and the second serves PostgresTenant1 and PostgresTenant2 using the PostgreSQL provider.
Startup tenants are registered through the same registry pipeline as dynamically added tenants.
Key options in WorkflowTenantCreationOptions:
TenantIds— the list of logical tenant identifiers that will be handled by thisWorkflowRuntimeand database provider (i.e., by this physical tenant).PersistenceProviderId— the identifier of the database provider factory used to create bothIWorkflowProviderandIDataProvider. If omitted, the first registered provider will be used. The selected provider must be registered as a service. Available IDs are defined in thePersistenceProviderIdclass.ConnectionString— the database connection string used to create providers. You can define multiple physical tenants that use the same provider factory but different connection strings.
The remaining options in WorkflowTenantCreationOptions let you fine-tune how WorkflowRuntime and providers are created.
Dynamic Registration
After the ASP.NET host starts, use IWorkflowTenantRegistry to register or remove tenants without restarting the
application:
var tenantRegistry = services.GetRequiredService<IWorkflowTenantRegistry>();
IReadOnlyCollection<IWorkflowTenant> tenants =
await tenantRegistry.RegisterTenantsAsync(new WorkflowTenantCreationOptions
{
TenantIds = ["TenantA"],
PersistenceProviderId = PersistenceProviderId.Mssql,
ConnectionString = connectionString,
WorkflowRuntimeCreationOptions =
{
ConfigureWorkflowRuntime = runtime =>
{
runtime.AsSingleServer();
}
}
});
var tenant = tenants.Single();
To unregister the tenant later:
await tenantRegistry.UnregisterTenantsAsync(tenant);
The registry publishes a new immutable snapshot immediately, so new requests no longer see unregistered tenants. If an old request snapshot still references a removed tenant, registry-owned tenant shutdown waits until that snapshot is disposed.
You can also create IWorkflowTenant instances yourself and register them:
await tenantRegistry.RegisterTenantsAsync(customTenant);
Use WorkflowTenantLifecycleOwnership.Registry when the registry should call StartAsync() before publication and
ShutdownAsync() after removal. Use WorkflowTenantLifecycleOwnership.External when the host application starts and
stops the tenant lifecycle itself.
Usage
When calling the API in multi-tenant mode, you can explicitly specify TenantId via the
WorkflowApiConstants.TenantIdHeader request header ("Workflow-Api-Tenant-ID"). If the header is missing,
DefaultTenantId is used, which by default is WorkflowApiConstants.SingleTenantId.
In multi-tenant mode, it is recommended to set DefaultTenantId to null. This ensures that every API request must
explicitly provide a TenantId; otherwise, the request is rejected with an error.
The tenant header selects the tenant context only. It is not an authorization source. When
security services are enabled, the current WorkflowApiPermissions claim must also allow
the selected tenant. If the tenant permission is missing, the header contains an unknown or invalid tenant id, or the
caller changes the header to a tenant that is not allowed by the token, the request is rejected with 403 Forbidden.
When a TenantId is provided, the Workflow API resolves the physical tenant that serves the given logical tenant from
the request-scoped IWorkflowTenantSnapshot. The request is then processed by the corresponding WorkflowRuntime and
database provider. The snapshot is immutable for the duration of the request, even if tenants are registered or removed
concurrently.
The resolved tenant affects all API groups:
- Data API reads, creates, updates, and deletes data in the current tenant scope. Process-related endpoints
(
processes,statuses,parameters,timers,transitions,inbox entries, andapproval history) do not expose data from other tenants. Creating process parameters or timers requires the parent process to exist in the current tenant. Generic scheme and global parameter Data API create operations also write records into the current tenant scope. - RPC API runs under strict tenant validation. Process operations cannot read or modify a process that belongs to
another tenant. For single-process RPC calls, a foreign-tenant process is returned as
404 Not Foundwith the same public message as a missing process.rpc/is-process-existsreturnsfalsefor a process from another tenant. - Designer API passes the resolved tenant id to the Workflow Designer connector, so scheme and designer operations are executed in the current tenant context.
Workflow Engine Core supports shared and tenant-specific schemes and forms. Shared records use null or an empty tenant
id and can be used as a fallback by runtime/designer flows where that fallback is implemented. The generic Web API Data
API remains scoped to the current HTTP tenant and is not a cross-tenant management interface.
Using ASP.NET dependency injection, request code can retrieve the current tenant through IWorkflowTenantLocator or via
extension methods on IServiceProvider.
public class MyController : ControllerBase
{
private readonly IWorkflowTenantLocator _tenantLocator;
public MyController(IWorkflowTenantLocator tenantLocator)
{
_tenantLocator = tenantLocator;
}
[HttpGet("my-endpoint")]
public IActionResult MyEndpoint()
{
var currentTenant = _tenantLocator.GetHttpContextWorkflowTenant();
WorkflowRuntime runtime = currentTenant.WorkflowRuntime;
// Use currentTenant here...
return Ok();
}
}
Equivalent extension methods are also available from IServiceProvider:
var snapshot = HttpContext.RequestServices.GetHttpContextWorkflowTenantSnapshot();
var tenantId = HttpContext.RequestServices.GetHttpContextWorkflowTenantId();
var tenant = HttpContext.RequestServices.GetHttpContextWorkflowTenant();
var runtime = HttpContext.RequestServices.GetHttpContextWorkflowRuntime();
Outside an HTTP request, use IWorkflowTenantRegistry.GetSnapshot() and dispose the snapshot when you are done:
await using var snapshot = tenantRegistry.GetSnapshot();
var tenant = snapshot.GetTenant("TenantA");
var runtime = tenant.WorkflowRuntime;