# Approval History [ApprovalPlugin](https://docs.workflowengine.io/plugins/approval-plugin/overview/) 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: 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](https://docs.workflowengine.io/get-started/concepts/process-logs/) is the technical path of the process. `WorkflowApprovalHistory` is an approval-oriented projection for application screens. | Question | `WorkflowProcessTransitionHistory` | `WorkflowApprovalHistory` | |----------|------------------------------------|---------------------------| | What does it represent? | Each persisted transition that fired | Predicted and completed approval stages | | Does it show future approvers? | No | Yes, through pending rows and `AllowedTo` | | Does it store `Commentary`? | No | Yes | | Does ApprovalPlugin use it for outbox? | No | Yes, completed rows are grouped into outbox summaries | | What is its main use? | Reconstruct the process path and state changes | Show 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](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/ApprovalHistoryItem/) records through [IApprovalProvider](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/IApprovalProvider/). Filter by process ID for a document timeline or by completed executor identity for an "Approved by me" view. | Method | Result | |--------|--------| | [GetApprovalHistoryByProcessIdAsync](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/IApprovalProvider/#method-getapprovalhistorybyprocessidasync-guid-paging) | Returns pending and completed history for one process | | [GetApprovalHistoryCountByProcessIdAsync](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/IApprovalProvider/#method-getapprovalhistorycountbyprocessidasync-guid) | Counts history rows for one process | | [GetApprovalHistoryByIdentityIdAsync](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/IApprovalProvider/#method-getapprovalhistorybyidentityidasync-string-paging) | Returns completed history recorded for one executor identity | | [GetApprovalHistoryCountByIdentityIdAsync](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/IApprovalProvider/#method-getapprovalhistorycountbyidentityidasync-string) | Counts completed history rows for one executor identity | ```csharp filename="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> GetProcessPageAsync( Guid processId, int pageIndex, int pageSize) { return _runtime.PersistenceProvider .GetApprovalHistoryByProcessIdAsync( processId, CreatePaging(pageIndex, pageSize)); } public Task> 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](https://docs.workflowengine.io/workflow-engine-http-api/#tag/Approvals) 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 path | Result | |-----------------|--------| | [POST /workflow-api/search/processes/approvals](https://docs.workflowengine.io/workflow-engine-http-api/#tag/Approvals/operation/workflow-api.search.processes.approvals) | Searches approval rows across processes in the current HTTP tenant | | [GET /workflow-api/data/processes/{processId}/approvals](https://docs.workflowengine.io/workflow-engine-http-api/#tag/Approvals/operation/workflow-api.data.processes.approvals.get-collection) | Returns a filtered, sorted, and paged process history | | [GET /workflow-api/data/processes/{processId}/approvals/{id}](https://docs.workflowengine.io/workflow-engine-http-api/#tag/Approvals/operation/workflow-api.data.processes.approvals.get) | Returns one approval row | | [DELETE /workflow-api/data/processes/{processId}/approvals](https://docs.workflowengine.io/workflow-engine-http-api/#tag/Approvals/operation/workflow-api.data.processes.approvals.delete-collection) | Deletes matching rows for one process | | [DELETE /workflow-api/data/processes/{processId}/approvals/{id}](https://docs.workflowengine.io/workflow-engine-http-api/#tag/Approvals/operation/workflow-api.data.processes.approvals.delete) | 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: ```bash 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` to the process parameter configured by `ParameterName`. Table: GetApprovalHistory action parameters | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `GetParameterType` | Yes | - | Selects `ProcessId` or `IdentityId` | | `GetParameterValue` | Yes | - | Supplies the process GUID or executor identity | | `ParameterPurpose` | Yes in Designer | `Persistence` | Controls how Workflow Engine stores the output parameter; the handler uses the default when it is absent | | `ParameterName` | Yes | - | Names the output process parameter | | `PageIndex` | No | `-1` | Uses a 1-based page; values below 1 disable paging | | `PageSize` | No | `-1` | Enables 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`. ```csharp filename="ApprovalCommandService.cs" using OptimaJet.Workflow.Core.Runtime; public sealed class ApprovalCommandService { private readonly WorkflowRuntime _runtime; public ApprovalCommandService(WorkflowRuntime runtime) { _runtime = runtime; } public async Task 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. ```csharp filename="ApprovalPluginNames.cs" using OptimaJet.Workflow.Plugins; public static class ApprovalPluginNames { public static ApprovalPlugin Create( IReadOnlyDictionary 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](https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Persistence/ApprovalHistoryItem/) connects one process instance to a predicted stage or completed execution. ApprovalPlugin stores the item in `WorkflowApprovalHistory` through the configured [persistence provider](https://docs.workflowengine.io/get-started/concepts/persistence/). Table: ApprovalHistoryItem properties | Property | Pending entry | Completed entry | |----------|---------------|-----------------| | `Id` | Unique history record ID | Preserved for a match, or newly generated for an inserted completed record | | `ProcessId` | Process whose path was predicted | Process whose execution produced the record | | `TenantId` | Tenant of the process instance | Retained for a match, or copied from the process when a completed record is inserted | | `IdentityId` | Empty | Executor identity when supplied; it can be empty for a timer | | `AllowedTo` | Eligible identities or mapped display names | Values retained from a matching pending row; empty when a completed row is inserted without a match | | `TransitionTime` | `null` | Runtime date and time of execution | | `Sort` | Sequence allocated by ApprovalPlugin | Retained from a match, or newly allocated when inserted | | `InitialState` | Predicted source state or activity | Actual source state or activity | | `DestinationState` | Predicted destination state or activity | Actual destination state or activity | | `TriggerName` | Predicted command name, or `Unknown` when `CurrentCommand` is empty | Command, `Timer: `, or `SetState` | | `Commentary` | Empty | Value 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][api-execute-command-with-restriction-check] method validates actor restrictions against `impersonatedIdentityId`. [api-execute-command-with-restriction-check]: https://api.workflowengine.io/core/OptimaJet.Workflow.Core.Runtime/WorkflowRuntime/#method-executecommandwithrestrictioncheckasync-workflowcommand-string-string-cancellationtoken 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: | Purpose | Identity | |---------|----------| | Actor restriction check | `manager-42` | | Completed history `IdentityId` | `administrator-1` | | Outbox that receives the completed action | `administrator-1` | | Pending `AllowedTo` value | The 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. 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 path | Approval History result | |----------------|-------------------------| | ApprovalPlugin pre-execution refresh | Creates a pending prediction. The trigger is the predicted command or `Unknown`; identity is empty. | | Command | Creates a completed row. The trigger is the command name; identity is the command `identityId`. | | Timer | Creates a completed row. The trigger is `Timer: `; identity is empty because timer execution clears it. | | `SetState` with execution | Creates a completed row. The trigger is `SetState`; identity is `SetStateParams.IdentityId`. | | `SetState` with `PreventExecution = true` | Creates no completed row and records neither trigger nor identity. | ### Inbox and Outbox results | Execution path | Worklist result | |----------------|-----------------| | ApprovalPlugin pre-execution refresh | The plugin handler rebuilds Inbox after prediction. A pending row does not contribute to Outbox. | | Command | The root process refreshes Inbox. The recorded executor appears in Outbox. | | Timer | The root process refreshes Inbox. Outbox has no per-user entry for the timer. | | `SetState` with execution | The root process refreshes Inbox. The supplied executor appears in Outbox. | | `SetState` with `PreventExecution = true` | Inbox 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. Table: Approval history switches | Setting | Default | Effect | |---------|---------|--------| | [AutoApprovalHistory](https://api.workflowengine.io/core/OptimaJet.Workflow.Plugins/ApprovalPlugin/#field-autoapprovalhistory) | `true` | Installs the build step at registration and hides `FillApprovalHistory` from ordinary Designer action lists | | [DisableApprovalHistory](https://api.workflowengine.io/core/OptimaJet.Workflow.Plugins/ApprovalPlugin/#property-disableapprovalhistory) | `false` | Prevents build-step installation when set before registration and skips the handler's history refresh while true | | [UseLocalizedNamesInApprovalHistory](https://api.workflowengine.io/core/OptimaJet.Workflow.Plugins/ApprovalPlugin/#property-uselocalizednamesinapprovalhistory) | `true` | Resolves localized state and command names before storing them | | [NameParameterForComment](https://api.workflowengine.io/core/OptimaJet.Workflow.Plugins/ApprovalPlugin/#field-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 placement | Recorded result | |------------------|-----------------| | `PreExecutionImplementation` only | Pending rows with predicted states and `AllowedTo`; the action does not complete those rows during normal execution | | `Implementation` only | Completed rows for command, timer, or `SetState` execution; a newly inserted row has an empty `AllowedTo` when no pending match exists | | Both implementations | Pending 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.