A personal task inbox needs two lists: the tasks a user may act on, and the tasks that user has already processed. The first is expressible today. The second is not, because nothing records who completed a work item.

WorkItemManager::complete() computes the completer at src/WorkItemManager.php:730, the acting operator passed by the caller falling back to the assignee holding the item. It uses that value for the optional store-completer-in-variable node setting and for the actor on the completed audit event, then discards it: the saved entity keeps only its state.

Falling back to the assignee instead is wrong in the cases that matter. A task completed straight out of a pool never had an assignee, since complete() only rejects terminal states and does not require a claim. A task completed on someone's behalf keeps an assignee who did not act. A task claimed and then reassigned would appear in the wrong person's history. The completer variable is no substitute, being per-node opt-in and reachable as a Views field with no filter handler, and audit events are audit only, not a query surface.

Storage

  • completer, an unsigned integer written in complete() from the value already computed there, indexed together with the completion time so a personal history serves its filter and its sort from one index. It stays empty whenever nobody identifiable acted, such as an external interaction resumed through a bearer link or an engine-driven timeout, so a completed item is recognised by its state and never by this field being set, and such an item belongs to no one's personal history.
  • completed, a timestamp, so a history can be ordered by when each item was processed. The changed time cannot do that, since any later write moves it. Stamped by completeForToken() as well, which is the externally-resumed path.
  • The existing result field becomes outcome. It stores the resolved decision, and every other layer already calls that an outcome: the outcome signaler, the outcome result value object, the outcome key on the audit event, the inbox outcome buttons. The old name is also heavily overloaded, not least by Views, where a result is a row.
  • OutcomeResult::OUTCOME_KEY replaces the literal naming the outcome inside a structured completion payload, at nine call sites plus the act link query parameter. Its value stays result, so this issue changes no stored data: that key is echoed in saved process variables and in links already sent, and flipping it belongs with the naming split below.

Views handlers

  • Actionable by the current user, a filter expressing the rule WorkItemManager::accountVisibilityCondition() defines for the inbox: assigned to the acting user, or unassigned and pooled either to everyone or to one of their audiences. It mirrors that condition in SQL, because a Views filter cannot consume an entity query, and a kernel test asserts both select the same items so the mirror cannot drift.
  • Completed by the current user, the history filter. Its negation excludes items with no completer rather than counting them as somebody else's.
  • Act link, a field resolving the route the task type declares through TaskActRouteInterface, labeled with the task's own act label. The work item has no canonical link template on purpose, so Views has no link-to-entity to offer here. It renders nothing for a finished task, for a row the viewer may not act on, or for a type declaring no act route.
  • The completer is also exposed with the username autocomplete filter, matching how the instance initiator already reads.

The shipped view

A My tasks view with two page displays as sibling tabs, at orchestra/my-tasks and orchestra/my-tasks/history. Two displays of one view rather than two views, so both tabs share one row template and cannot drift apart, and the style is an unformatted list of fields rather than a table, so a theme overrides views-view-fields--my-tasks.html.twig once and lays the row out itself. Each row carries the task label, the business data from its process through the instance relationship (initiator, correlation key, the workflow-authored status as its term label, the engine state, the current step) and the act link. The history display swaps the act link for the outcome and the processed time.

A bridge module

The work item views cannot live in either parent. orchestra_views has Views, but the views enforce orchestra_inbox; orchestra_inbox has the work item, but does not depend on Views. Either side alone would ship a menu link to a route that was never registered on a site running only that side. So orchestra_inbox_views depends on both and owns the two work item views (including the existing tenant tasks view, moved into it), the act link field with its hook and config schema, and their menu links.

The dashboards had no entry point

The shipped views declare local tasks under a dashboard path, but nothing owns that path and none of them is a default tab, so there was no tab set and no menu link anywhere: they were reachable only by typing a URL. A Views item now groups them beside Tenants and Settings, one link per view, each declared by the module that guarantees its route exists. The trace view gets none, since it takes an instance id and has no argument-less landing page.

Notes

  • Pre-release, so no update hook. On an existing site: install the three storage definitions, copy the old field's values into outcome before uninstalling it so recorded decisions survive the rename, enable orchestra_inbox_views, and re-import the tenant tasks view so its new enforced dependency takes effect (the personal task view arrives with the module install). The (completer, completed) index needs no manual DDL: the same schema update that installs those two columns applies it. Items completed before this keep an empty completer, which is correct rather than backfilled from the assignee, so they belong to no personal history.
  • Documentation and the French translations are updated, including one term fix across modules: the task inbox was translated with the French for an email inbox.

Follow-ups, to be filed separately

  • Split the overloaded result vocabulary. The value passed to complete() is the submitted payload, a scalar outcome or an array carrying a task type's extra data, and that is a different concept from the decision. Note that result_variable stores the whole payload rather than the outcome, so renaming it to an outcome name would be wrong, and a mechanical find and replace would make this worse rather than better.
  • Read access on an instance for the operator who processed it. The existing requester-facing route is initiator-only, so an agent handling someone else's request cannot open it.

Issue fork orchestra-3613700

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review

  • mably committed 8db0e8ca on 1.x
    feat: #3613700 Record a work item's completer and completion time,...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mably’s picture

Issue summary: View changes

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.