External interaction¶
orchestra_inbox lets a logged-in operator act on a parked step from an inbox.
orchestra_interaction is its customer-facing counterpart: it lets an
external party, typically not logged in, act on a parked step through a
public link. A booking guest confirms a request, a customer signs a document on
a partner site, an applicant fills a form: each is a wait the outside world
advances.
Going deeper
This page is the overview. For a code-level, illustrated walkthrough of the URL mechanism, the capability token, and the security model, see External interaction internals.
How it works¶
A wait node opts in by declaring an External interaction node feature, choosing an interaction plugin and its settings. While the instance is parked on that node, the party visits the dispatcher:
/interaction/{instance}?token=<capability token>
The dispatcher reads the step the instance is parked on, the interaction that step declares, and hands the link holder to the matching plugin. Each step returns to the dispatcher, so the flow chains to the next interaction with no landing screen. When the instance is no longer running, or is parked on a step the link holder has nothing to do on, it shows that step's chain of messages instead, and a neutral message when the chain carries none.
A step is not the link holder's to act on in two cases: it declares no External interaction at all, or it is assigned to an operator. An assigned step is completed through the task route under its own authorization check, so the dispatcher never hands the link holder its surface even when it declares an interactive interaction, and the link doorway refuses to resume one.
Shipped interactions¶
- Message: show a message while the step is parked (an "awaiting confirmation" notice). The workflow is resumed by something else (an operator, a timer), so this offers no outcome chosen through the link. The body can carry tokens to insert process values, such as a reference number or a process variable.
- Redirect: send the link holder to an external URL (an off-site form, a partner site, a document signer), optionally appending a return link so the external step can send them back.
- Comment: collect a comment and an outcome on a small page (a comment field
and one button per the node's outcomes), recording the note and resuming the
step. A review is only this with
approved/rejectedoutcomes. It takes the comment task's three comment settings:comment_outcomesnames the outcomes that require a comment (they also require opening the step, never one-click from a list),comment_descriptionis shown under the comment field to state the commenting rules, and acomment_variablealso stores the note under a process variable name of your own (it already sits in the payload variable, e.g.validation.comment). It is the interaction counterpart of the comment task, reusable on any interaction node. - Webform (in
orchestra_interaction_webform): collect input on a Webform and resume the workflow on submission. See below.
A site can add its own interaction plugin; it is a tagged plugin like every other Orchestra extension point (see Extending Orchestra).
Three doorways: link, task or operation¶
An interaction plugin is written once and reached three ways. The public
dispatcher above is the link doorway: a capability-token link, for a party
who is typically not logged in. The same plugin also runs behind the task
doorway, an interaction_task inbox task,
for a logged-in assignee who reaches it from the inbox rather than a link. And
it runs behind the operation doorway, an
interaction operation an assigned user
acts on from their pending-actions list. The plugin does not know or care which
doorway opened it: it is handed a context that carries a resume handle (an
opaque "how to resume this step"), and resolving that handle is the doorway's
job. The link doorway resolves it from the capability token. The task and
operation doorways resolve it under the assignment check, so a leaked handle is
inert for anyone but the assignee.
So a Webform can be a public form an outside party fills or a review an
operator does in-site, with no change to the form or the plugin, only to which
node type carries it: a wait node with an External interaction feature for
the link, an interaction_task node for the task, or an interaction operation
for the operation.
Page titles, descriptions and breadcrumbs¶
Each page names itself, by the way in that opened it. A page the External
interaction's link opens takes the Page title its
author wrote on the External interaction, else the heading the interaction gives
its page (a payment's "Complete your payment"), else the step label. Only a page
naming no step falls back to one for what it shows ("Continue", "Status", "Thank
you"). The Page title is offered only for an interaction Orchestra shows (a
message, a payment, a submission to review, a comment): one that hands the link
holder off (a webform to fill in, an entity form, a redirect) leaves the title
to that page. The interaction says which it is (isAuthorable()), as it does
for a task's action page title. The Page title is translatable and fills
@variable placeholders from the instance's variables.
A task or operation page takes the step's action page title, else its action label, else the heading the interaction it shows gives its page, else its step label. The comment page a Comment interaction sends the actor to follows the way in that opened it: from the External interaction's link, its Page title, as the other pages that link opens; from a task or an operation, the step's action page title. With no title written, it takes the Comment interaction's heading, "Add a comment". It also shows that way's own interaction: an operator on a step that carries an External interaction too sees the step's settings, not the External interaction's. A link that opens no step the actor may act on keeps "Comment".
Below the title, the page the link opens shows the Page description its
author wrote on the External interaction, for the same interactions. It is
written with a text format, as the step's action
description is: the format decides which
HTML tags are kept, its @variable placeholders are filled from the instance's
variables, and it is translatable. The workflow depends on its text format. The
comment page shows the description of the way in that opened it: from the
External interaction's link, its Page description; from a task or an operation,
the step's action description. The action description is written for the step's
actor, so a page the link opens never falls back to it.
A breadcrumb (Easy Breadcrumb, for instance) asks the route for the page's title
instead of reading the page, and caches the answer. The link holder's route
names the run's workflow, and only for a link whose token opens that run, so a
guessed run id names nothing; the task and operation routes name the step while
it waits in the current tenant. The breadcrumb is cached per token, and rebuilt
when the step changes, when the workflow or one of its translations is saved,
when the tenants change, and, for a title with a placeholder, when the
instance's variables change. A route of your own that names a step opts in to
the same caching with the _orchestra_step_token route option, set to the route
parameter or query argument that holds the step's token.
Security model¶
The dispatcher routes are public, so a guessable instance id must not be enough to act on a run. The capability is a token, not the id:
- It is self-encoding: one opaque string that names its own instance and carries its own expiry, both bound into an HMAC keyed by the site private key and hash salt. It cannot be forged, pointed at another instance, or have its expiry extended.
- It is time-limited (30 days by default). A link that leaks into a web-server log, a Referer header, or a redirect to an external site stops working once it expires. Every dispatcher link is issued fresh with a new expiry as the flow chains, so a live party never meets an expired one.
- It is kept out of the Referer in the first place. Expiry is the floor, not
the answer: for the thirty days a token is live, anything off-site on a page
reached with it in the URL would hand it to a third party. Every surface that
renders with a handle sends
Referrer-Policy: no-referrer, a refusal included - the page saying the link is not yours is the last one that should pass it on. A surface that returns its own page or response attaches the policy to it; one that refuses by throwing puts it on the exception, which core persists onto the page it builds from it. Adding a surface means doing one of those two: seeNoReferrerTrait. - A node whose declared interaction plugin is no longer installed resolves to "nothing to do" rather than erroring, so an uninstalled provider cannot turn a public link into a server error.
- A step assigned to an operator (a node declaring assignments) is completed
only through the task doorway (the task route, under the
inbox's
checkActionAccess()check), never a bearer link. The link doorway may display such a step's status (an "awaiting confirmation" message) but refuses to complete it, so a party holding a capability link cannot approve an operator's task. This holds regardless of link scope, so an instance-scoped link chaining onto a later operator step is safe: it shows the status and stops there. - Being shown a step and being able to complete it are separate
permissions, and the doorways disagree about them, which is why a gateway
answers
checkViewAccess()andcheckCompleteAccess()rather than one question. On the link doorway possession authorizes viewing, so an assigned step is displayed (the bullet above); on the task and operation doorways it does not, because what a step renders is the workflow's own context, which can carry an earlier reviewer's note. A surface that renders a step askscheckViewAccess(); one that completes a step askscheckCompleteAccess().
An outcome chosen through the link (cancel, back) that an interaction allows is resumed by its signal link, on the GET that follows it: the link holder usually has no session, so there is no form token to ask for, and the capability token in the link is the authorization. A signal link only ever reaches the link holder from the step's own page: the webform's extra buttons, the payment step's actions, or the redirect the payment step issues itself when there is nothing to pay. No notification carries one, so a mail scanner never meets it. Following it again, a double click or a link kept after the fact, is a no-op: the step it names has already moved on. A step whose outcome must not be taken on a GET collects it on the Comment interaction's form instead, which resumes only on its POST.
When the Redirect interaction passes a return link, that link carries the capability token, so the destination site receives it (in its logs and Referer). The token is time-limited, but enable "pass a return link" only for a destination you trust.
Request limits¶
Everything above says what the doorway refuses. Refusing is not the same as being cheap to refuse: by the time a link that does not verify is turned away, the run has been loaded and an HMAC has been computed. Nothing in the security model caps how often a stranger may ask for that, because there is no identity here to cap it against - possession of the link is the whole of the authorization, which is the point.
So requests to the doorway can be counted, in two tiers inside one period:
- Per link. One capability link, counted on its own. A caller wearing out a single link is refused while everybody else is still answered, which is what keeps one link being hammered from closing the door on a live party.
- In total. Requests to the doorway counted together. A caller working through links it is guessing presents a fresh one each time, so it never reaches the per-link limit; this is the tier that sees it.
The two are not independent. A request the per-link limit has already refused is not counted towards the total, because otherwise one link being hammered would use up the number everybody shares and shut the doorway for every other link holder - which is the thing the per-link tier exists to prevent. The total still sees a flood spread across many links, because each of those is inside its own limit.
Be deliberate about the total: it is one number for everybody, so when a flood spread across many links reaches it, the doorway refuses every link holder until the period rolls. That is the choice it offers - shut rather than overwhelmed - and it is why the per-link limit is the one to reach for first, and why this one wants to sit well above what the site answers on a busy day.
Every request is counted, not only the failures: the work being bounded is the work done before a request is refused, so a counter that recorded only the requests it liked would leave the whole problem in place. Leave room in the per-link number for a person reloading their own page and for the return trips an off-site step makes.
It ships off, and off means untouched. A site fronted by a proxy or a firewall already caps this, two limiters are worse than one, and a number Orchestra guessed would refuse a busy site its own work. While it is off, nothing is counted, nothing is written, and core's flood storage is not touched at all. The status report says it is off, because a protection nobody can see is one nobody turns on.
Turning it on takes two things, deliberately:
- Set the limits at Administration > Configuration > Workflow > Orchestra >
Interaction. Either limit at
0means no limit. - Switch counting on at Administration > Configuration > Web services > Orchestra request limits, which is also where the period the limits count over is set (3600 seconds is an hour). One period for the whole site.
To turn it back off, clear the switch: the numbers stay where they are and
nothing is counted against them. Setting both limits back to 0 stops the
counting just as completely; the switch is the one to reach for during an
incident, because it is one place and it does not make you retype the numbers
afterwards.
A caller over either limit gets a 429 Too Many Requests with one plain sentence: that too many requests have been made to this address, and to try again later. Nothing else - not whether the link was any good, not whether the run exists, not which limit was reached. The caller it is written for is guessing, and an answer that varied with the guess would be the oracle the limit exists to close.
It is a plain response rather than this site's themed error page, which is not a
style choice. A themed 4xx page is served by a full subrequest, so a refused
caller would cost a page render - more than answering it would have - and a
flood would cost this site more the harder it was pushed back. The refusal
carries no Retry-After, because the count is a rolling window and a number
there would be a promise it does not keep. It is marked not to be stored, so no
proxy holds one and serves it to somebody who never made the request, and it
carries Referrer-Policy: no-referrer like every other refusal here: the URL it
turns away has the link in it.
The first refusal of a caller in each period is logged, with a hash of the link rather than the link itself; the rest of that period is quiet, because the log is the one thing a stranger can still fill after being turned away.
What this does not cover. The run named in the path is loaded while the route is matched, before any listener can know which route it is, so one entity load per request is still spent on a caller this refuses. Everything after it - the token check, the capability resolution, the interaction plugin, the render - is not.
A request naming a run that does not exist is not counted at all: routing answers it 404 while matching the route, so it never reaches the limits and never appears in the total. What such a request costs the site is one lookup that finds nothing. Size the total for the traffic the doorway actually answers, rather than for everything aimed at its path.
A site reachable from the internet wants a proxy or a firewall in front of this as well; this only bounds what gets as far as Drupal.
Link scope: instance or branch¶
A link comes in two scopes, and the capability token records which. Both are
built by InteractionUrlsInterface:
- Instance-scoped (
getStepUrl($instance_id)): drives whatever step the instance is parked on, and chains from one step to the next as the run advances. This is the right link for a sequential flow, and for a single wait offered to several people where the first to act wins (email the same link to all of them). - Branch-scoped (
getStepUrlForToken($instance_id, $token_id)): drives one specific parked token. When an instance parks several branches in parallel, each waiting on an External interaction, mint one branch-scoped link per branch and send a distinct one to each person, so each drives only their own step and the others are untouched. Once that branch has been acted on, its link shows the neutral "nothing to do" message.
To enumerate the branches awaiting an external party (to build and send a link
per recipient), use InteractionResolver::findParkedInteractiveTokens($instance),
which returns every parked token whose node declares an interaction. The webform
integration scopes its binding to the specific parked token automatically, so a
form always resumes the exact branch it was opened for, even if other branches
are parked or the instance moves on before the form is submitted.
Webform¶
orchestra_interaction_webform makes any Webform a form step. It ships:
- a
webforminteraction plugin chosen on the wait node's External interaction feature; - an Orchestra interaction Webform element: one hidden field that binds the form to its instance. The interaction prepopulates it with the self-encoding token, so this single element is the whole binding (no separate instance or token fields to add by hand);
- an Orchestra: start or resume on submit Webform handler: one handler that,
keyed on the binding element, either starts a workflow when a fresh
submission completes (binding that submission to the new instance and seeding
configured variables like the submission id and the submitter's uid) or
resumes the bound instance's parked step with a configured outcome (default
submitted) when the form is reopened as a step, then advances the run.
Setting it up takes no code:
- Build a Webform for the input to collect.
- If the workflow will reopen the form as a step, add one Orchestra interaction element to it (its key is free; the handler and interaction find it by type). A form that only starts a workflow needs none.
- Add the Orchestra: start or resume on submit handler: set a Start workflow to begin a run from the submission, a Resume outcome for a step, or both for a form that does both.
- On the wait node, set External interaction to Webform and pick the form, or leave it empty to use the form of the submission the step's submission id variable names, so that one workflow serves several forms.
See Webform integration for the full configuration reference: every handler setting, the interaction modes, chaining, and the gotchas.
The submission is bound to the instance as the Webform source entity, so a later step can reopen it (the party's earlier answers are kept) and Views relates the two natively. A re-edit after the run has moved on resumes nothing. Reopening is Webform's own secure-token reopen, so it needs the form's Allow users to update a submission using a secure token setting on; without it the party gets a blank form and a second submission binds alongside the first.
Modes: collect, review, edit¶
The webform interaction renders in one of three modes, so the same form
serves the whole life of a request:
- Collect input (default): show the empty form to fill. This is the classic interactive wait.
- Review (read-only): render an existing submission as a read-only summary, for a step that inspects what was submitted (a validator reading a request) without changing it.
- Review (editable): reopen an existing submission for editing, for a step that revises what was submitted (the poster amending after a request for changes).
The review modes read the submission to show through a configured
submission variable, the same variable the start (or a collect step) wrote
the submission id into, so the interaction always reopens the right submission.
A step can also display context: each context_display entry names a
process variable (a dotted path reaches into a structured value, e.g.
validation.comment), a label and an optional severity, and its current value
is posted as a Drupal message wherever the step surfaces, on every page of a
wizard included, always read live from the instance and never stored into the
submission. A form on the site that started the workflow gets them read when
the person opens the step, and shows them while the person is on it. A line with no severity takes the severity of the outcome the
value came with (from the deciding step's outcome colors, see
Human tasks), so a rejection colored Danger
shows as an error and an approval as a status message; with none recorded, it
shows as status. These are the pieces the orchestra_interaction_webform_examples
workflow wires together.
Chaining: who owns what happens after submit¶
By default a Webform shows its own confirmation page after submit, the same as any standalone form. The handler offers one Return to the workflow after submit option that hands that decision to the workflow instead: once the step has resumed (or the run has started), the link holder is sent on to the interaction dispatcher, which renders whatever the run parked on next, with no landing screen. That is what lets one workflow chain several forms, or show a workflow-handled confirmation page (a read-only receipt, a message) rather than the webform's own. It is off by default, so a form keeps its own confirmation unless chaining is turned on.
- On the resume path it returns the link holder to the step's own dispatcher doorway: the bearer dispatcher for an anonymous party, the inbox for a task.
- On the start path it returns the link holder to the new run's dispatcher, so a standalone form flows straight into the workflow it just started.
The next step is read from the workflow itself, not carried on the form: by the time the form's confirmation runs, the branch has resumed and the run has advanced, so the handler resolves the branch's current chain and the token now parked on it and scopes the return link to that token. A workflow that runs parallel branches through chained forms is handled automatically: each branch lands on its own next step rather than drifting onto a sibling, with no extra form configuration and no dependence on the catch-up setting. Nothing about the return is read from the request query or a hidden field, so it does not depend on anything surviving the POST.
The redirect is issued from the form's confirmation step, which webform runs
after its own setConfirmation(), so it only takes effect when the form's
confirmation type performs a redirect. The inline and modal types
re-render the form in place (a rebuild) and cancel the redirect, so
return-to-workflow silently does nothing with them: use a redirecting type such
as Page (the default). The handler's settings form disables the option, and explains why, when the form's
confirmation type is inline or modal.