When someone is away, their tasks sit in their inbox until they come back. Nobody else can see them, claim them or complete them. The only way to move the work is for a manager to reassign each task one by one, and to reassign each one back afterwards, which loses the fact that it was ever someone else's work.
Orchestra should let a user declare a delegation: for a period, another user acts for them. The delegate sees the delegator's tasks in their own inbox, pooled ones and already-claimed ones alike, acts on them as themselves, and the record says who they were acting for. This is absence cover, not a transfer: it expires, it is revocable, and the delegator keeps their tasks throughout.
This is a different thing from the Reassign the inbox already offers, which moves ownership once and for good. #3613913: Call handing a task to another user reassignment, not delegation, so the word is free for delegating authority freed the word, since the docs called that reassignment "delegation".
Where it plugs in. Every "is this task mine?" decision already goes through two places: AssignmentMatcher::viewerTokens(), the opaque token identity of a viewer, and WorkItemManager::accountVisibilityCondition() with userCanAct(), shared by the inbox, the pending-actions list, VBO, interaction tasks and content-bound tasks. Widening those covers every surface at once. Two things need care: the assignee field is a hard equality in both, so a task the delegator has already claimed needs its own widening, and the CurrentUserCandidate views filter holds a third copy of the rule in SQL.
The seam. Core ships a DelegationResolverInterface with a no-op implementation, the way orchestra.notification_context already does, and a new orchestra_delegation submodule decorates it with a stored delegation entity. An install without the submodule pays one method call returning an empty array. A site whose absences already live in an HR system or an LDAP attribute can decorate the service instead and keep its own source of truth.
Scope decisions.
- A delegation covers every task in one tenant. Not per workflow: viewerTokens() is task-independent, so a per-workflow scope cannot be expressed at the seam that makes this work everywhere at once.
- One hop. If A delegates to B and B delegates to C, C does not act for A, so no cycle is possible.
- Both directions are many and uncapped: several stand-ins for one person, and one person covering several. Only a duplicate of the same pair over an overlapping period is refused, since two rows saying the identical thing cannot be reasoned about.
- Cover is read when the question is asked, never written to the task. A step assigned to one named person is still auto-claimed for that person mid-absence; the assignee is never rewritten. That is load-bearing: if assignment resolved through cover, a task created during an absence would belong to the stand-in for good, which is a reassignment with extra steps. Because nothing is written, declaring cover applies at once to work already waiting, and revoking it takes the work back with nothing to unwind.
- The delegate is added to the task's notification audience. A "Keep notifying me" checkbox on the delegation lets the delegator step out; with several delegations active it takes unanimity, this being the only place Orchestra removes a resolved recipient rather than adding one.
- Two permissions: one to declare your own cover, one to manage anyone's, so someone who left unexpectedly can still be covered.
- Delegation widens which tasks a user reaches, never whether they may act. The delegate still needs "process orchestra tasks", and a delegation grants no permission of its own.
What gets recorded. The work item gains an on_behalf_of field naming who the actor was acting for. The completer stays whoever acted and the assignee is never moved, so the three answer three different questions. It is deliberately never a guess: a value appears only when exactly one delegation accounts for the reach. Two covered users in the same audience, either of whom could equally explain it, records nothing, because naming one would be decided by row order and an audit field is better silent than confidently wrong. The two paths also carry different strengths, so the field is not described as ownership: on a task its assignee already held the person named is unambiguously whose work it was, while on one taken from a pool it records the capacity the actor was acting in, a pool being nobody's in particular.
Every covered row explains itself. A held row reads "bob (for alice)". An unclaimed pooled row reached only through cover says so too, naming the person when one delegation explains it and otherwise reading "covering for another user", so a stand-in is never shown a row addressed to a group they do not belong to with nothing to explain it.
Reading it back. on_behalf_of is exposed to Views as a filter by username plus a third personal lens, "Handled on the current user's behalf": the list to read on returning from an absence. It is the delegator's side of the same rows a stand-in sees in their own history, and both are true at once, one recording who acted and the other who they acted for.
Issue fork orchestra-3613914
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
Comment #3
mably commentedComment #4
mably commentedComment #6
mably commented