Roadmap¶
Built¶
- The engine kernel: token-based execution with
start(),advance()andsignal(), asynchronous advancement through theorchestra_advancequeue. - Execution mode (queued or synchronous), site-wide and per workflow:
synchronous advances a process inline, in the request that starts or resumes
it, with no cron in the loop (the default); queued advances through the
orchestra_advancequeue on cron. See Execution mode. - The entities:
Tenant,Workflow,ProcessInstance,TokenandVariable, multi-tenant from the start. A workflow imports/exports through the Modeler API (a Models collection with an Import tab and export / export-as-recipe operations) and clones viaentity_clone, so no bespoke template UI is needed. - The
TaskTypeplugin type and thestart,end,passthroughandwaitprimitives. - The
Joinplugin type and gateways as(join, split)pairs. A node's join is its incoming-side policy:immediate(merge),wait_all(every branch), ormatching(the branches that can still arrive); the decision runs atomically under a per-node lock so concurrent arrivals cannot fire it twice or leave it stuck. WithSplitalready in place, the named gateways are now just presets over the two knobs (parallel= (wait_all, all),exclusive= (immediate, first),inclusive= (matching, all)), driven through the same Join and Split plugins any node uses. A gateway is a routing node with no task of its own; a task node may carry its own join and split too. - The
orchestra_inboxsubmodule: ausertype that parks the token and creates a human task, claimed and completed from a task inbox. Completing a task signals the token, so the process resumes. - The
orchestra_ecasubmodule: aStartProcessECA action that launches a process from any ECA event (ECA to Orchestra), and aneca_eventtask type that emits a custom ECA event when a token reaches it (Orchestra to ECA). - Per-node configuration: nodes carry an optional
configmap, so task types can read their own settings (for example theeca_eventtask's event ID). - The
orchestramodule:OrchestraClientInterface, a transport-agnostic contract for driving workflows (start, signal, inspect, variables). Callers depend only on the interface, so the same code will run same-site and cross-site. LocalOrchestraClientin the engine, wrapping it in-process and scoping every operation to the current tenant.- The
orchestra_serversubmodule: an OAuth-gated HTTP surface (/orchestra-api/...) whose controllers are a thin transport over the local client. The acting OAuth consumer is bound to a tenant (an immutableorchestra_tenantfield added to the consumer), so every API call is scoped to that consumer's tenant via a tagged tenant resolver. - The
orchestra_clientsubmodule:RemoteOrchestraClient, which bindsOrchestraClientInterfaceto the server API over HTTP (client-credentials OAuth, the secret held by the Key module). Enabling it rebinds the contract to the remote adapter, so the same caller code runs against a remote site. The local and remote halves are now symmetric. - Resuming a parked step from another site: the client contract checks and
completes the step a resume handle names, with a proof of the person, and the
engine decides with the checks its own pages use (
StepResume). - Human-node action handoff on interactions (
orchestra_inbox+orchestra_interaction): a human node's action is completed inline (a one-click outcome, or a comment task's note) or through an interaction that owns its own surface (a comment/review page, a webform, an off-site page resumed via a resume handle). External systems integrate through the interaction bearer gateway, not a signed callback. - The
FlowConditionplugin type and conditional flows: a flow may carry a condition (acomparisonof a process variable, or anall/anycomposite nesting others into a boolean tree). When a node finishes, the engine sends a token down every outgoing flow whose condition holds. That one rule is the split: mutually exclusive conditions give an exclusive choice, none give a parallel fork, overlapping ones give an inclusive split, and a task branches on its own, no gateway needed. - The
Splitplugin type: a node's split chooses, among the live (condition- passing) outgoing flows, which the token follows:all(the default, a fork) orfirst(an exclusive choice). Split and condition compose: the condition says which branches are eligible, the split says how many. Custom splits (weighted, random) are now just plugins. - Token lineage and the join merge policy: each token records its parent, so a
token-local variable set on a branch is inherited by that branch's
descendants and reaches a downstream join. A variable is instance-wide by
default, or token-local when scoped to a token. A join may declare collect
lines, each gathering one variable from the joined branches into its own
list, in arrival order and optionally without repeats; with the
countcondition this expresses a synchronous quorum ("two of three approved") from generic pieces (wait_all+ merge + count) with no special node. See theexample_quorumworkflow inorchestra_examples. A synchronizing join can also keep the branches' token-scoped variables on the token going on, each from the first or the last arrival that set it. - Per-task assignment (
orchestra_inbox): anAudienceplugin that also implementsAssignmentInterfacetargets a user task at an audience. It answers two questions in opaque string tokens: who a task is for (candidates) and which audiences a viewer belongs to (viewerTokens), and the inbox shows a task when the two sets intersect. The resolved audience is materialized onto a multi-valuecandidatesfield at task creation; no candidates at all is a step open to anyone who may process tasks. Every human node must name an audience: the modeler refuses to save a step that names none, and a definition that reaches the engine without one halts with an incident rather than parking a task no account can see. Five plugins ship (usersandroles, named in config;users_variableandroles_variable, resolving their audience from a process variable, a uid list or a role-name list decided at runtime by an upstream step; andeveryone, which restricts nobody and is how an open step says so out loud); a node carries them as a structuredassignmentslist (several union), which a canvas modeler edits through one field per audience that is one list (assignee_users,assignee_roles). New audiences are new plugins minting a new token namespace, with no schema or storage change: the optionalorchestra_groupsubmodule addsgroupsandgroups_variablethat way. Theexample_quorumworkflow shows the structured form, its third ballot a union of a role pool with a named user. - Visual authoring (
orchestra_modeler,orchestra_bpmn_io,orchestra_cm):orchestra_modeleris the Modeler API model owner, mapping Orchestra nodes and flows to a modeler's components and back so a workflow round-trips id-stable (running instances are never orphaned), with element templates carrying each node's task type and per-node settings.orchestra_bpmn_ioadapts the BPMN.io canvas (layout capture/restore, palette and context-pad restrictions);orchestra_cmis the accessible, form-based "Complete Modeler" alternative that also edits the node-level structures a canvas cannot (timer ladders, assignments). A subscriber warns before editing a workflow that has running instances. See Integrations. - A browser UI (
orchestra_ui): screens to start a workflow, observe an instance's trace (its whole token trail in order, consumed tokens included) and its variables, and manage instances, including bulk deletion that cascades to the instance's tokens, variables and tasks. Each parked human step in the trace offers an Open link to its operator surface (the task form, the operation page or the interaction), the same page the pending actions list opens, so an operator can act on a step straight from the trace. On a task somebody else holds, the link is offered only to who may act on it, and a manager who may take the task over is offered Reassign instead, plus Return to pool for a claimed task drawn from a pool. The trace needs no Views;orchestra_viewsadds richer trace and reporting dashboards on top. - Task timeouts in the engine core: a parking node opts in with a
timeoutnaming aDeadlineProvider(when the wait runs out) and aTimeoutAction(what to do once it has), each with its own settings. The bundled providers arerelative(a duration, measured from each park, from the start of the run or from the first arrival at the step, falling back to the site-widedefault_timeoutthat is the safety net against any task hanging forever) andabsolute(a date, optionally shifted by a signed offset). A module ships its own when the deadline belongs to a live subject the run does not own: it is asked as the task parks, so no step has to exist merely to keep a written deadline current. On expiry a cron sweep runs the action:resume(the default),notify(a recurring reminder via an event/ECA),spawn(fork a parallel branch, keep waiting) andunclaim(release a stale claim); an action that resumes with an outcome of its own declares it, so the editor can tell an author what to route on. For a schedule, a node declares stagedtimers({after, action, repeat}) that the engine arms as one timer token each: fired at its offset, canceled when the task is answered. Arepeatcount makes a timer recurring (re-arm N times, or 0 = until answered), so one stage can remind every day while another escalates or gives up later. See Timers. - Token cancellation:
WorkflowEngine::cancel()moves a live token and its live descendants to a terminalCANCELEDstate and completes the instance if nothing else is live. The shared primitive for ending work early: a superseded branch, a task's pending timers, the late branches of a early-firing join. - Subprocesses: a
subprocesstask type runs another workflow as a child (in the parent's tenant), parks the parent token, and resumes it when the child completes, injecting the child's mapped output into the parent (input/outputrename maps, one variable per line,nameorname|other). The engine resumes the parent generically via the instance'sparent_token; the mapping lives in the task type (SubprocessTaskInterface), so any task that runs a child process plugs in. The token parks, so escalation timers apply to a stuck child, and a timeout (or a cancel) that moves the parent on cancels the orphaned child. - Task reassignment (
orchestra_inbox): on top of claim (self-assign), the inbox offers Reassign (hand a task to another user, who becomes its claimed assignee) and Return to pool (clear a claimed task's assignee so its audience can claim it again). A manager (the newreassign orchestra taskspermission) may reassign any task; a task's current assignee may reassign their own. Reassignment goes throughWorkItemManager::reassign()(terminal tasks are immutable) and is logged to the orchestra channel for an audit trail. It transfers ownership outright, which is what distinguishes it from a delegation. - Delegation, absence cover (
orchestra_delegation): for a period, one user acts on another's tasks, seeing both their pooled and their already-claimed work in their own inbox and completing it on their behalf. Core carries only the question (DelegationResolverInterface, answered "nobody" by default); the submodule stores the delegations and decorates it, so a site whose absences live in an HR system can answer from there instead. One hop (cover does not chain), one tenant, and it grants no permission: the delegate still needsprocess orchestra tasks. A completed task records the delegator in itson_behalf_offield, distinct from the assignee (which a delegation never moves) and the completer (who acted). See Delegation. - Reminders and escalation to a person (
orchestra_inbox): areassigntimeout action (the timer-driven half of the manual Reassign) hands a timed-out task to an audience chosen with an assignment plugin (the sameusers/roles/users_variable/roles_variableplugins assignment uses). A single resolved user is assigned the task directly; a pool (a role, several users) is re-offered to claim. Paired with the recurringnotifyreminder in a stagedtimersladder, the common "remind the assignee at T1, escalate to a manager at T2" pattern is now pure config, with no custom code. See Timers. - Loops and task regeneration: a flow pointing back to an earlier node is a
loop, with no special construct. Re-entering a parking node regenerates its
work (a
userre-parks and creates a fresh task each pass). A join fires once for each pass through its opening split, with that pass's branches only, so a loop back through the split starts a new pass that the join fires for again. A join set to cancel the split tears down only the pass that fired it. See Loops and re-entry. - Views dashboards and bulk actions (
orchestra_views,orchestra_vbo,orchestra_vbo_inbox): the runtime entities, instances, tokens, variables and tasks, are exposed to Views with readable workflow and node labels, their references as relationships, and a current-tenant filter (backed by anorchestra_tenantcache context), plus six ready-made dashboards: processes, tokens, completed processes and one instance's trace here, with the inbox and the acting user's own tasks shipping inorchestra_inbox_views, which reads the work item.orchestra_vbolayers Views Bulk Operations actions over them, cancel or delete a process, signal or cancel a token, andorchestra_vbo_inboxthe task-inbox actions (claim, complete with an outcome, reassign). See Views and dashboards. - Content-bound tasks (
orchestra_content): a process binds to a content entity (via theorchestra_attachmentside entity, no fields on the kernel), and anentity_forminteraction (on an interaction task) opens that entity's edit form as the work of the step, save-and-decide in one screen, reusing the inbox, assignment and outcomes. Anattach_entitytask binds content the run only learned about while running, the entity two of its own variables name. See Content-bound tasks. - Content moderation (
orchestra_content_moderation): amoderation_transitiontask sets the attached entity'smoderation_state, so an Orchestra decision drives Drupal's editorial states, the approved branch publishes, the rejected branch sends back. The workflow graph is the mapping. Ships anexample_content_reviewworkflow, which binds its own content at its first step, so starting it takes nothing but the two variables naming it. - Content + ECA glue (
orchestra_content_eca): a "load the attached entity" ECA action, so a model reacting to an Orchestra event can act on the run's content, for example setting its state via content_moderation's own ECA action. Starting a content-bound run needs nothing from this module: the generic "start process" action names the entity in variables and the workflow's attach step binds it. Ships an example ECA model that reviews new articles, which is exactly that pairing. - Action tasks (
orchestra_action): anactiontask runs a Drupal Action plugin as automated work, then advances, Orchestra's answer to Maestro's batch function, on Drupal's own action system rather than a callable string. An entity action runs on the entity a variable names, or, through thecontent_actiontask itsorchestra_content_actionsibling ships, on the run's own attachment; a system action runs as-is; a configurable action's settings are embedded in the node. A failing action with a payload variable routes to an error branch (success/failure), or halts the step on an incident without one. An action implementing the kernel'sOrchestraContextAwareInterfaceis handed the instance and token first, so it can read and write process variables, parity with a Maestro function's process/queue IDs. See Action tasks. - Durable audit log: the engine and inbox dispatch a neutral
OrchestraAuditableEventat each lifecycle transition (instance started/completed/failed/canceled, a canceled token, the human-task transitions), correlated by the instance UUID. The optionalorchestra_audit_trailsubmodule records those into the Audit Trail chain, a durable, tamper-evident record that survives instance deletion, with per-event config gating and a permanent/transient bucket split. The kernel keeps no audit_trail dependency (it only dispatches the event), so any subscriber, a log of your own for example, can record it instead. Notifications do not ride it: reaching people is a separate concern, with an event of its own. See Audit log. - Incidents: a token whose task fails deterministically is dead-lettered after a bounded number of retries instead of retrying forever. By default this raises an incident and leaves the instance's other branches running for an operator to resolve (retry, resume, skip, cancel the branch, or fail the instance), rather than failing the whole process. See Incidents.
- Notifications (
orchestra_mail): the inbox dispatches those same neutral events, and the optionalorchestra_mailsubmodule turns the two human-task ones into email, a message to a task's audience when it is assigned and a reminder when it times out. Who is notified is configured per assignment, anotifyflag on eachusers/roles/users_variable/roles_variableassignment, which resolves its own audience to recipients, so pooling to a broad role never mails people by surprise (named users notify by default, a role opts in). The kernel keeps no mail dependency; swap the delivery channel to deliver the same events another way (Easy Email, the Message stack).orchestra_interactiondispatches the neutral event for the link instead of the inbox, for a party reached through the link doorway: a node's Notify on arrival audience is sent the capability link to its step when the branch parks, and any channel delivers it. See Notifications. - Early-firing threshold join (
threshold): an N-of-M join that fires the moment N branches arrive, counting the branches still on their way, then cancels the branches still heading to it (and their inbox tasks), or the whole split they came out of when set to do so, so a late branch neither carries on nor re-fires the join. What is on its way is read from the workflow's flows, whatever their conditions. Each token records its fork cohort; canceling a split closes it, and a still-live branch of a closed cohort self-cancels at its next step, with no lock on the hot path. Cohorts are per-split and per-iteration, so a loop starts a fresh one. See Joins and splits. - Quorum join (
quorum): the value-aware sibling of the threshold join. It tests its quorum condition, a flow condition, on each arrived branch and fires as soon as the decision is settled, either the condition holds on enough branches, or that becomes unreachable (enough have come back without it), ending early on both outcomes and tearing down what is left as the threshold join does. Routing on the outcome compares the number of branches the condition held on, which the join writes to its result variable when it fires. See Joins and splits. - Join timeout (
join_timeout): any join that waits for branches can give up on a deadline, with the deadline providers, anchors and outcome a task's timeout has. Fire the join with what arrived, remind, or stop on an incident. See Timeouts at a join. - Domain tenancy (
orchestra_domain): resolve the active tenant from the current domain. Each tenant names the domains it lives on, on its own Domains form (a domain record holds Domain's configuration and nothing of ours), and a tagged tenant resolver maps the negotiated domain back to the tenant that names it, so each domain runs its own isolated set of processes on one site. See Multi-tenancy. - Pull-based human operations: the
user_operation,comment_operationand interaction-operation task types park an assignment-gated step and mint the sameWorkIteman inbox task does, differing only in its mode:operationrather thantask, so it is acted on from the "My Orchestra actions" list instead of the inbox. Completion is unified with the inbox behind oneOutcomeSignaler. See Human tasks. - Payment as a workflow step (
orchestra_payment, experimental): a payment interaction node and a settlement subscriber (authorize-then-capture, settle or fail a parked payment by hand), backed by the Kessai payment engine. The step never prices what it has not first held still: rendering the page previews only, and the click on "Go to payment" announces a checkout opening, so a domain module freezes whatever it owns before the charge is priced. The amount the link holder was shown travels back signed and is compared against what the frozen subject prices, and a subscriber may refuse the checkout outright with reasons the payer reads. See Payment. - Task failure classification and retry: a task failure is retryable, an outcome, or fatal, with a configurable retry policy and an on-demand Retry action from the instance trace. See Incidents.
- Notification template resolution by a tenant / workflow / type / suffix cascade for the Easy Email channel. See Notification delivery.
- Nobody waits for a notification to be sent, and a lost one is sent again: each is queued with the step and goes once the page has been sent, before it, or by cron after a delay, as its notification profile says. A failed send is tried again for the recipients it missed, and one given up is a line in the process's history or, for an interaction link, an incident whose Send again sends it. A node, a workflow, a tenant and the site each name the profile. See When a notification is sent.
- The process initiator: the start-user is recorded on each instance (distinct from the audit actor), surfaced as a list column, a Views field and a filter. See Concepts.
- A requester-facing surface: a "My requests" page and a "my workflow instances" View list the instances a user started, each with a read-only detail view, gated by a dedicated View own Orchestra process instances permission (no operator rights). See Views and dashboards.
- A workflow-authored status vocabulary: the tenant-scoped
orchestra_statusconfig entity a node references and a requester filters on, resolved to a labeled, palette-styled tag, distinct from the engine state. Managed at Configuration > Workflow > Orchestra > Statuses, exposed as a State/Status Views field and filter and the[orchestra:status]token. See Concepts.
Planned¶
Nothing is currently queued: the engine is feature-complete against this roadmap.
Maestro parity¶
Orchestra's engine is, by design, more capable than Maestro's (a token/Petri-net core with real gateways, token-scoped variables and merge/quorum); the gap was only the content-workflow ergonomics and task-management UX layered on top, and that gap is now closed.
At parity: content-bound interactive tasks (orchestra_content's entity_form
interaction), content moderation (orchestra_content_moderation) and
automated / named-function tasks (orchestra_action), the content-approval core
of Maestro; loops and task regeneration (a flow back to an earlier node
regenerates a re-entered task, see
Loops and re-entry); template clone plus
import/export (the Modeler API's Export, Export as recipe and Import, and
entity_clone, see Integrations); a durable, tamper-evident
audit log (orchestra_audit_trail, see Audit log); and email
notifications on assignment and reminder (orchestra_mail, see
Notifications).
The content-approval workflows Maestro targets are fully covered.