Skip to Content
Evaluate Get Started Plugins Glossary

Approval History

Key takeaways

Approval history explains how a document moves through approval. Pending stages show who can act; completed stages show what happened, when, any comment, and who acted when an executor was recorded. Use ProcessId for one document's timeline and IdentityId for one person's completed approval activity.

ApprovalPlugin approval history supplies the data for a document approval timeline. It shows which approval stages are complete, who performed each action, when it happened, and any recorded comment. It can also show predicted stages and the people who may act next.

Query history by process ID when a document owner, approver, or auditor needs the full timeline. Query by identity ID when the application needs an "Approved by me" activity view containing the completed actions recorded for one executor.

Build a document approval timeline

An approval timeline helps document owners, approvers, and auditors see where a document is in the approval process and how it reached that point. It distinguishes approvals still waiting for a decision from completed decisions, including who may act at each upcoming stage, who handled completed stages, when each action happened, and any comment.

This example follows the path in which the manager completes the first approval and the boss completes the final approval:

Document approval timelineThree snapshots show a document after submission, after manager approval, and after boss approval. Pending stages identify who can approve next. Completed stages identify who approved.DOCUMENT APPROVALSee who can act and who has approvedPENDINGCOMPLETED1AFTER SUBMISSIONDocument is waiting for approvalFirst approvalManager or Boss can approvePENDINGFinal approvalBoss approval follows the first stagePENDINGAFTER FIRST APPROVALManager completes the first approvalFirst approvalApproved by ManagerCOMPLETEDFinal approvalBoss can approve nextPENDINGAFTER FINAL APPROVALBoss completes the final approvalFirst approvalApproved by ManagerCOMPLETEDFinal approvalApproved by BossCOMPLETEDCompleted decisions stay visible as the document moves forward.

On a document page, present entries in Sort order. Use pending entries to explain the next approval stages and completed entries as the audit trail. For a personal activity screen, query by executor identity and show only that person's completed entries. Approval history does not provide document titles or other business fields, so the application loads those fields by ProcessId.

Compare the two histories

Workflow Engine transition history and ApprovalPlugin approval history can describe the same executed command, but they answer different questions. The transition history is the technical path of the process. WorkflowApprovalHistory is an approval-oriented projection for application screens.

QuestionWorkflowProcessTransitionHistoryWorkflowApprovalHistory
What does it represent?Each persisted transition that firedPredicted and completed approval stages
Does it show future approvers?NoYes, through pending rows and AllowedTo
Does it store Commentary?NoYes
Does ApprovalPlugin use it for outbox?NoYes, completed rows are grouped into outbox summaries
What is its main use?Reconstruct the process path and state changesShow who can approve and who already approved

Use transition history for a general execution audit. Use approval history for a document approval timeline, eligible-approver display, or "approved by me" view. Keeping both is expected because approval history adds pending-stage and business-facing data that a fired-transition record does not contain.

Query approval history

Application code queries ApprovalHistoryItem records through IApprovalProvider. Filter by process ID for a document timeline or by completed executor identity for an "Approved by me" view.

MethodResult
GetApprovalHistoryByProcessIdAsyncReturns pending and completed history for one process
GetApprovalHistoryCountByProcessIdAsyncCounts history rows for one process
GetApprovalHistoryByIdentityIdAsyncReturns completed history recorded for one executor identity
GetApprovalHistoryCountByIdentityIdAsyncCounts completed history rows for one executor identity
ApprovalHistoryReader.cs
using OptimaJet.Workflow.Core.Persistence;using OptimaJet.Workflow.Core.Runtime;public sealed class ApprovalHistoryReader{    private readonly WorkflowRuntime _runtime;    public ApprovalHistoryReader(WorkflowRuntime runtime)    {        _runtime = runtime;    }    public Task<List<ApprovalHistoryItem>> GetProcessPageAsync(        Guid processId,        int pageIndex,        int pageSize)    {        return _runtime.PersistenceProvider            .GetApprovalHistoryByProcessIdAsync(                processId,                CreatePaging(pageIndex, pageSize));    }    public Task<List<ApprovalHistoryItem>> GetIdentityPageAsync(        string identityId,        int pageIndex,        int pageSize)    {        return _runtime.PersistenceProvider            .GetApprovalHistoryByIdentityIdAsync(                identityId,                CreatePaging(pageIndex, pageSize));    }    private static Paging CreatePaging(int pageIndex, int pageSize)    {        if (pageIndex < 1)        {            throw new ArgumentOutOfRangeException(nameof(pageIndex));        }        if (pageSize < 1)        {            throw new ArgumentOutOfRangeException(nameof(pageSize));        }        return Paging.Create(pageIndex, pageSize);    }}

Use GetApprovalHistoryCountByProcessIdAsync or GetApprovalHistoryCountByIdentityIdAsync to calculate page counts. Identity queries match IdentityId, so they return completed entries for that executor and do not return pending rows whose identity is empty.

Paging.Create does not validate its arguments. For direct provider calls, pass a page index and page size greater than zero; the workflow action disables paging when either configured value is below one.

Tenant scope: identity-based history methods have no tenant argument. Their result and count include every record with the matching IdentityId in a shared provider store, and paging is applied before application-side tenant filtering. Use identity values that are unique across tenants or a tenant-aware data-access implementation. The identity branch of the GetApprovalHistory action inherits the same scope.

Read history through Web API

Workflow Engine HTTP API reference documents the Data API operations that expose stored approval history. The default base path is /workflow-api; an application can configure another base path.

Method and pathResult
POST /workflow-api/search/processes/approvalsSearches approval rows across processes in the current HTTP tenant
GET /workflow-api/data/processes/{processId}/approvalsReturns a filtered, sorted, and paged process history
GET /workflow-api/data/processes/{processId}/approvals/{id}Returns one approval row
DELETE /workflow-api/data/processes/{processId}/approvalsDeletes matching rows for one process
DELETE /workflow-api/data/processes/{processId}/approvals/{id}Deletes one approval row

The search request accepts search, nested filters, sorts, skip, and take. The response contains collection and total. This request returns the latest completed approval rows for one identity:

curl --request POST \  --url "https://example.com/workflow-api/search/processes/approvals" \  --header "Authorization: Bearer $TOKEN" \  --header "Content-Type: application/json" \  --header "Workflow-Api-Tenant-ID: acme" \  --data '{    "filters": [      {        "type": "Equal",        "field": "IdentityId",        "value": "manager-42"      }    ],    "sorts": [      {        "field": "TransitionTime",        "direction": "Desc"      }    ],    "skip": 0,    "take": 20  }'

The host application determines the authentication scheme and tenant selection. When Workflow API security is enabled, the caller's permissions must allow the route's operation ID and, in multi-tenant mode, the current tenant.

An IdentityId filter is request data; Workflow API does not compare it with the authenticated principal. If users may read only their own approval records, enforce that rule in the host authorization layer or expose a host-owned endpoint that supplies the identity.

HTTP API reads and deletes provider records directly. It does not execute FillApprovalHistory, GetApprovalHistory, or another ApprovalPlugin action, and it does not provide create or update operations for approval rows. Register ApprovalPlugin in each runtime that must keep these rows populated. Treat the DELETE operations as administrative data operations: deleted completed history changes the audit timeline and derived outbox and is not automatically backfilled.

Use the history action

The GetApprovalHistory workflow action performs the same process-or-identity query and writes the resulting List<ApprovalHistoryItem> to the process parameter configured by ParameterName.

GetApprovalHistory action parameters
ParameterRequiredDefaultDescription
GetParameterTypeYes-Selects ProcessId or IdentityId
GetParameterValueYes-Supplies the process GUID or executor identity
ParameterPurposeYes in DesignerPersistenceControls how Workflow Engine stores the output parameter; the handler uses the default when it is absent
ParameterNameYes-Names the output process parameter
PageIndexNo-1Uses a 1-based page; values below 1 disable paging
PageSizeNo-1Enables paging only when both page values are positive

Record comments and names

NameParameterForComment connects a command parameter to ApprovalHistoryItem.Commentary. Attach the parameter before executing the command; the default name is Comment.

ApprovalCommandService.cs
using OptimaJet.Workflow.Core.Runtime;public sealed class ApprovalCommandService{    private readonly WorkflowRuntime _runtime;    public ApprovalCommandService(WorkflowRuntime runtime)    {        _runtime = runtime;    }    public async Task<CommandExecutionResult> ApproveAsync(        Guid processId,        string identityId,        string comment)    {        var commands = await _runtime.GetAvailableCommandsWithConditionCheckAsync(            processId,            identityId);        WorkflowCommand command = commands.Single(            item => item.CommandName == "Approve");        command.SetParameter("Comment", comment, persist: false);        return await _runtime.ExecuteCommandWithRestrictionCheckAsync(            command,            identityId,            identityId);    }}

When localized names are enabled, ApprovalPlugin asks WorkflowRuntime for localized state and command names. If no localization exists, it stores the technical state, activity, or command name. Set UseLocalizedNamesInApprovalHistory to false to always store technical names.

During pre-execution, GetUserNamesByIds can map eligible identity IDs to display values for AllowedTo. The delegate is synchronous. If it is not configured, ApprovalPlugin uses WorkflowRuntime.GetUserByIdentityAsync when available and otherwise keeps the identity IDs.

ApprovalPluginNames.cs
using OptimaJet.Workflow.Plugins;public static class ApprovalPluginNames{    public static ApprovalPlugin Create(        IReadOnlyDictionary<string, string> displayNames)    {        return new ApprovalPlugin        {            GetUserNamesByIds = identityIds => identityIds                .Select(identityId => displayNames.TryGetValue(                    identityId,                    out var displayName)                    ? displayName                    : identityId)                .ToList()        };    }}

GetUserNamesByIds changes only the values stored in ApprovalHistoryItem.AllowedTo. Actor rules, inbox rows, command lookup, and command authorization continue to use workflow identity IDs. Return the identity ID as a fallback when the application has no display name for a value.

Known limitation - commas in AllowedTo values: every built-in persistence provider stores AllowedTo as comma-separated text. Identity IDs and display names that contain commas do not round-trip as one item, so use values without commas.

Interpret history entries

An ApprovalHistoryItem connects one process instance to a predicted stage or completed execution. ApprovalPlugin stores the item in WorkflowApprovalHistory through the configured persistence provider.

ApprovalHistoryItem properties
PropertyPending entryCompleted entry
IdUnique history record IDPreserved for a match, or newly generated for an inserted completed record
ProcessIdProcess whose path was predictedProcess whose execution produced the record
TenantIdTenant of the process instanceRetained for a match, or copied from the process when a completed record is inserted
IdentityIdEmptyExecutor identity when supplied; it can be empty for a timer
AllowedToEligible identities or mapped display namesValues retained from a matching pending row; empty when a completed row is inserted without a match
TransitionTimenullRuntime date and time of execution
SortSequence allocated by ApprovalPluginRetained from a match, or newly allocated when inserted
InitialStatePredicted source state or activityActual source state or activity
DestinationStatePredicted destination state or activityActual destination state or activity
TriggerNamePredicted command name, or Unknown when CurrentCommand is emptyCommand, Timer: <name>, or SetState
CommentaryEmptyValue read from NameParameterForComment

For every incoming item, the provider looks for a pending record whose ProcessId, InitialState, and DestinationState match. The predicate does not include TriggerName or AllowedTo. The provider updates a match or inserts the incoming pending or completed record when no match exists.

For a match, each built-in persistence provider updates only TriggerName, TransitionTime, IdentityId, and Commentary; it retains the existing Id, TenantId, AllowedTo, and Sort values.

Known limitation - ambiguous pending matches: if several pending records have the same process, initial state, and destination state, the provider updates the first matching record it returns. Matching does not consider the trigger and has no explicit ordering, so do not rely on a specific pending row being completed when a scheme can produce this pattern.

Record delegated command identities

WorkflowRuntime separates the identity that initiates a command from the workflow participant on whose behalf it runs. The identityId argument identifies the initiator, while impersonatedIdentityId identifies the represented participant. The ExecuteCommandWithRestrictionCheckAsync method validates actor restrictions against impersonatedIdentityId.

ApprovalPlugin records the initiator in the completed ApprovalHistoryItem.IdentityId. It does not copy ProcessInstance.ImpersonatedIdentityId into approval history. For a command initiated by administrator-1 on behalf of manager-42, the resulting identities are:

PurposeIdentity
Actor restriction checkmanager-42
Completed history IdentityIdadministrator-1
Outbox that receives the completed actionadministrator-1
Pending AllowedTo valueThe eligible identity or display value predicted before execution, such as manager-42

Retrieve the available command for manager-42, then pass administrator-1 as identityId and manager-42 as impersonatedIdentityId when executing it. Retrieving the command for the administrator instead would test the administrator's own actor restrictions rather than the represented participant's restrictions.

This split lets approval history audit who initiated the operation while WorkflowRuntime authorizes the represented participant. An application that needs both values in its audit UI must store impersonatedIdentityId separately because ApprovalHistoryItem has no property for it.

Follow the history lifecycle

Automatic history combines scheme building, pre-execution, and normal process execution. The build step inserts the same FillApprovalHistory action into both execution modes for each eligible activity with a non-blank state.

Automatic approval history lifecycleThe runtime prepares the scheme, predicts pending approvals, records a completed decision, and refreshes pending stages while retaining completed history.AUTOMATIC APPROVAL HISTORYPrediction and execution keep the timeline current1SCHEME BUILDPrepare the schemeThe builder adds FillApprovalHistory to eligible stateactivities in both execution modes.ACTIONADDED2PRE-EXECUTIONPredict upcoming approvalsPre-execution records upcoming stages and theidentities eligible to approve.PENDINGWHO CAN ACT3PROCESS EXECUTIONRecord the completed decisionCommand, timer, or SetState execution updates amatching prediction or inserts a completed row.COMPLETEDWHO ACTED4HISTORY REFRESHRefresh what comes nextObsolete pending rows are deleted before predictionruns again from the new activity.KEEP COMPLETEDREPLACE PENDINGPrediction, recording, and refresh repeat as the process advances.

Before pre-execution starts again, ApprovalPlugin deletes history rows whose TransitionTime is null. This refresh prevents predictions from an obsolete branch from remaining after the process takes a different route.

Pending-history replacement uses separate cleanup and pre-execution operations. If pre-execution fails after cleanup, completed history remains, but pending predictions can be missing or incomplete until a later refresh succeeds.

SetStateAsync participates in this lifecycle only when execution is enabled. With SetStateParams.PreventExecution = true, the runtime changes the current activity without running its implementation or invoking ApprovalPlugin's refresh handler. ApprovalPlugin therefore does not write completed history or replace existing pending history and inbox rows during that operation.

ApprovalPlugin persists an internal WorkflowPluginApprovalHistoryOrder process parameter. Each history record written by FillApprovalHistory uses the current value as Sort and then increments it, so history queries can return the process sequence in ascending order.

Compare execution paths

ApprovalPlugin records different data according to how the process advances. The first table shows what appears in Approval History. The second shows how the same path affects Inbox and Outbox. Worklists refresh only when root process execution reaches its idled or finalized handling point.

Approval History records

Execution pathApproval History result
ApprovalPlugin pre-execution refreshCreates a pending prediction. The trigger is the predicted command or Unknown; identity is empty.
CommandCreates a completed row. The trigger is the command name; identity is the command identityId.
TimerCreates a completed row. The trigger is Timer: <name>; identity is empty because timer execution clears it.
SetState with executionCreates a completed row. The trigger is SetState; identity is SetStateParams.IdentityId.
SetState with PreventExecution = trueCreates no completed row and records neither trigger nor identity.

Inbox and Outbox results

Execution pathWorklist result
ApprovalPlugin pre-execution refreshThe plugin handler rebuilds Inbox after prediction. A pending row does not contribute to Outbox.
CommandThe root process refreshes Inbox. The recorded executor appears in Outbox.
TimerThe root process refreshes Inbox. Outbox has no per-user entry for the timer.
SetState with executionThe root process refreshes Inbox. The supplied executor appears in Outbox.
SetState with PreventExecution = trueInbox is not refreshed, and Outbox receives no new contribution.

Configure automatic history

AutoApprovalHistory controls whether ApprovalPlugin installs AutoApprovalHistoryBuilderStep during registration. Its default value is true, which also removes FillApprovalHistory from normal Designer action lists while the value remains true.

Set AutoApprovalHistory to false before registration when the scheme must place FillApprovalHistory explicitly. Set DisableApprovalHistory to true before registration to prevent build-step installation; the runtime handler also checks this property each time and skips pending-row cleanup and history pre-execution while it is true. The action handler does not check DisableApprovalHistory, so a FillApprovalHistory action already present in a scheme can still write history.

Approval history switches
SettingDefaultEffect
AutoApprovalHistorytrueInstalls the build step at registration and hides FillApprovalHistory from ordinary Designer action lists
DisableApprovalHistoryfalsePrevents build-step installation when set before registration and skips the handler's history refresh while true
UseLocalizedNamesInApprovalHistorytrueResolves localized state and command names before storing them
NameParameterForComment"Comment"Selects the process parameter copied into Commentary

Changing either switch after registration does not remove an installed build step or an action already injected into a built scheme.

FillApprovalHistory ignores ordinary execution when both CurrentCommand and ExecutedTimer are empty.

Record history manually

Set AutoApprovalHistory to false before WithPlugin when the scheme must control which activities write approval history. Leave DisableApprovalHistory set to false when ApprovalPlugin should still clear obsolete predictions and run pre-execution before the root process becomes idled or finalized.

Add FillApprovalHistory to each state activity that the application needs to track. Its placement determines which part of the history lifecycle the activity records.

Action placementRecorded result
PreExecutionImplementation onlyPending rows with predicted states and AllowedTo; the action does not complete those rows during normal execution
Implementation onlyCompleted rows for command, timer, or SetState execution; a newly inserted row has an empty AllowedTo when no pending match exists
Both implementationsPending rows followed by completed rows, matching the automatic build-step behavior

Adding the action to both implementations is the manual equivalent of AutoApprovalHistoryBuilderStep. Setting DisableApprovalHistory to true does not block an explicitly placed FillApprovalHistory action during ordinary execution, but it does prevent the plugin handler from starting the pre-execution refresh that normally runs the manually placed pre-execution action.

See also

Frequently asked questions

Why does ApprovalPlugin create history rows before a command executes?

ApprovalPlugin uses pre-execution to write predicted stages and eligible identities. Those records have a null TransitionTime until execution updates a matching stage.

How does ApprovalPlugin match a completed execution to a pending row?

The persistence provider matches the process ID, initial state, and destination state. It updates a matching pending row or inserts a completed row when none exists; the match does not include the trigger name.

Which identity does ApprovalPlugin record for an impersonated command?

ApprovalPlugin records the command initiator from identityId. WorkflowRuntime checks actor restrictions for impersonatedIdentityId, but ApprovalPlugin does not store that represented identity in ApprovalHistoryItem.

How do I add an approval comment to history?

Set a command parameter whose name matches NameParameterForComment before executing the command. ApprovalPlugin uses Comment by default.

Can I disable approval history without disabling inbox updates?

Yes. Set DisableApprovalHistory to true before registering the plugin and keep DisableInbox set to false. ApprovalPlugin continues rebuilding inbox entries, but a FillApprovalHistory action already present in a scheme can still write history.