Operation Callbacks
Workflow Engine Web API callbacks add application-specific input preparation, resource authorization,
response processing, and exception recovery around an existing operation. Register them through
ConfigureWorkflowApi before calling MapWorkflowApi. The built-in handler and its HTTP contract remain
in place.
Start with Web API Setup. The libraries support .NET 8 and .NET 10; build consumers with
the .NET 10 SDK and C# 14 to use the api.Forms extension property.
Register callbacks
Add the following configuration to your existing service registration. Keep your provider, license, runtime, tenant, and security configuration.
using Microsoft.AspNetCore.Http;
using OptimaJet.Workflow.Api;
builder.Services.ConfigureWorkflowApi(workflow =>
{
workflow.ConfigureOperation(api => api.Rpc.ExecuteCommand)
.OnBefore(context =>
{
context.ReplaceInput(context.Input with
{
IdentityId = context.User.Identity?.Name
});
return ValueTask.FromResult<IResult?>(null);
})
.OnAuthorize(context => ValueTask.FromResult(
context.User.Identity?.IsAuthenticated == true
))
.OnAfter((context, result) =>
{
context.HttpContext.Response.Headers["X-Workflow-Operation"] = context.OperationId;
return ValueTask.FromResult(result);
});
});
IdentityId must use the identity format expected by your workflow rules. If your application uses a
claim other than User.Identity.Name, read that claim instead. Explicitly handle impersonation according
to your application's rules; replacing IdentityId does not authorize a caller to impersonate another user.
Configuration actions run once per service provider, in registration order, after all module defaults have been loaded. They can be registered before or after Core and Forms services. Selecting an operation again appends callbacks to its existing configuration. Mapping freezes the configuration; retained builders cannot change it afterward. Separate application hosts have separate configurations.
Select an operation
| Selector | Operation family |
|---|---|
api.Liveness, api.Readiness, api.TenantReadiness | Health checks |
api.Search.Processes | Search operations |
api.Data.Processes.Get | Data operations; nested groups include parameters, timers, transitions, approvals, and inbox entries |
api.Rpc.ExecuteCommand | Runtime operations, including separate bulk operations |
api.Designer.Get, api.Designer.Post | Designer requests |
api.Forms.GetForm | Forms operations, after AddFormsPlugin and using OptimaJet.Workflow.Api.Forms |
Callbacks receive public DTOs from OptimaJet.Workflow.Api.Models or
OptimaJet.Workflow.Api.Forms.Models. IWorkflowApiBuilder, IOperationBuilder<TInput, TSuccess>, and
IOperationContext<TInput> live in OptimaJet.Workflow.Api.Configuration; their implementations are internal.
The operation ID is permanent and identifies the same operation in permissions and endpoint metadata.
Understand execution order
ASP.NET authentication and authorization run first, followed by HTTP binding. The operation pipeline then runs these stages in a fixed order, regardless of the order of different fluent method calls. Callbacks within one stage run in registration order.
| Stage | Behavior |
|---|---|
OnBefore | null continues. An IResult skips the remaining preparation, callback authorization, and handler, then passes through OnAfter. |
OnAuthorize | Checks the final prepared input. All callbacks must allow the request; the first false returns 403 and skips the handler. OnAfter still runs. |
| Built-in handler | Executes once with the prepared logical input. |
OnAfterSuccess | Transforms a non-null payload from the original handler's 2xx response, preserving status, content type, and headers. |
OnAfter | Transforms the final IResult before execution and serialization, including handler errors, short circuits, authorization denials, and recovered exceptions. |
ReplaceInput is allowed only in OnBefore. Input includes the complete logical request: route,
query, and body values. Replacement reaches the next callback and the built-in handler without a JSON
round trip. Authorization callbacks must treat the input and nested mutable values as read-only.
OnAfterSuccess skips errors, short circuits, authorization denials, exception fallbacks, and responses
without a payload, including null payloads. An empty response record serialized as {} is a payload.
Readiness with HTTP 503 is an error even when its payload has the same type as the HTTP 200 response.
Authorize access to a resource
OnAuthorize supplements existing ASP.NET and Workflow API authorization. It also runs when built-in
Workflow API security is disabled. A callback cannot replace earlier authentication or policies.
To evaluate an application policy against the prepared process ID, resolve IAuthorizationService
from the request scope. Register ReadWorkflowProcess and its authorization handler through the
standard ASP.NET authorization API. Share the resource record with that handler.
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using OptimaJet.Workflow.Api;
builder.Services.ConfigureWorkflowApi(workflow =>
{
workflow.ConfigureOperation(api => api.Data.Processes.Get)
.OnAuthorize(async context =>
{
var authorization = context.Services.GetRequiredService<IAuthorizationService>();
var resource = new ProcessAuthorizationResource(context.Input.Id, context.TenantId);
var result = await authorization.AuthorizeAsync(
context.User,
resource,
"ReadWorkflowProcess"
);
return result.Succeeded;
});
});
public sealed record ProcessAuthorizationResource(Guid ProcessId, string? TenantId);
The policy handler checks the requested resource against your application's access rules.
Tenant resolution, process isolation, license restrictions, and protected global
parameters remain enforced. Filtering a collection in OnAfterSuccess is not a complete access-control
solution: access restrictions must be applied before pagination and counting.
Bulk operations have their own callbacks. Configuring api.Rpc.ExecuteCommand does not configure
api.Rpc.BulkExecuteCommand; a bulk callback must inspect every affected process when required.
Localize a successful response
Register ASP.NET localization and request-localization middleware in your host. Resolve a localizer
inside OnAfterSuccess; the callback works with the successful payload, including union results, without
unwrapping an HTTP result. In this example, CommandMessages is your application's resource marker type.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using OptimaJet.Workflow.Api;
builder.Services.AddLocalization();
builder.Services.ConfigureWorkflowApi(workflow =>
{
workflow.ConfigureOperation(api => api.Rpc.ExecuteCommand)
.OnAfterSuccess((context, response) =>
{
var localizer = context.Services.GetRequiredService<IStringLocalizer<CommandMessages>>();
var key = response.WasExecuted ? "CommandExecuted" : "CommandNotExecuted";
return ValueTask.FromResult(response with { Message = localizer[key].Value });
});
});
Recover from application failures
OnException handles exceptions from preparation, callback authorization, the handler, and response
callbacks. Returning null passes the original exception to the next recovery callback. The first
non-null result handles it; if none does, the exception propagates with its original stack trace.
builder.Services.ConfigureWorkflowApi(workflow =>
{
workflow.ConfigureOperation(api => api.Rpc.ExecuteCommand)
.OnException((_, exception) => ValueTask.FromResult<IResult?>(
exception is TimeoutException
? TypedResults.StatusCode(StatusCodes.Status503ServiceUnavailable)
: null
));
});
A fallback produced before OnAfter passes through that stage once. A fallback for an exception in
OnAfter is sent directly, without repeating callbacks. An exception in a recovery callback is not
passed back into the same recovery chain. Recovery does not retry or roll back the workflow operation.
Request cancellation bypasses recovery. Binding, result execution, serialization, already-started responses, and external middleware are outside its scope. Existing process/tenant guards convert protected process-not-found and tenant-mismatch failures to 404 before user recovery.
Use the request context
Resolve scoped dependencies through context.Services, which is HttpContext.RequestServices.
Use context.CancellationToken for cancellable application work and HttpContext.Items to pass data
between callbacks. Do not capture scoped services or mutable request state in configuration actions.
TenantId captures the normalized Workflow-Api-Tenant-ID header or configured default once when
the context is created. An absent or single-tenant ID becomes null. It is not a tenant-existence or
access check, and changing the header later does not change the captured value. Existing locator and
process/tenant validation still apply.
Work with Forms and OpenAPI
The Forms API exposes GetForm, GetExecutableForms, SaveForm, and ExecuteForm.
The Forms sample uses callbacks to restrict standalone templates and construct a
server-side read-only fallback. It records the original handler's successful empty payload in
OnAfterSuccess, then uses OnAfter to produce the final success or error response. The HTTP
GetProcessForm operation is not available; the trusted server-side SDK method remains available.
Callbacks do not automatically add OpenAPI response declarations for short circuits or exception
fallbacks. Document application-specific responses through your host's generator. The published fluent
API configures callbacks; it does not expose operation path/method overrides, policy replacement, or
Request/Response contract adaptation. Configure the API-wide prefix with WorkflowApiCoreOptions.BasePath.
Workflow API maps operations in the order Root, Search, Data, RPC, Designer, then Forms. To retain that
order with Swashbuckle, configure options.OrderActionsBy(_ => String.Empty) inside AddSwaggerGen.
The Sample and Swagger hosts already do this. Native AddOpenApi retains the mapped order.