Human tasks¶
A parked token on a human node becomes work a person does. Orchestra core owns
the WorkItem entity and mints one for every human node when its token parks;
the orchestra_inbox submodule adds the user node type and the inbox where
task-mode work items are claimed and completed. Completing a task signals its
token, so the process resumes, the same wait + signal primitive the kernel
already has, with a human in the loop.
The lifecycle¶
A user node, when its token arrives, creates an open task and parks
the token. The task waits in the inbox until someone completes it; the chosen
outcome is written to the node's payload_variable, and the token is
signaled so the process moves on.
sequenceDiagram
actor User
participant Engine
participant Task as User task
participant Inbox
Engine->>Task: token reaches user node
Task-->>Engine: create open task, Park the token
User->>Inbox: open /orchestra/inbox
Inbox-->>User: tasks they may act on
User->>Task: claim (now mine)
User->>Task: complete with an outcome
Task->>Engine: write payload variable, signal the token
Engine-->>Engine: token advances past the node
A task moves through a small state machine. It starts open; claiming makes it claimed by one user; completing makes it completed. A task whose branch is canceled is canceled with it: its instance was deleted or canceled, a threshold or quorum join, or a join timeout, fired without waiting for that branch, or an operator canceled the branch. The people who were told about the task are told it is canceled, and why (see Notifications).
stateDiagram-v2
[*] --> open: node creates the task
open --> claimed: a user claims it
claimed --> in_progress: sent to the node's handler
claimed --> open: claim timeout (unclaim)
claimed --> open: the holder is blocked or deleted
in_progress --> open: the holder is blocked or deleted
claimed --> completed: the user completes it
in_progress --> completed: the handler reports back
open --> completed: completed while still pooled
open --> canceled: its branch is canceled
claimed --> canceled: its branch is canceled
claimed --> canceled: the holder was its only candidate
open --> canceled: nobody is left in its audience
completed --> [*]
What a completion records¶
Completing a task stamps up to four things on it, so a task's history is a query rather than a reconstruction:
- outcome: the value the completer chose, validated against the node's configured outcomes. Empty for a completion that carries none, and for a canceled task, which never held a decision.
- completer: the user who acted, or the assignee holding the task when the caller passed no actor. This is what a personal task history filters on, and it is deliberately not the assignee: a task completed straight out of a pool never had one, a task completed on someone's behalf keeps an assignee who did not act, and a reassignment moves the assignee to a user who may never act at all. It stays empty when nobody identifiable acted, such as an external interaction resumed through a bearer link or an engine-driven timeout, so a completed task is recognized by its state and never by this being set.
- completed: when it happened, distinct from the changed time, which any later write moves.
- on behalf of: whose work it was, when a delegation is what brought it within the completer's reach. Empty for ordinary work, so a delegated item is recognized by this being set. It is neither of the other two: the assignee is never moved by a delegation, and the completer is always whoever actually acted.
A node can also mirror the completer into a process variable with its completer variable setting, so a later step can route work back to whoever decided; the field above is for querying, the variable for routing.
Claim timeout¶
A user node whose timeout names action: unclaim reclaims a
stale hold: if a task sits claimed past its deadline without being acted on,
it is released back to open for its pool to pick up again. See
Timers.
A holder who can no longer act¶
A task claimed by someone who is then blocked or deleted would otherwise stay claimed to them: they cannot act on it, because they cannot sign in, and nobody else can, because it is held. Blocking or deleting an account therefore frees the work it was holding, in the same transaction, and the answer depends on whether anyone else can take it.
Usually someone can, and the task goes back to open for its pool. This covers a task being handled as well as one merely claimed, unlike the claim timeout above: "you have had it long enough" must not take work off someone mid-way, but an account that cannot act at all is a different matter, and being part-way through is no reason to leave the task with it.
A task whose audience names the departing person and nobody else has no pool to return it to, so returning it would leave a task nobody can see. That one is canceled and its step raised as an incident instead, which is how a step nobody can take is reported everywhere else. Widen the audience and retry the incident: the node runs again and mints one task, rather than a second one beside the first.
Holding a task is not the same as being the one person it names. A reassignment can hand a task to someone the audience never named, and blocking that someone returns the task to the person the step does name.
The inbox¶
The inbox at /orchestra/inbox lists the tasks the current user may act on,
gated by the process orchestra tasks permission. A task offers a claim
action while it is open and unassigned, and one-click completion per outcome
once it is the user's to finish, except for an outcome that requires opening the
task. A task that needs its own page to act on, for such an
outcome or an interaction, offers an
action link (labeled with the node's action label) to
that page instead.
Each outcome is a machine value (recorded on the task as its
outcome, routed on by flow conditions). A node may also set an outcome label per value, a
translatable, human-readable string shown on the completion button (e.g.
"Approved by manager" for approved) while the value stays unchanged. Labels
are translated per language through Config Translation, and a task type can
override one in code via WorkItemPresentationInterface::getOutcomeLabel(); an
outcome with no label shows its value.
Which tasks a user sees is decided by assignment:
a task is visible when it is assigned to the user, or it is unclaimed and either
pooled (no candidates) or carries a candidate token the user holds. The match is
a plain query against the task's materialized candidates field, not a
per-request recomputation.
Reassignment¶
Beyond claiming, a task can be handed to someone else. A task's current
assignee may Reassign their own from the inbox (no extra permission). A user
with the reassign orchestra tasks permission may reassign any task, from the
Tasks list (/admin/orchestra/views/tasks, in orchestra_inbox_views,
listed in the Orchestra admin menu and under Structure > Orchestra beside
Incidents), which shows an administrator every tenant's tasks. Their inbox
lists their own work like anyone else's, not the tasks other people hold.
Reassignment makes the chosen user the task's claimed assignee whatever its
prior state, a terminal (completed or canceled) task is left
untouched, and the change is logged to the orchestra channel as an audit
trail.
A task somebody holds is theirs to work: nobody else may open or complete it,
not a fellow candidate of the pool it came from and not a manager. A manager
takes it over by reassigning it, to themselves or anyone else, or by returning
it to its pool; both are recorded and tell the holder, where completing it
under them would do neither. The permission does let its holder act directly
on any task nobody holds, whoever it is offered to. It is a supervisor's
permission, and restricted as one. A holder of administer orchestra is the
exception: they may open and complete any task of their tenant, held or not,
as the instance page's signal links already let them, and are recorded as the
task's completer.
Reassignment is a transfer: ownership moves to the new assignee, at once and for good. It is not time-bounded, nothing comes back, and the task keeps no memory of who held it before. A task that should stay its holder's while someone else acts on it wants a delegation, not a reassignment.
A claimed task that was offered to a pool of more than one candidate also offers
Return to pool: it clears the assignee and reopens the task so its pool can
claim it again. A directly-assigned or single-candidate task has no pool to fall
back to, so the action is not shown, though the same release the
claim timeout uses underlies both.
Reassignment moves the work for good. For work that stays its holder's while somebody covers it during an absence, see Delegation.
Notifications¶
So an assignee need not watch the inbox, the optional
orchestra_mail submodule emails a task's audience when it
is assigned and again when it is reminded (the notify timeout action).
Notification is opted in per assignment, a Notify this audience checkbox on
each staffing assignment, on by default for named users and opt-in for a pool,
a role or a group. The inbox only dispatches the events; leave the submodule
off to notify another way (orchestra_easy_email, or a subscriber of your own).
Four of the staffing audiences differ only in how they name people: users and
roles name them in config, while users_variable and roles_variable read
them at runtime from a process variable (user IDs, usernames or email addresses,
or role names). The fifth, everyone, names nobody in particular and is how a
step says it is
deliberately open. It has to be asked for: an audience that names people and
matches none of them halts the step rather than opening it, and a human step
naming no audience at all is refused when the model is saved. A pool that
empties after the task was offered to it, the last member leaving the role or
every named account being blocked, is caught by the cron recovery sweep and
reported the same way, since the task is otherwise left where nobody can see it
(see a step nobody can take). The
variable plugins let an earlier step decide who acts: the submission-validation
example seeds the original poster's uid into a variable and assigns the modify
step back to them with users_variable. The optional orchestra_group
submodule adds a sixth, groups, which pools a step to the members of one or
more Group entities: the same shape as
a role pool, with membership rather than a role deciding who may act. Its
twin, groups_variable, reads the group IDs from a process variable, the way
roles_variable reads role names. See
Task assignment.
Users who have not logged in yet¶
On a site that provisions accounts at first login, by single sign-on for
example, a workflow often names a person before their account exists. The
users_variable_with_provisioning audience, "Users by variable, including
users who have not logged in yet", is users_variable for that case:
- a value naming an account offers it the task, as
users_variabledoes; - a username with no account is offered the task when the site will provision its account, and its address is notified when the username is an email address. The task then waits for the account: it is in the inbox of the account created under that username, compared without case or accents, from its first login, with nothing to run at login. A task list names it "(username) has not logged in yet" until then, and the recovery sweep leaves the step alone;
- anything else names nobody, so a mistyped username halts the step as it does
with
users_variable.
Orchestra cannot know who will log in one day; the site, which provisions the
accounts, does. Orchestra asks the orchestra_engine.user_provisioning service,
whose default provisions nobody, so the audience behaves exactly like
users_variable until the site answers for itself. Decorate the service with a
class implementing UserProvisioningInterface:
# my_site.services.yml
services:
my_site.user_provisioning:
class: Drupal\my_site\MyUserProvisioning
decorates: orchestra_engine.user_provisioning
final class MyUserProvisioning implements UserProvisioningInterface {
public function canProvision(string $username): bool {
// Ask where the accounts come from: a web service, for example.
}
}
The service is asked only about a username no account has, when a task is created or notified, never per page. It is built for every inbox, though, so keep its constructor cheap and connect on the first question. Those users open their tasks in the inbox, which needs the "Process Orchestra tasks" permission: grant it to the authenticated user role, or have the single sign-on give it a role that has it.
The action label¶
The link that opens a task to act on it (in the inbox and the pending-actions
lists) is labeled with the node's own label by default, so it reads "Review
request" rather than a generic "Open". A node may give it a text of its own,
its Action label (action_label): a translatable string (Config
Translation translates it per language, alongside the node's own label) that
may carry @variable placeholders filled from the instance's variables, e.g.
Review @doc_type. It is shared by every human node, a user task, a comment
task, an interaction task or a pull operation, so a step reads the same
wherever it is listed.
The action page title¶
The link and the page it opens do different jobs: the link is a short verb in a
list, such as Cancel my request, and the page needs a heading, such as
Cancel your leave request. A node gives that heading in its Action page
title (action_title). Like the action label, it is translatable and fills
@variable placeholders.
Every page that opens a step to act on it (an operation, an inbox task, an interaction task or operation) is titled with the first of these the node sets:
- its action page title;
- its action label;
- the heading the interaction it shows gives its page, such as a payment's
Complete your payment(an interaction task or operation only); - its step label.
The External interaction's "Page title" titles only the pages its link opens.
The page's breadcrumb names it the same way. It is rebuilt when the step is done, when the workflow is saved, and, for a title with a placeholder, when the instance's variables change.
The action description¶
The page that opens a step shows, above its buttons, what the person is asked to
do. A node says it in its Action description (action_description), for
example Check the request of <strong>@applicant</strong> before the 15th.
Like the action label, it is shared by every human node, translatable, and
fills @variable placeholders from the instance's variables.
It is written with a text format, as a block body is. The format decides which HTML tags the page keeps: "Basic HTML" keeps the bold text above, "Plain text" shows it as typed. Who may choose a format is set by the format's permission, and a translation is filtered by the same format.
The placeholders are filled first, as core fills tokens in a formatted text,
and the format then filters the whole text, the values included. A value's own
markup is shown as typed. In "Basic HTML" a value filled into a link address,
such as <a href="@url">, cannot make a javascript: link: the format removes
it. "Full HTML" does not, so keep it for trusted authors.
The workflow depends on the text format, so a config import creates it first. Deleting the format keeps the workflow: the description falls back to the site's fallback format.
Where it shows:
- an operation (
user_operation,comment_operation) shows it in place of the generic "Choose how to proceed.", which stays when the description is empty; - an inbox task (
user,comment_task) shows it at the top of its form, and shows nothing there when it is empty; - an interaction task or operation shows it above what the interaction shows on the page (a message, a submission to review), and on its comment page.
The node type says whether its page shows the action page title and action
description (HumanNodeInterface::isAuthorable()), and its settings offer the
two only when it does. An inbox task and an operation always show them: their
page is Orchestra's own form. An interaction task or operation asks its
interaction (InteractionInterface::isAuthorable()): one that hands the person
off to another module's form (a webform to fill in, an entity form, a redirect)
shows neither, so the step does not offer them, and drops them when saved. A
webform answers by its mode: a submission to review shows them, a webform to
fill in does not. A task type of your own using HumanNodeConfigTrait shows
them by default; override isAuthorable() when its page may not.
Laying out a step's form¶
An operation and an inbox task are acted on through one form template,
orchestra-step-form.html.twig, in Orchestra Presentation. It prints the
description, the fields a step adds (a comment), then one button per outcome.
A theme changes it for the whole site by copying it into its templates
directory, or for part of the site by naming the copy after a suggestion. For
the step withdraw of the workflow leave_request, in the application hr
and the tenant north, Drupal tries these, the first one found wins:
orchestra-step-form--north--leave-request--withdraw.html.twigorchestra-step-form--north--hr--withdraw.html.twigorchestra-step-form--leave-request--withdraw.html.twigorchestra-step-form--hr--withdraw.html.twigorchestra-step-form--north--leave-request.html.twigorchestra-step-form--north--hr.html.twigorchestra-step-form--north.html.twigorchestra-step-form--leave-request.html.twigorchestra-step-form--hr.html.twigorchestra-step-form--operation.html.twig(or--taskfor an inbox task)orchestra-step-form.html.twig
An id's underscores are written as hyphens. The workflow is the one the step
belongs to: for a step of an embedded child, the child's. The application is the
one that workflow belongs to, so orchestra-step-form--hr.html.twig lays out
every step of every workflow in hr. A workflow in no application has no
application suggestion. Tenant, application and workflow ids share one name
space: give them distinct machine names, or a template named after one also
reaches the other.
The application is never paired with the workflow alone: a workflow belongs to
one application only, so --leave-request already reaches only hr
instances. It is paired with the step, though. A step id is only unique within
its workflow, so when the workflows of an application share one, say a
manager_approval step in every hr workflow,
orchestra-step-form--hr--manager-approval.html.twig lays out that step in all
of them, where --leave-request--manager-approval reaches one workflow only.
For example, to put a reminder above every step form of the hr application:
{# orchestra-step-form--hr.html.twig #}
<p class="hr-reminder">{{ 'Your manager is told of every answer.'|t }}</p>
{{ description }}
{{ form|without('description', 'actions') }}
{{ actions }}
Besides the form's parts (description, actions, and form for the rest),
the template gets:
label: the step's label, the modeler's name for the step (the page title is set as the action page title says);node_values: the step's named texts (its Values), read by name as an email layout reads them, for example{{ node_values.intro }}. A name the step does not set reads as nothing, so{% if node_values.intro %}leaves it out;orchestra: the Orchestra tokens, read by name with underscores for hyphens, for example{{ orchestra.label }};kind(operationortask),tenant,application(empty when the workflow belongs to none),workflowandstep.
Print form without the parts printed on their own,
{{ form|without('description', 'actions') }}, so the fields a step adds and
the form's hidden values stay in the page.
The task actions and their order¶
A listed task offers its actions in one dropdown, the same on the inbox page and in the task lists: Claim an unassigned task, open it (the action label above), each outcome that can be chosen in one click (or a plain Complete for a step with no outcomes), Reassign, and Release a claimed task back to its pool. Each appears only when it applies to the task and the viewer, and in that order.
A module changes the set with hook_orchestra_inbox_task_operations_alter()
(documented in orchestra_inbox.api.php): each link carries a weight (10, 20,
30, 40, 50 in the order above, every one-click outcome at 30), so an
implementation reorders by weight, removes a link, relabels one or adds its own,
and every surface follows.
Comment tasks¶
A comment_task node (in orchestra_inbox) is a user task that collects
a note alongside the decision. It opens a small page with a comment field and
one button per outcome; picking one records the comment and completes the task.
A review is only this with approved / rejected outcomes, there is no
separate review type. Three extra config keys shape it:
comment_outcomes: the outcomes that require a comment. Such an outcome requires opening the task: it is withheld from the inbox's one-click list and reached only by opening the page, which enforces the note first. An outcome not listed here can still be completed in one click.comment_description: text shown under the comment field, for the author to state the commenting rules (when a comment is needed, what it should say). Translated per language through Config Translation.comment_variable: an extra process variable the note is written to. The note already travels in thepayload_variable, as itscommententry next tooutcome(e.g.validation.comment) once one is entered; name a variable here to reach it under a name of your own, or to keep it when the step has no payload variable.
The comment operation and every interaction that ends in a comment decision (the comment interaction, a Webform review) carry the same three settings.
Every human node can also give each outcome a color (outcome_classes, a
status palette class keyed by outcome value: Neutral, Info, Success, Warning or
Danger), so the outcome reads like a status. A task list showing the outcome as
a tag colors it (the inbox history does), and a comment entered with the outcome
is shown by a later step at the matching level: Danger as an error, Warning as a
warning, the others as a status message. That level is written next to the
comment, in the same write: as the severity entry of the payload variable
(validation.severity beside validation.comment) and, when a
comment_variable is set, under that name followed by _severity
(validation_comment_severity). Several steps can share one comment variable:
each one writes its own severity beside its own comment, an empty one when its
outcome has no color, so the comment shown always comes with the level its own
step gave it. Only the node sets it: a severity in the submitted payload is
dropped. A context line with no severity of its own reads it from there (see
Interactions).
Requiring the node to be opened is a general human-node seam
(HumanNodeInterface::getOutcomesRequiringOpening()): any outcome a type
withholds from one-click completion, for any reason, is offered only by opening
the node. The inbox honors it uniformly. An interaction task or operation
requires opening for the outcomes in its requires_ui key the same way, and
for those its interaction requires a comment for: they need the interaction's
own input (a mandatory field, a comment, a review of what is shown) and can be
chosen only by opening the interaction, while the rest stay one-click. Both
interaction families share one implementation, so a task and an operation answer
identically. A step can also be opened for every outcome with the Open the
step for every outcome checkbox (require_all): when ticked, no outcome is
one-click and the requires_ui field is hidden as moot. A step with no outcomes
loses its one-click Complete too
(HumanNodeInterface::isOpeningRequiredToComplete()), its completion URL sends
the person to the step instead, and the bulk Complete task action skips it.
That checkbox's default is seeded from the interaction, so a review or a webform
interaction (whose shouldOpenForEveryOutcome() is TRUE, its work being to be
seen or filled in) shows it pre-ticked while a plain message shows it clear.
Saving the form stores the chosen value, and from then on that stored checkbox,
not the interaction, is what decides, so the author always sees the setting and
can change it. A node authored outside the form (a modeler that does not render
the checkbox) has no stored value and falls back to the interaction's own
default, so a review still opens first there too.
The orchestra_examples module ships example_review (a comment task requiring
a note when rejecting) and example_approval (requiring one for either
decision), clickable round-trips built this way.
Reading the run behind a task¶
An operator works a step, not a run, but they usually need to see what the run is
about. That is the run's own read-only page (/orchestra/instance/{instance}),
and holding or having completed one of the run's steps is what admits them to it,
with no administrative permission. A stand-in is admitted the same way, by the
steps the person they cover holds or has completed. See
read access for exactly who is
admitted and how a site, a tenant or a workflow narrows or widens that. The
personal task lists link it from their Reference column, and the
pending-actions list from its Process column. It is a read surface only: the work
itself is still done on the task's own page.
The pending-actions surface¶
Every parked human node a user may act on, a user task, a comment task, a
pull operation or an interaction task, is listed in one place: the My
Orchestra actions page (orchestra_ui, at /orchestra/actions, linked
from Content alongside Orchestra tasks), and per-instance on the
process-instances list. On the standalone page the Process column links to the
instance trace for a user who may read it, and otherwise to the run's own
read-only page (/orchestra/instance/{instance}), which the listed work item
itself entitles them to; only when neither is open to them does it stay plain
text. Each entry's actions fold into an operations dropbutton, the action
link that opens the node first, then a one-click item for every
directly-signalable outcome (one that needs no opening), labeled with the
action label. Where that action link points is the task type's
own choice: a user task opens its outcome-buttons form, a comment task its
comment form, an interaction its interaction page. The type declares the route
(there is no default), so a type that declares none exposes no action link at all,
only its one-click outcomes.
Returning to the list you came from¶
When an operator opens a task from a pending-actions list, the My Orchestra
actions page, an instance's actions column, the process-instances list or the
instance trace, completing it returns them to that list rather than to the task
surface's default landing (the inbox for a task, the actions list for an
operation). Every such list threads its own URL onto each action link as an
orchestra_return argument (through the shared pending-action link builder),
and every completion surface honors it, redirecting only to an internal
location. A task opened from anywhere else keeps its usual landing. For a step
that hands off to a webform or an off-site payment, the return is remembered
server-side for the opening operator and honored when that step resumes; a
webform set to show its own confirmation, or a payment whose link holder returns
in a different session, keeps the default behavior.
Pull-based operations¶
Some human steps are pulled, not pushed: rather than waiting in the inbox, the
work is picked up from the actor's own actions list. The operation task
types park the token and gate it by assignment exactly like an inbox task, and
mint the same WorkItem; the only difference is its mode. A task is
task mode (shown in the inbox); an operation is operation mode (shown on the
My actions list and the per-instance column). The entity and its
lifecycle are identical, so a task is really just an operation shown a different
way. So is who may act on it: an operation that somebody has claimed, or that was
reassigned to somebody, is theirs to work, and the operation page refuses the
other people it was first offered to, as the inbox does.
user_operationparks a token as an assignment-gated operation with outcome buttons, the pull sibling of theusertask.comment_operationis auser_operationthat also collects a comment, written to its comment variable, the pull sibling of thecomment_task.- The interaction operation (
orchestra_interaction_operation) renders an attached interaction (a Webform, a review) as the operation body, the pull sibling of the interaction task.
Choose an operation over an inbox task when the step should be pulled from the
actor's actions list rather than pushed to the inbox: an assigned user acts on
it directly when they get to it, instead of claiming and completing it in the
inbox. Completion goes through the same shared contract as the inbox
(OutcomeSignaler), so the outcome, the completer variable and any task-type
extras (a comment) are recorded identically on both paths.
Interaction tasks¶
A user node completes through outcome buttons. An interaction_task node
(in orchestra_interaction_task) completes through an interaction plugin
instead: it parks the token and assigns the task like any other, but its work is
the same interaction the public dispatcher serves (a Webform to fill, review or
edit; a comment/review; a message; a custom plugin). The assignee opens it from
the inbox and acts on it in-site, no public link involved.
This is the task doorway to the interaction system: one interaction
plugin runs either as a capability-token link to an outside party (the
external interaction dispatcher) or as a task to a logged-in
assignee, with only the doorway differing. Opening the task hands the assignee
to the interaction's own surface (its page, an off-site form) under the
assignee's identity; the interaction owns its completion, so the inbox adds
no outcome buttons of its own. An interaction_task carries the chosen
interaction_plugin and its interaction_settings, and otherwise reads like a
user node: assignments, outcomes and a payload_variable, so it notifies,
claims and routes exactly as above. The labels inside the interaction's
settings (a context message, an escape button) are translated per language on
the workflow's Translate tab, like the node's own label.
The orchestra_interaction_webform_examples module is a worked example: a
submission a validator reviews (a Webform interaction in Review mode), can
send back for changes (the original poster edits their own submission in a
modify task), and a processor finally handles. The same workflow ships in a
link variant where the modify step is a public link mailed to the poster
instead of a task, the two doorways side by side. See
external interaction.