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/tasks
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 instance is deleted (or otherwise abandoned) is canceled.
stateDiagram-v2
[*] --> open: node creates the task
open --> claimed: a user claims it
claimed --> open: claim timeout (unclaim)
claimed --> completed: the user completes it
open --> completed: completed while still pooled
open --> canceled: instance deleted
claimed --> canceled: instance deleted
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 with timeout_action: unclaim (and a timeout) 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.
The inbox¶
The inbox at /orchestra/tasks 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
(non-guarded) outcome once it is the user's to finish. A task that needs its own
page to act on, a guarded 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, the inbox can hand a task to someone else. A user with the
reassign orchestra tasks permission may Reassign any task to another user;
a task's current assignee may reassign their own the same way (no extra
permission). 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.
That permission also lets its holder act on any task directly, without reassigning it first, because reassigning the work to themselves would reach the same place in two steps. It is a supervisor's permission, and restricted as one.
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 users / roles / users_variable / roles_variable assignment, on by
default for named users and opt-in for a role pool. The inbox only dispatches
the events; leave the submodule off to notify another way (ECA).
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 (a uid list or a role-name list). 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. 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. See
Task assignment.
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 override it with an 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.
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. Two extra config keys shape it:
comment_outcomes: the outcomes that require a comment. Such an outcome is guarded, 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_variable: the process variable the note is written to (next to the scalar outcome inpayload_variable), so a later step can read it.
Guarding is a general human-node seam (HumanNodeInterface::getGuardedOutcomes()):
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 guards the outcomes in its requires_ui key the same way:
those that need the interaction's own input (a mandatory field, a review of what
is shown) 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 guard 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. That
checkbox's default is seeded from the interaction, so a review or a webform
interaction (whose guardsAllOutcomes() 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 guards, so the author always sees the guard 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: 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/my-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 (non-guarded) outcome, 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 visitor 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 Orchestra 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.
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 identity 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 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.