Forms Plugin
The Forms Plugin integration adds ready-to-use Forms endpoints to the Workflow Engine Web API and attaches a
FormsPlugin instance to every Workflow Runtime created by the Web API. For an ASP.NET application, this removes the
need to implement a Forms controller for the standard form retrieval, save, validation, and execution scenarios.
For an overview of forms, the viewer and manager components, and workflow configuration, see the Forms documentation.
Use operation callbacks to customize these four operations. The Forms sample demonstrates template authorization and server-side fallback without changing the library defaults.
Integration
Start with the application from the Web API Setup guide, then make the following additions.
Install the Forms Plugin Web API package
dotnet add package OptimaJet.Workflow.Api.Forms
The package contains both the Forms Plugin runtime integration and the Web API operations.
Register the Forms Plugin
Add the Forms Plugin namespace to Program.cs:
using OptimaJet.Workflow.Api.Forms;
Register the plugin after the Workflow Engine Web API core and runtime services. The relevant part of the
Program.cs example from Web API Setup becomes:
using OptimaJet.Workflow.Api;
using OptimaJet.Workflow.Api.Options;
using OptimaJet.Workflow.Api.Sqlite;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.Api.Forms;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddWorkflowApiSqlite();
builder.Services.AddWorkflowApiCore(SetupWorkflowApiCore);
builder.Services.AddWorkflowRuntime(SetupWorkflowRuntime);
builder.Services.AddFormsPlugin(SetupFormsPlugin);
var app = builder.Build();
app.UseRouting();
app.MapWorkflowApi();
app.Run();
void SetupWorkflowApiCore(WorkflowApiCoreOptions options)
{
options.LicenseKey = "PUT YOUR LICENSE KEY HERE";
}
void SetupWorkflowRuntime(WorkflowTenantCreationOptions options)
{
options.ConnectionString = "Data Source=workflow_engine.db";
options.WorkflowRuntimeCreationOptions.ConfigureWorkflowRuntime = runtime =>
{
runtime.AsSingleServer();
};
}
void SetupFormsPlugin(FormsPluginCreationOptions options)
{
options.FormsManagerUrl = "https://your-application.example.com/forms-manager";
}
MapWorkflowApi() maps the Forms endpoints together with the other Workflow Engine Web API endpoints. No additional
controller registration or endpoint mapping is required.
The same registration works in multi-tenant mode. The Web API runtime configurator attaches an independently configured
Forms Plugin instance to every tenant's WorkflowRuntime, including tenants registered dynamically.
Forms Plugin Options
Configure the integration through FormsPluginCreationOptions, passed to AddFormsPlugin.
| Name | Type | Default | Description |
|---|---|---|---|
| FormsManagerUrl | string | "" | URL of the Forms Manager page opened from Workflow Designer. Set it to the Forms Manager route in your application when users edit forms from the process scheme designer. |
| FormsProcessParameterName | string | "FormsPlugin_ProcessForms" | Name of the process parameter that stores the Forms Plugin metadata describing which forms are associated with workflow activities and commands. |
| FormVersionPropertyName | string | "__formVersion" | Name of the property in form data used for optimistic concurrency checks when form data is saved or executed. |
| DefaultFormDefinition | string | "{}" | Form definition returned by the plugin when an empty/default form definition is needed. |
| FormValidator | IFormValidator? | null | Server-side validator used by save and execute operations. When null, the Forms Plugin creates its default validator. |
To add custom server-side validation, implement IFormValidator and assign it during registration:
void SetupFormsPlugin(FormsPluginCreationOptions options)
{
options.FormsManagerUrl = "https://your-application.example.com/forms-manager";
options.FormValidator = new MyFormValidator();
}
See Server-Side Form Validation for the validator contract and an implementation example.
Important Behavior
Endpoint paths
All Forms operations use POST and are mapped under the Web API base path. With the default
WorkflowApiCoreOptions.BasePath, the endpoints start with /workflow-api/forms/. If you change BasePath, the Forms
paths change with the rest of the Web API.
Form data pipelines and property names
Form data is sent as a JSON object. Form Engine binds values and validation errors to components by their keys, including letter case. The Forms API preserves names and values when no form data pipeline steps are registered.
Implement IFormsApiFormDataPipelineStep to transform dynamic form data at the HTTP boundary. Register each
implementation with AddFormDataPipelineStep<TStep>(order); it is created as a singleton and can use constructor
injection like any other service. The Forms API owns and registers the internal pipeline that composes the steps:
public sealed class AuditingFormsApiFormDataPipelineStep(
ILogger<AuditingFormsApiFormDataPipelineStep> logger) : IFormsApiFormDataPipelineStep
{
public Dictionary<string, object?> ConvertToRuntime(
IReadOnlyDictionary<string, object?> formData)
{
logger.LogDebug("Converting form data for the Forms runtime.");
return formData;
}
public Dictionary<string, object?> ConvertToClient(
IReadOnlyDictionary<string, object?> formData)
{
logger.LogDebug("Converting form data for the HTTP client.");
return formData;
}
}
builder.Services.AddFormDataPipelineStep<AuditingFormsApiFormDataPipelineStep>(order: 100);
The Forms API applies the pipeline in these places:
- Inbound: form data in
save-formandexecute-form, before it reaches the Forms runtime. - Outbound: process form data, executable form data, saved form data, and save/execute validation error objects.
Multiple implementations are composed by their registration order value. Inbound conversion runs in ascending order, using DI registration order for equal values. Outbound conversion runs in the exact reverse order, which lets each implementation undo its corresponding inbound transformation. Consequently, equal-order implementations run in reverse registration order on the way back to the client.
Each step accepts a string-keyed dictionary and returns a dictionary. Values can still contain nested dictionaries,
arrays, scalar values, and null. A step can inject IOptions<FormsPluginCreationOptions> when its conversion depends
on settings such as FormVersionPropertyName. The standard package includes CamelCaseFormsApiFormDataPipeline for
recursive lowerCamelCase/PascalCase conversion; register it explicitly when the client contract needs that behavior.
Validation
save-form and execute-form call the configured IFormValidator. Validation failures are returned as the standard
Error envelope with 400 Bad Request, code ValidationError, and an errors dictionary. A successful save returns
the normalized form data; a successful execute returns whether the workflow command was executed.
Runtime errors
Forms runtime failures are returned as typed Error responses. The API does not throw a Forms-specific exception for
these expected failures, so the response body remains available independently of the application's exception handler
configuration.
| Status | Error code | When it is returned |
|---|---|---|
400 Bad Request | BadRequest | The submitted form data is invalid, a form version check fails, or another request value cannot be accepted. |
400 Bad Request | ValidationError | The configured form validator rejected one or more form values. |
404 Not Found | NotFound | A requested form, command, or process is missing or is not accessible to the current tenant. |
500 Internal Server Error | SystemError | Forms Plugin configuration or stored runtime state is inconsistent, or command execution fails internally. |
The JSON response contains code and message properties. Validation errors additionally contain an errors
dictionary whose keys match form component names. Unexpected exceptions outside the typed Forms runtime result continue
through the application's standard ASP.NET exception handling.
Multitenancy
The Forms API uses the same tenant resolution as the rest of the Workflow Engine Web API. In multi-tenant mode, send the
tenant in the Workflow-Api-Tenant-ID header. See Multitenancy for tenant registration and
request resolution.
get-formresolves a tenant-specific form first and can fall back to a shared form when the Forms Plugin supports a shared definition for the requested key.get-executable-forms,save-form, andexecute-formvalidate that the process belongs to the current tenant. A process from another tenant is exposed in the same way as a missing process and returns404 Not Found.
Do not use a tenant ID from a request body or query string as an authorization decision. Let the Web API resolve the tenant from its configured request context and, when security is enabled, from tenant permissions.
Security
When security services are enabled, the Forms operations participate in the standard
Workflow Engine Web API authorization model. The common permission branch is workflow-api.forms; individual operation
IDs are listed in the API reference below.
For example, the following permission configuration allows only the Forms API and all tenants:
var claim = permissions.BuildClaim(builder => builder
.DenyAllOperations()
.Allow("workflow-api.forms")
.AllowAllTenants());
API Reference
AddFormsPlugin(this IServiceCollection services, Action<FormsPluginCreationOptions>? setupAction)
Adds the Forms Plugin runtime integration and Forms API operations to the Workflow Engine Web API.
Parameters
| Name | Type | Description |
|---|---|---|
| services | IServiceCollection | The ASP.NET service collection. |
| setupAction | Action<FormsPluginCreationOptions>? | A delegate that configures Forms Plugin creation options. Pass null to use all default values. |
Returns
| Type | Description |
|---|---|
IServiceCollection | The service collection, which can be used for further configuration. |
IFormsApiFormDataPipelineStep
Defines one application-provided form data transformation in the OptimaJet.Workflow.Api.Forms namespace.
| Member | Description |
|---|---|
Dictionary<string, object?> ConvertToRuntime(IReadOnlyDictionary<string, object?> formData) | Converts client data before save-form or execute-form passes it to the Forms runtime. |
Dictionary<string, object?> ConvertToClient(IReadOnlyDictionary<string, object?> formData) | Converts runtime data before the Forms API returns form data or validation errors to the client. |
Register zero or more implementations with AddFormDataPipelineStep<TStep>(order). With no registrations, the
Forms API leaves form data unchanged.
Forms operations
The paths below are relative to WorkflowApiCoreOptions.BasePath.
| Operation ID | Method | Path | Description |
|---|---|---|---|
workflow-api.forms.get-form | POST | forms/get-form | Retrieves a standalone form definition by name and optional version. |
workflow-api.forms.get-executable-forms | POST | forms/get-executable-forms | Retrieves the forms and commands currently available for a process. |
workflow-api.forms.save-form | POST | forms/save-form | Validates and saves form data without executing a command. |
workflow-api.forms.execute-form | POST | forms/execute-form | Validates form data and executes the selected workflow command. |
The Forms HTTP API exposes these four operations. The former get-process-form endpoint and its generated HTTP-client
method have been removed. FormsRuntimeApi.GetProcessFormAsync(GetProcessFormParameters) remains available in the
server-side FormsPlugin SDK; applications choose DataParameterName in trusted server code.
All operations can return the typed Error responses described in Runtime errors. Form validation
failures from save-form and execute-form use the same envelope with Code = "ValidationError" and field messages in
Errors.
get-form
Request
| Name | Type | Required | Description |
|---|---|---|---|
| FormKey | FormKey | Yes | Name and optional version of the form to return. |
Response
| Name | Type | Description |
|---|---|---|
| Form | Form | The resolved standalone form. |
get-executable-forms
Request
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| ProcessId | Guid | Yes | — | ID of the process. |
| IdentityId | string? | No | null | Identity used to determine available commands. |
| CommandNameFilter | string? | No | null | Command name used to restrict the returned forms and commands. |
| ConditionCheck | bool | No | false | Whether command conditions are evaluated when resolving executable forms. |
| Culture | string? | No | null | .NET culture name used for localized command names. |
Response
| Name | Type | Description |
|---|---|---|
| Forms | List<ExecutableForm> | Forms and commands available for the process. |
save-form
Request
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| ProcessId | Guid | Yes | — | ID of the process. |
| FormKey | FormKey | Yes | — | Name and optional version of the form. |
| FormData | Dictionary<string, object?> | Yes | — | JSON object containing the form values. |
| IdentityId | string? | No | null | Identity used to check access to the form and commands. |
| CommandNameFilter | string? | No | null | Command name used to restrict access checks. |
| ConditionCheck | bool | No | false | Whether command conditions are evaluated before the form is saved. |
Response
| Name | Type | Description |
|---|---|---|
| Data | Dictionary<string, object?> | The saved and normalized form data. |
execute-form
Request
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| ProcessId | Guid | Yes | — | ID of the process. |
| FormKey | FormKey | Yes | — | Name and optional version of the form. |
| CommandName | string | Yes | — | Workflow command to execute. |
| FormData | Dictionary<string, object?> | Yes | — | JSON object containing the form values. |
| IdentityId | string? | No | null | Identity executing the command. |
| ImpersonatedIdentityId | string? | No | null | Identity on whose behalf the command is executed, if required. |
Response
| Name | Type | Description |
|---|---|---|
| WasExecuted | bool | Whether the workflow command was executed. |
Common response models
| Model | Properties | Description |
|---|---|---|
FormKey | FormName: string, FormVersion: int? | Identifies a form and, optionally, a specific version. |
Form | FormKey: FormKey, FormCode: string | A standalone form definition. |
ProcessForm | ProcessId: Guid, FormKey: FormKey, FormCode: string, FormData: Dictionary<string, object?> | A form definition with data bound to a process. |
ExecutableForm | ProcessId: Guid, FormKey: FormKey, FormCode: string, FormData: Dictionary<string, object?>, AllowSave: bool, Commands: List<FormCommand> | A process form together with its available actions. |
FormCommand | CommandName: string, LocalizedName: string | A workflow command displayed for an executable form. |
Error | Code: string, Message: string, Errors: Dictionary<string, string[]>? | The standard API error envelope. Errors is populated for validation failures. |
The generated OpenAPI document uses this single Error schema for every Forms 400 response, including validation.
For a complete MSSQL weekly-report application using these endpoints directly from React, see the FormsPlugin Web API sample.