Skip to content

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 in payload_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_operation parks a token as an assignment-gated operation with outcome buttons, the pull sibling of the user task.
  • comment_operation is a user_operation that also collects a comment, written to its comment variable, the pull sibling of the comment_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.