Skip to main content

Introducing Formengine - The New Formbuilder, try for FREE formengine.io.

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:

  1. TenantIds — the list of logical tenant identifiers that will be handled by this WorkflowRuntime and database provider (i.e., by this physical tenant).
  2. PersistenceProviderId — the identifier of the database provider factory used to create both IWorkflowProvider and IDataProvider. If omitted, the first registered provider will be used. The selected provider must be registered as a service. Available IDs are defined in the PersistenceProviderId class.
  3. 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:

  1. Data API reads, creates, updates, and deletes data in the current tenant scope. Process-related endpoints (processes, statuses, parameters, timers, transitions, inbox entries, and approval 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.
  2. 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 Found with the same public message as a missing process. rpc/is-process-exists returns false for a process from another tenant.
  3. 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;
🔑 Get Trial Key

Alternatively, you can use an AI agent (Claude, Codex, GitHub Copilot, Cursor, or similar) to generate a trial key automatically. Provide the agent with the contents of trial.workflowengine.io/llms.txt and follow the instructions.

Stay in the know
Build Workflow Applications Faster
Star us on GitHub