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(AND-join), ormatching(the condition-driven OR-join); 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
orchestra_apisubmodule:OrchestraClientInterface, a transport-agnostic contract for driving workflows (start, signal, inspect, variables), withLocalOrchestraClientwrapping the engine in-process and scoping every operation to the current tenant. Callers depend only on the interface, so the same code will run same-site and cross-site. - The
orchestra_server_apisubmodule: an OAuth-gated HTTP surface (/orchestra-api/...) whose controllers are a thin transport over the 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. - External task handoff (
orchestra_inbox): a user node with ahandler_urlshows a "Process" action that claims the task and sends the assignee to that external handler, passing a signed completion URL. The handler resumes the process by calling that URL with its signature (no login), and the call is idempotent. - 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 a
mergethat collects one variable from each joined branch into a list; with thecountcondition 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. - 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; an empty set leaves the task pooled. Four 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); a node carries them as the flat modeler fieldsassignee_users/assignee_rolesor as a structuredassignmentslist (several union). New audiences (group, expression) are new plugins minting a new token namespace, with no schema or storage change. Theexample_quorumworkflow shows all three forms. - 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. 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
timeout(seconds or an ISO-8601 duration), and a site-widedefault_timeoutis a safety net against any task hanging forever. On expiry a cron sweep runs the node'sTimeoutActionplugin:resume(the default),notify(a recurring reminder via an event/ECA),spawn(fork a parallel branch, keep waiting) andunclaim(release a stale claim). For a schedule, a node declares stagedtimers({after, action, repeat}) that the engine arms as one timer token each: fired at its offset, cancelled 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 terminalCANCELLEDstatus 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 discriminator 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 result into the parent (input/outputrename maps, one variable per line,nameorname: other). The engine resumes the parent generically via the instance'sparent_token; the result 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 and delegation (
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 delegate their own. Reassignment goes throughWorkItemManager::reassign()(terminal tasks are immutable) and is logged to the orchestra channel for an audit trail. - 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), and a join re-entered by a loop re-arms on its own, its only state is the tokens waiting at the node, which firing consumes, so the next pass starts clean. (The early-firing threshold join scopes its straggler teardown per iteration with fork cohorts: a loop re-entering the split starts a fresh cohort, so closing one does not touch the next pass.) 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 three ready-made dashboards (processes, tokens, inbox).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_formtask opens that entity's edit form as the work of the step, save-and-decide in one screen, reusing the inbox, assignment and outcomes. See Content-bound tasks. - Content moderation bridge (
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. - Content + ECA glue (
orchestra_content_eca): a "start process for this entity" ECA action (start + attach in one step) and a "load the attached entity" action, so an event model starts content reviews and a reacting model can set state via content_moderation's own ECA action, the "when to start" living in the model. Ships an example ECA model that reviews new articles. - 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 attached entity or one named by a variable; a system action runs as-is; a configurable action's settings are embedded in the node. A failing action with a result variable routes to an error branch (success/failure), or fails the process 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/cancelled, a cancelled token, the human-task transitions), correlated by the instance UUID. The optionalorchestra_audit_trailsubmodule bridges 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 the same event is the spine the notifications feature reuses. 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; disable the submodule to deliver the same events another way (ECA, the Message stack). A companionorchestra_interaction_notificationsubmodule dispatches the neutral event for the link instead of the inbox, for a party reached through the bearer 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, then tears the rest of the fork cohort down, cancelling the branches still in flight (and their inbox tasks) so a late branch neither carries on nor re-fires the join. Each token records its fork cohort; firing early 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 reads each arrived branch's collected vote and fires as soon as the decision is settled, either enough branches approve, or approval becomes unreachable (enough have come back negative), ending early on both outcomes and tearing the rest of the cohort down. Routing to the approved or rejected path is the usual merge plus acountcondition over the collected votes. See Joins and splits. - Timeout join (
timeout): waits for every incoming branch likewait_all, but gives up once a deadline passes, firing with whatever has arrived and tearing the branches still running down (reusing the threshold join's cohort teardown). The deadline is armed on the first branch to wait and the cron timeout sweep wakes the join, so it needs no clock beyond the sweep already run for parked tasks. See Joins and splits. - Domain tenancy (
orchestra_domain): resolve the active tenant from the current domain. Each domain is bound to a tenant (a third-party setting on the domain record, edited on the domain form), and a tagged tenant resolver maps the negotiated domain to it, so each domain runs its own isolated set of processes on one site. See Multi-tenancy.
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
task), the content-moderation bridge (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.