No-code booking flow: driven by Orchestra interactions¶
How a site builder assembles a working booking flow without writing PHP. The booking lifecycle is driven entirely by configuration: each booker-facing step is declared on its workflow node, and a generic, shipped dispatcher routes the booker through them, with no bespoke controller anywhere in the path.
This shipped as the orchestra_interaction mechanism in the Orchestra
project (issue #3606290), because driving an external party through a parked
workflow step is generic, not booking-specific. Yoyaku contributes only the
booking-domain pieces. The note below records the design and what landed.
The problem it removed¶
Almost everything in a booking was already configuration:
| Piece | How it is built | Code? |
|---|---|---|
| Resources, slots, tariffs, prices, cutoff | admin UI / config | no |
| The gateway | site/tenant setting (resolver) | no |
| The workflow | the Orchestra modeler (BPMN / orchestra_cm) | no |
| The webform | Webform UI; bind with a handler | no |
| A calendar on a page | yoyaku_node (bind a resource to a node) |
no |
| The booker flow | the shipped interaction dispatcher | no |
The one part that required code was the dispatcher: the example's
CheckoutController read where a booking's workflow was parked and sent the
booker to the matching page (form, payment, message). It was bespoke PHP that
hardcoded node ids (n_form, n_payment, n_validate), so it could only
ever live in the example, never as a feature. A parked wait node carried no
statement of what the booker should see while it waited; the dispatcher
supplied that knowledge in code. Moving that knowledge onto the node, as
configuration, is what made the dispatcher generic.
Interactive waits and user tasks¶
An interactive wait and an operator user task are two surfaces over the same
primitive (a wait: park, then resume on an outcome). The axis that separates
them is not authenticated-versus-anonymous, it is role and channel:
- Role: is the step for the external party (the booking's own booker) or an operator (staff handling it)?
- Channel: is it reached through the back-office inbox (assignment), or through a link to the party's own flow?
| for | channel | response via | |
|---|---|---|---|
| wait | anyone or anything | any signal | bare park/resume |
user task (orchestra_inbox) |
an operator | the inbox (pull) | claim and complete |
interactive wait (orchestra_interaction) |
the external party | a link to their flow (push) | the link plus a step |
So a UserTask is a wait plus the back-office machinery (a work item,
assignment to recipients, the inbox UI); an interactive wait is a wait plus an
interaction reached over a link to the party's own flow. They are symmetric:
both are "a human task" (park, present something to a human, collect a response,
resume), differing in audience and channel. This is why the mechanism is the
mirror of orchestra_inbox: the operator side is the assignments feature, the
external-party side is the interaction feature, and the two coexist on one
node.
A booking workflow normally has both kinds. The booker steps (the booking
form, payment) are interactive waits; an operator step such as "a team member
confirms this booking" is a user task that belongs in the inbox. The
External interaction node feature applies to any parking node, so the same
node can carry an operator assignments feature and an external interaction:
on the operator validation step, the interaction is just a message the booker
sees ("awaiting confirmation by our team") while the operator works.
What shipped¶
Three layers: the generic engine mechanism in Orchestra, a generic Webform integration in Orchestra, and the booking-domain plugins in Yoyaku.
1. The generic mechanism: orchestra_interaction¶
- An External interaction
NodeFeature(plugin idinteraction, config keyinteraction). It applies to any task type that can park, and in the modeler lets the author pick an interaction and configure it, storingfeatures: {interaction: {plugin: webform, settings: {webform: yoyaku_booking}}}on the node, under thefeatureskey where each node feature owns one entry. No code, no node ids in PHP. - An
Interactionplugin type (manager, base, attribute). Each plugin knows how to present one kind of parked step and declares the visitor-initiated outcomes it offers (getSignalOutcomes(), e.g. cancel, back). Two generic plugins ship here:message(render a configured message) andredirect(send to an external URL). - A public dispatcher (
InteractionController) addressed by instance id plus a per-instance capability token:/interaction/{orchestra_instance}?token=…. It finds the parked step, reads its interaction descriptor, instantiates the plugin, and callsrespond()(a redirect or a render array). Each step returns here so the flow chains to the next interaction with no landing screens. A signal route,/interaction/{instance}/signal/{outcome}, resumes the parked step with a visitor outcome the plugin allows. - The capability token (
InteractionToken, an HMAC of the instance id), the URL builder (InteractionUrls), and the parked-step resolver (InteractionResolver). - A small core helper,
AdvanceQueueDrainer, so a signaled queued instance advances in the same request rather than waiting for cron.
2. The generic form step: orchestra_interaction_webform¶
A reusable "collect input on a Webform during a wait" integration, useful well beyond booking (intake, approval response, survey, RFQ reply):
- a
webforminteraction plugin that redirects to a configured Webform bound to the instance (its id and capability token travel as prepopulated hidden elementsorchestra_instanceandorchestra_interaction_token), with the return and cancel URLs; - an
OrchestraInteractionHandlerWebform handler that, on submission, either starts a workflow (a fresh submission with a workflow configured) or validates the token and resumes the instance's parked step with a configured result, then advances the instance. It exposes a protectedonResumed()extension point and binds the submission to the instance as its source entity (so a later step can reopen it).
3. The booking-domain pieces (Yoyaku)¶
- A
paymentinteraction plugin (yoyaku_orchestra_payment): resolves the order from the instance's transaction and shows a landing page with the amount due and three choices, "Go to payment", "Change details" (the back outcome) and "Cancel booking" (the canceled outcome). Only an explicit "Go to payment" click creates or reuses the order payment through the gateway resolver and redirects to the gateway, merging the return, cancel and back URLs. A return from a canceled or declined payment lands back on this page (the failed attempt does not advance the workflow), so the payer can retry, change details or cancel rather than being looped to the gateway. Every response is uncacheable, since it depends on mutable payment state. BookingFormHandler(yoyaku_webform) now extendsOrchestraInteractionHandler, keeping only the booking addition: copy the submitted name/email onto the order's booker fields inonResumed(). The booking webform carries this handler in place of the generic one.yoyaku_calendar_interaction, an opt-in bridge: after a calendar hold starts a booking workflow, it redirects the visitor into the instance's interaction flow. Enabling the module is the opt-in; the calendar by itself just holds.
The no-code recipe (end to end)¶
- Define resources, slots, prices, cutoff (admin UI).
- Draw the workflow in the modeler; on each booker wait, set the External interaction feature (the Webform plugin pointed at a form, the Payment plugin, or a Message), and on the operator step a Message interaction for the status the booker sees.
- Build the booking webform (Webform UI); add the booking form handler (which resumes the instance and maps name/email to the order) and the two hidden binding elements.
- Pick the gateway (site or tenant setting).
- Put the calendar on a page (
yoyaku_node). - Enable
orchestra_interaction,orchestra_interaction_webformandyoyaku_calendar_interaction.
A working booking flow, no PHP. The shipped dispatcher does the work, reading the example workflow's tagged nodes.
Security¶
The dispatcher routes are public (the booker is typically not logged in), so
the capability that authorizes acting on an instance is the per-instance
token (an HMAC of the instance id), never the guessable id. Every dispatcher
link carries it; the routes reject a request without it, and the signal route
additionally resumes only an outcome the parked interaction allows. The Webform
resume handler re-validates the token before resuming, and an off-site return
value is dropped to close an open redirect.
Designed but not yet built¶
The conceptual design covers two more capabilities the shipped code does not yet implement; they are recorded here so the extension points are not re-derived:
- Off-site delivery (emailed magic link). On-site delivery shipped (the
calendar bridge redirects the browsing booker into the flow). The off-site
case, emailing the tokenized link, is not built. It would reuse
orchestra_mail's recipient/notify mechanism with the booker as the recipient, added as an optionalnotifyon an interaction fired when the token enters the node. This is what makes the "an assigned operator needs more information from the booker" choreography work: the operator's user task has an outcome routing to a booker interactive wait that emails a link; the booker answers, resuming back to the operator's task. - Logged-in account access. The dispatcher is token-only today. A logged-in
booker who owns the booking could be authorized by account access instead
(reached from a "my bookings" page), with the token kept as the bearer for an
emailed link. Keeping booker steps as interactive waits for members too
keeps the flow uniform; modeling a member's step as a
UserTaskassigned to that user (so it lands in their own inbox) is a valid alternative when wanted.
Open questions¶
- Anonymous identity. A booker returning in the same session is already
led back to their parked step: the session's cart pointer keeps the order
through checkout (locked), the booking page says so from their held cookie,
and its link to
/booking/checkoutre-resolves the running instance and mints a fresh token for its payment step alone: an order locked while its workflow waits on any other step, someone else's perhaps, is sent back to the booking page instead. It never unlocks the order (a payment may be in flight); the only way out of checkout stays the workflow's own Back/Cancel step. What stays open is a longer-lived, still-unguessable handle for a booker whose session is gone, or a multi-booking cart. - Non-workflow deployments. A site using yoyaku without Orchestra has no
parked node to dispatch on; the dispatcher is orchestra-only by construction
(it lives in
orchestra_interaction), so such a site drives its steps another way.
Relation to other docs¶
This is the booker-flow corollary of Use cases and surfaces: the interaction is a surface, and making it declarative is how a surface is assembled from configuration rather than code. It composes with resource types (which leave workflow and payment account selection to configuration) so that the whole booking setup, engine to front door, needs no PHP.