Skip to Content
Evaluate Get Started Plugins Glossary

Inbox and Outbox

Key takeaways

Use ApprovalPlugin inbox data for a "My Tasks" screen and outbox data for a "Completed by me" screen. Inbox rows identify pending processes and available command names; Outbox results summarize completed history by process. Query IApprovalProvider directly for application screens. Web API exposes tenant-scoped Inbox and Approval history records. It has no direct aggregated Outbox endpoint, but an application can derive "Completed by me" from history or expose its own endpoint that calls IApprovalProvider.

ApprovalPlugin supplies two personal worklists for an approval application. Inbox is the user's "My Tasks" list: it shows documents on which that user can currently act and the available commands. Outbox is the user's "Completed by me" list: it summarizes documents on which that user has completed an approval action.

The application can also query Inbox by process ID to show the current approvers on a document page. ApprovalPlugin supplies the worklist data; the host application joins each process ID to its business document, renders the screens, and uses WorkflowRuntime command APIs when the user selects a command.

What happens to Inbox and Outbox after approval

Suppose a purchase request needs approval from a manager and then a boss. The manager first sees the request under My Tasks. After the manager selects Approve, the request appears under the manager's Completed by me list, and the boss receives it under My Tasks.

Inbox and Outbox after manager approvalBefore approval, the purchase request is in the manager's My Tasks list and the boss has no task. After the manager approves, the request is in the manager's Completed by me list and the boss's My Tasks list.PURCHASE REQUEST APPROVALThe same request moves to the next approverBEFORE MANAGER APPROVALManagerMY TASKS (INBOX)Purchase requestAPPROVEBossMY TASKS (INBOX)No approval task yetManager selects ApproveAFTER MANAGER APPROVALManagerCOMPLETED BY ME (OUTBOX)Purchase requestCompleted and removed from My TasksBossMY TASKS (INBOX)Purchase requestAPPROVEBoss can act nowThe same request is now in the manager's history and the boss's task list.

The purchase request remains the same business document. Approval only changes who needs to act next: the request leaves the manager's My Tasks list, remains visible under the manager's Completed by me list, and appears in the boss's My Tasks list. Outbox is calculated from completed Approval history when the application requests it; there is no separate Outbox store.

Use the worklist data for three business screens:

  • My Tasks - show documents waiting for the current user's action and the actions available to that user.
  • Completed by me - show one summary for each document on which the current user completed an approval action.
  • Current approvers - show the people who can currently act on a document.

Assign approval work

The actor restriction on an outgoing command transition decides which identities ApprovalPlugin writes to WorkflowInbox. For a document assigned to one person, store that person's workflow identity in a process parameter such as ApproverId. Define an actor backed by an IWorkflowRuleProvider rule whose selected GetIdentities or GetIdentitiesAsync method returns that parameter. Its selected Check or CheckAsync method must accept the same identity for command authorization. Apply the actor as an Allow restriction on the Approve transition. The rules documentation contains a complete provider implementation for this pattern.

For a role or team, return every current member identity from the same rule. ApprovalPlugin creates an inbox row for each returned identity; it does not create one shared row for the role or team. The rule provider must return the same identity IDs that the application passes to command authorization and execution.

Query the inbox by process ID when a document screen must show its current approvers. The query returns InboxItem records. This example selects only identities whose stored row contains the requested command:

CurrentApproverReader.cs
using OptimaJet.Workflow.Core.Persistence;using OptimaJet.Workflow.Core.Runtime;public sealed class CurrentApproverReader{    private readonly WorkflowRuntime _runtime;    public CurrentApproverReader(WorkflowRuntime runtime)    {        _runtime = runtime;    }    public async Task<List<string>> GetAsync(        Guid processId,        string commandName)    {        List<InboxItem> inbox = await _runtime.PersistenceProvider            .GetInboxByProcessIdAsync(processId);        return inbox            .Where(item => item.AvailableCommands.Any(                command => command.Name == commandName))            .Select(item => item.IdentityId)            .Distinct(StringComparer.Ordinal)            .ToList();    }}

The result is the stored inbox snapshot. Recheck current permission with GetAvailableCommandsWithConditionCheckAsync before enabling a command, and use ExecuteCommandWithRestrictionCheckAsync to recheck actor restrictions when executing it. This matters especially when external role membership can change without workflow execution.

Show completed work

The "Completed by me" screen shows one summary per document rather than one row for every completed action. An OutboxItem groups approval history by executor identity and process ID. The provider calculates each summary when queried and orders the results by the most recent approval time.

OutboxItem properties
PropertyDescription
ProcessIdProcess whose completed history is summarized
FirstApprovalTimeEarliest completion time for the identity in that process
LastApprovalTimeLatest completion time for the identity in that process
ApprovalCountNumber of matching approval history rows
LastApprovalTrigger name from the most recent matching history row

The outbox does not receive rows moved from the inbox. A command can complete or insert an approval history entry, as can SetState with execution enabled. Later outbox queries group records that contain the requested executor identity.

For a command executed on behalf of another workflow participant, outbox grouping uses the initiating identityId stored in approval history. It does not use impersonatedIdentityId. See delegated command identities for the authorization and audit split.

Query inbox and outbox

Use identity-based queries to populate "My Tasks" and "Completed by me". The host application supplies the workflow identity whose work the screen should show. Use a process-based Inbox query to show the current approvers for one document. Application code accesses these queries through IApprovalProvider on WorkflowRuntime.PersistenceProvider.

MethodResult
GetInboxByProcessIdAsyncReturns inbox rows for one process and optionally localizes command names
GetInboxCountByProcessIdAsyncCounts inbox rows for one process
GetInboxByIdentityIdAsyncReturns pending work stored for one identity and optionally localizes command names
GetInboxCountByIdentityIdAsyncCounts inbox rows stored for one identity
GetOutboxByIdentityIdAsyncGroups completed history into per-process summaries for one executor
GetOutboxCountByIdentityIdAsyncCounts the per-process outbox summaries for one executor
ApprovalQueueReader.cs
using OptimaJet.Workflow.Core.Persistence;using OptimaJet.Workflow.Core.Runtime;public sealed class ApprovalQueueReader{    private readonly WorkflowRuntime _runtime;    public ApprovalQueueReader(WorkflowRuntime runtime)    {        _runtime = runtime;    }    public Task<List<InboxItem>> GetInboxAsync(        string identityId,        int pageIndex,        int pageSize)    {        return _runtime.PersistenceProvider.GetInboxByIdentityIdAsync(            identityId,            CreatePaging(pageIndex, pageSize));    }    public Task<List<OutboxItem>> GetOutboxAsync(        string identityId,        int pageIndex,        int pageSize)    {        return _runtime.PersistenceProvider.GetOutboxByIdentityIdAsync(            identityId,            CreatePaging(pageIndex, pageSize));    }    public Task<List<InboxItem>> GetProcessInboxAsync(Guid processId)    {        return _runtime.PersistenceProvider            .GetInboxByProcessIdAsync(processId);    }    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);    }}

Paging.Create uses a 1-based page index. Call GetInboxCountByIdentityIdAsync, GetInboxCountByProcessIdAsync, or GetOutboxCountByIdentityIdAsync to calculate the number of pages before requesting a page.

Every built-in persistence provider orders inbox results by AddingDate descending and outbox summaries by LastApprovalTime descending. Inbox reads populate CommandName.LocalizedName from the process scheme and requested culture; only CommandName.Name is persisted. When no translation exists, LocalizedName falls back to the technical name.

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

The built-in provider methods filter inbox rows by process ID or identity ID; they do not join those rows to application business data. If a worklist must search business fields together with inbox membership, maintain an application-owned search projection.

Known limitation - commas in command names: every built-in persistence provider stores AvailableCommands as comma-separated technical names. A command name that contains a comma does not round-trip as one item, so use technical command names without commas.

Tenant scope: identity-based inbox and outbox methods have no tenant argument. Their results, counts, and paging span every matching IdentityId in a shared provider store. InboxItem exposes TenantId after reading, but OutboxItem does not. Use identity values that are unique across tenants or a tenant-aware data-access implementation. Identity-based workflow actions inherit the same scope.

Build worklists through Web API

The built-in Workflow Engine HTTP API exposes stored Inbox and Approval history records. It does not expose GetOutboxByIdentityIdAsync or return the provider's aggregated OutboxItem result directly.

An HTTP application has two ways to provide "Completed by me":

Client-side grouping must fetch every matching history row before grouping. Grouping one page cannot reproduce an Outbox page because the history endpoint pages individual rows first, while GetOutboxByIdentityIdAsync groups processes first and then applies paging.

Workflow Engine HTTP API reference documents the Data API operations that expose stored Inbox entries for "My Tasks" and current-approver screens.

Method and pathResult
POST /workflow-api/search/processes/inbox-entriesSearches inbox rows across processes in the current HTTP tenant
GET /workflow-api/data/processes/{processId}/inbox-entriesReturns a filtered, sorted, and paged process inbox
GET /workflow-api/data/processes/{processId}/inbox-entries/{identityId}Returns one process and identity row
DELETE /workflow-api/data/processes/{processId}/inbox-entriesDeletes matching rows for one process
DELETE /workflow-api/data/processes/{processId}/inbox-entries/{identityId}Deletes one process and identity row

The search request accepts search, nested filters, sorts, skip, and take. The response contains collection and total. This request returns one page of pending work stored for manager-42:

curl --request POST \  --url "https://example.com/workflow-api/search/processes/inbox-entries" \  --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": "AddingDate",        "direction": "Desc"      }    ],    "skip": 0,    "take": 20  }'

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

HTTP API reads and deletes WorkflowInbox records directly. It does not execute GetInbox or refresh ApprovalPlugin, and it does not provide create or update operations. Register ApprovalPlugin in each runtime that must populate the inbox. Deleting an inbox row does not revoke the identity's current workflow permission; a later inbox refresh can recreate the row.

Use inbox and outbox actions

The GetInbox action performs a process-or-identity query inside a workflow and stores List<InboxItem> in a process parameter. Use direct provider calls when application code needs to build a user worklist.

GetInbox action parameters
ParameterRequiredDefaultDescription
GetParameterTypeYes-Selects ProcessId or IdentityId
GetParameterValueYes-Supplies the process GUID or 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

The GetOutbox action performs an identity query and stores List<OutboxItem> in the named process parameter.

GetOutbox action parameters
ParameterRequiredDefaultDescription
IdentityIdYes-Selects the identity whose history is grouped
ParameterPurposeYes in DesignerPersistenceControls how Workflow Engine stores the output parameter; the handler uses the default when it is absent
ParameterNameYes in Designer"" in the handlerNames 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

Keep Inbox data current

ApprovalPlugin rebuilds inbox rows each time root process execution is about to become idled or finalized. It first deletes existing rows for the process, resolves actor restrictions on command transitions, and then inserts one InboxItem for each selected identity.

WorkflowInbox does not store a role or team name as one shared task. Each actor rule resolves its current members to workflow identity IDs, and ApprovalPlugin considers each returned identity separately. For example, a team rule that returns five members can produce five rows for the same process, subject to the transition-classifier rules below.

By default, an identity gets a row only when its resolved transitions include at least one Direct classifier. When that gate passes, AvailableCommands contains the distinct command names resolved for the identity across all transition classifiers. Setting InboxActorsWithOnlyRejectCommands to true also keeps identities with no Direct transition, but their command list is empty.

An InboxItem stores the resulting command names for one identity and one process. Actor resolution searches the selected process and its descendant subprocesses, but the resulting row uses the root process ID being refreshed.

InboxItem properties
PropertyDescription
IdUnique inbox row ID
ProcessIdProcess ID used for the inbox refresh
TenantIdTenant associated with the process instance
IdentityIdIdentity that can act
AddingDateRuntime date and time when the row was created
AvailableCommandsDistinct technical command names persisted for the identity; localized names are added when the record is read

InboxCheckConditions determines whether actor resolution also evaluates transition conditions. AllowedActorsFilter receives the names of actors used by Allow restrictions. With Or concatenation, a rejected actor is skipped individually. With And concatenation, the filter suppresses the transition only when it rejects every allowed actor; if any actor passes, all allowed actors still participate in the identity intersection. Actor definitions used by Restrict restrictions are never passed to this filter.

Condition evaluation triggered by InboxCheckConditions is an actor lookup, not a pre-execution pass, so ProcessInstance.IsPreExecution is false. Conditions used for inbox filtering should be read-only and must not send notifications or write to external systems as a side effect.

Changing any input to actor resolution does not refresh inbox rows by itself. This includes membership in an external directory, role, or team; a persisted process parameter such as ApproverId; and data read by an actor rule or transition condition. Inbox rows keep the identities and commands resolved by the previous refresh until later root process execution reaches the refresh point. ApprovalPlugin does not expose a public method that forces its inbox rebuild for a process.

Control inbox contents

ApprovalPlugin configuration changes which inbox rows and commands are persisted. DisableApprovalHistory does not disable inbox generation. DisableInbox controls inbox maintenance; when both settings are enabled, the runtime handler returns immediately.

Inbox configuration
PropertyDefaultEffect
DisableInboxfalseSkips future inbox deletion and rebuilding; existing records remain
InboxCheckConditionsfalseAdds transition-condition evaluation to actor resolution
AllowedActorsFilternullFilters Allow restrictions according to their Or or And concatenation; Restrict restrictions are unaffected
InboxActorsWithOnlyRejectCommandsfalseKeeps an empty-command row when an identity has no Direct transition

Filter actor definitions

AllowedActorsFilter receives actor names, not the identity IDs returned by those actors. For example, a scheme can use DocumentApprover and Observer actors in Allow restrictions while the worklist should contain only approval assignments. Configuring AllowedActorsFilter = actorName => actorName == "DocumentApprover" excludes the Observer actor from inbox calculation for Or-joined restrictions.

AllowedActorsFilter changes only the stored inbox projection. It does not revoke workflow permission, filter Restrict actors, or replace the actor checks performed during command lookup and execution. For And-joined Allow restrictions, the plugin keeps the full identity intersection when at least one allowed actor passes the filter.

Keep non-direct actors

InboxActorsWithOnlyRejectCommands keeps an inbox row for an identity whose resolved transitions contain no Direct classifier. A common case is an identity that can only return a document through a Reverse transition. The retained row has an empty AvailableCommands list; the setting does not persist the reverse command name.

Use the empty row when the application needs to show that the identity is involved with the process even though the main worklist has no direct action for it. Query current commands from WorkflowRuntime before rendering an action, because the empty inbox row is not an executable command or an authorization grant.

The plugin deletes current inbox rows before inserting replacements. Consumers should query the provider after workflow execution reaches an idled or finalized status instead of caching an inbox row as permanent state.

Inbox replacement uses separate delete, actor-resolution, and insert operations. If actor resolution or insertion fails after deletion, the inbox can remain empty or incomplete until a later refresh succeeds.

Diagnose an empty inbox

Check the scheme and runtime in this order when WorkflowInbox remains empty.

CheckExpected configuration
Plugin registrationRegister ApprovalPlugin with WithPlugin before StartAsync
Inbox switchKeep DisableInbox set to false
Execution cycleLet the root process execute and return to an idled or finalized status after plugin registration
Command transitionAdd an outgoing transition triggered by a command
Actor restrictionApply an actor whose rule resolves at least one workflow identity
Rule inputsPersist required values such as ApproverId before actor resolution
Transition classifierGive at least one resolved transition the Direct classifier by default
ConditionsIf InboxCheckConditions is true, confirm the transition condition succeeds during inbox refresh
Direct state changeUse SetStateAsync with execution when ApprovalPlugin must refresh; PreventExecution = true bypasses the refresh handler and leaves existing rows unchanged
Previous approver remainsA role, ApproverId, rule input, or condition input changed without workflow execution; revalidate commands and let root process execution reach the refresh point

After these checks, call GetInboxByProcessIdAsync(processId). An empty result means the refresh produced no identities for the process. If process-based rows exist but an identity query is empty, compare the exact IdentityId returned by the rule with the value used by the application. A role or team name is not an inbox identity unless the rule deliberately returns that name as an executable identity.

See also

Frequently asked questions

What is stored in the ApprovalPlugin inbox?

Each InboxItem stores one process ID, one identity, the insertion time, and the command names selected during actor resolution.

Can I read Outbox through Workflow Engine HTTP API?

The built-in API has no endpoint that returns OutboxItem. Search Approval history by IdentityId and group all matching records by ProcessId, or expose a host-owned endpoint that calls GetOutboxByIdentityIdAsync.

When does ApprovalPlugin refresh the inbox?

ApprovalPlugin deletes and rebuilds inbox rows when root process execution is about to become idled or finalized. A direct state change with SetStateParams.PreventExecution = true does not invoke that refresh.

Can ApprovalPlugin check transition conditions before adding inbox commands?

Yes. Set InboxCheckConditions to true. The default is false, so actor and command resolution does not evaluate conditions. The check runs outside pre-execution, so keep those conditions free of side effects.

Does the ApprovalPlugin inbox refresh when actor data changes?

No. A role change, ApproverId update, or another actor-rule input change does not trigger ApprovalPlugin. Stored inbox rows keep the previous identity and command resolution until the root process executes again, so applications should revalidate current commands and maintain a separate live projection when immediate updates are required.