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/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_variable does;
  • 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:

  1. its action page title;
  2. its action label;
  3. the heading the interaction it shows gives its page, such as a payment's Complete your payment (an interaction task or operation only);
  4. 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:

  1. orchestra-step-form--north--leave-request--withdraw.html.twig
  2. orchestra-step-form--north--hr--withdraw.html.twig
  3. orchestra-step-form--leave-request--withdraw.html.twig
  4. orchestra-step-form--hr--withdraw.html.twig
  5. orchestra-step-form--north--leave-request.html.twig
  6. orchestra-step-form--north--hr.html.twig
  7. orchestra-step-form--north.html.twig
  8. orchestra-step-form--leave-request.html.twig
  9. orchestra-step-form--hr.html.twig
  10. orchestra-step-form--operation.html.twig (or --task for an inbox task)
  11. 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 (operation or task), tenant, application (empty when the workflow belongs to none), workflow and step.

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 the payload_variable, as its comment entry next to outcome (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_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 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.