Skip to main content

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

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​

SelectorOperation family
api.Liveness, api.Readiness, api.TenantReadinessHealth checks
api.Search.ProcessesSearch operations
api.Data.Processes.GetData operations; nested groups include parameters, timers, transitions, approvals, and inbox entries
api.Rpc.ExecuteCommandRuntime operations, including separate bulk operations
api.Designer.Get, api.Designer.PostDesigner requests
api.Forms.GetFormForms 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.

StageBehavior
OnBeforenull continues. An IResult skips the remaining preparation, callback authorization, and handler, then passes through OnAfter.
OnAuthorizeChecks the final prepared input. All callbacks must allow the request; the first false returns 403 and skips the handler. OnAfter still runs.
Built-in handlerExecutes once with the prepared logical input.
OnAfterSuccessTransforms a non-null payload from the original handler's 2xx response, preserving status, content type, and headers.
OnAfterTransforms 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.

🔑 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