Compared to Maestro¶
Maestro is the established workflow engine for Drupal: mature, battle-tested and especially strong at content-approval flows. Orchestra is not a re-implementation of it. It is a different engine (a token / Petri-net core) built to remove a handful of structural limits that Maestro's design runs into once a workflow stops being a single, mostly-linear approval chain.
This page names those limits precisely and shows, with examples, how Orchestra addresses each. Where Maestro is still ahead today, the last section says so.
Fair comparison
Maestro is a solid, widely deployed module and the points below are about design trade-offs, not quality. For a classic "edit content → review → publish" approval, Maestro is a mature, proven choice; Orchestra offers the same flow (see Content-bound work and approvals) but is a newer project (see Where Maestro still leads).
At a glance¶
| Dimension | Maestro | Orchestra |
|---|---|---|
| Execution model | Task queue driven by a template graph | Tokens on nodes (Petri-net) |
| Branching / joining | Dedicated task types (If, And, Or) | A property of any node (split / join plugins) + per-flow conditions |
| Variables | Process-global | Instance-wide and token-scoped (branch-local), with lineage + merge |
| Quorum ("2 of 3") | Custom logic | wait_all + merge + a count condition, no custom code |
| Tenancy | Single realm | Multi-tenant from the first install |
| Distribution | In-process, single site | Same contract in-process or cross-site over an OAuth HTTP API |
| Extensibility | Task types in code | Attribute plugins for every extension point |
| Authoring | Bundled canvas builder | Standard BPMN.io and an accessible form modeler |
| Drupal baseline | Long support history | D11.3+ only, OOP hooks, D12-ready |
1. Routing is a property, not a task type¶
The Maestro limit. Branching and synchronization are task types you drop on the canvas: an If task to branch, an And task to fork/join all branches, an Or task for inclusive logic. The control flow is therefore modelled as extra nodes, and a pattern that does not map cleanly onto If/And/Or (say, "continue when most of the started branches are back") means custom code.
flowchart LR
s([Start]) --> a[Prepare]
a --> and1{{"AND (split)"}}
and1 --> l[Legal review]
and1 --> f[Finance review]
l --> and2{{"AND (join)"}}
f --> and2
and2 --> e([End])
In Orchestra, branching falls out of three orthogonal knobs that live on an
ordinary node: each outgoing flow may carry a condition, the node's
split decides how many of the eligible flows to take, and the node's
join decides when to fire on the incoming side. The named gateways are just
presets over those knobs: parallel = (all, wait_all),
inclusive = (all, matching), exclusive = (first, immediate), so you rarely
add a routing node at all.
flowchart LR
s([Start]) --> a["Prepare<br/>split: all"]
a -->|contract present| l[Legal review]
a -->|amount over 10k| f[Finance review]
l -->|contract present| j["Join: matching"]
f -->|amount over 10k| j
j --> e([End])
Here Legal runs only when there is a contract and Finance only above a threshold; the matching join waits for exactly the branches that started (see Joins and splits). The same diagram in Maestro needs an Or-split plus an Or-join task and careful wiring; in Orchestra it is two flow conditions and a join policy on the node that was already there.
2. Token-scoped variables and merge¶
The Maestro limit. Process variables are global to the process. That is fine for a linear flow, but the moment two branches run in parallel they share one variable namespace: each branch writing its own "verdict" overwrites the others, so collecting a result per branch needs uniquely-named variables and bespoke logic.
In Orchestra, a variable is instance-wide by default or token-scoped: scoped to a token, it is visible only to that token's lineage (itself and its descendants), so sibling branches do not collide. A join can then merge one variable from each joined branch into a list. Quorum is the worked example:
flowchart LR
f["Fork: all"] --> r1["Reviewer 1<br/>vote (token-scoped)"]
f --> r2["Reviewer 2<br/>vote (token-scoped)"]
f --> r3["Reviewer 3<br/>vote (token-scoped)"]
r1 --> t["Tally<br/>join: wait_all<br/>merge vote → votes"]
r2 --> t
r3 --> t
t -->|2 or more approved| ok([Approved])
t -->|otherwise| no([Rejected])
n_tally:
type: passthrough
join:
plugin: wait_all
settings:
collect: vote # each reviewer's branch-local vote
into: votes # gathered into a list on the continuing token
scope: token
"Two of three approve" is wait_all + merge + a count flow condition, with no
special node and no custom code. Maestro has no token-scoped variable or join
merge to build this from.
3. Multi-tenant from the start¶
The Maestro limit. Maestro models a single workflow realm. Running isolated workflow spaces (per client, per site section, per domain) on one install is not part of its model.
In Orchestra, every runtime entity (instance, token, variable, task) carries
a tenant, stamped at creation from the active tenant, and the active tenant is
chosen by pluggable resolvers. A query, a dashboard or a purge in one tenant
never reaches another's rows; a single-tenant site simply uses the default
tenant and behaves as if tenancy were absent.
flowchart TB
req[Request] --> res["Tenant resolver(s)"]
res -->|tenant A| a[("A: instances · tokens · variables · tasks")]
res -->|tenant B| b[("B: instances · tokens · variables · tasks")]
See Multi-tenancy.
4. Same code, in-process or cross-site¶
The Maestro limit. A Maestro engine drives workflows on the site it runs on. There is no first-class contract for starting or signalling a process on a different site.
In Orchestra, callers depend only on OrchestraClientInterface. Binding it
to LocalOrchestraClient runs against the in-process engine; binding it to
RemoteOrchestraClient runs the same calling code against a remote Orchestra
over an OAuth-gated HTTP API, with each consumer scoped to a tenant.
flowchart LR
caller["Your code"] --> api[["OrchestraClientInterface"]]
api -->|same-site| local["LocalOrchestraClient → engine"]
api -->|cross-site| remote["RemoteOrchestraClient → OAuth HTTP"] --> srv["Remote Orchestra"]
5. Everything is a plugin¶
The Maestro limit. Extending Maestro means working with its task-type classes and template internals. Useful, but the extension surface is a fixed set of task concepts.
In Orchestra every extension point is an attribute-based plugin you add without patching the engine:
TaskType: what a node does (start,end,passthrough,wait,user,subprocess,eca_event,entity_form,moderation_transition,action, …).FlowCondition: when a flow is eligible (comparison,all/any,count).Split/Join: the routing policies of §1.Audience: who a node reaches; a staffing audience (users,roles,variable, …) also implementsAssignmentInterfaceto staff a human task.TimeoutAction: what a timer does (resume,notify,spawn,unclaim,reassign).
A new gateway, a weighted split, a new audience, a new escalation: each is a small plugin, not a core change.
6. Standard, accessible authoring¶
The Maestro limit. Workflows are drawn in Maestro's own bundled canvas editor.
In Orchestra, authoring is split from the engine and offered two ways:
BPMN.io for a standard-notation diagram that round-trips
id-stable (so editing a definition never orphans running instances), and
orchestra_cm, an accessible, form-based modeler for building the same
workflows without a drag-and-drop canvas at all.
7. Observability¶
Orchestra exposes its runtime to Views (orchestra_views): tenant-aware
dashboards for running processes, live tokens and the task inbox; bulk actions
over them (orchestra_vbo); and a per-process trace: the node-by-node
history every finished process retains. See Views and dashboards.
Content-bound work and approvals¶
Maestro's signature strength is a process about a piece of content, with a step that opens that entity's edit form and an outcome that drives its editorial state. This has a direct Orchestra equivalent, built as additive plugins on the engine:
- Content-bound, interactive tasks (
orchestra_content): a process attaches to a content entity, 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 the approved branch publishes and the rejected branch sends back: the graph is the mapping. - Automated / named-function tasks (
orchestra_action): anactiontask runs a Drupal Action plugin as automated work: the answer to Maestro's batch function, on Drupal's action system rather than a callable string. See Action tasks. - First-class notifications (
orchestra_mail): templated assignment and reminder emails, with notification opted in per assignment, rather than leaning on ECA wiring. See Notifications. - Durable audit log (
orchestra_audit_trail): an append-only, tamper-evident record of every transition that survives instance deletion, alongside the live trace. See Audit log.
Where Maestro still leads¶
Orchestra's engine is, by design, the more capable core, and it covers the content-approval ergonomics above. Maestro is still ahead on:
- Maturity. Years of production use, documentation and community recipes, which only time and adoption build.
The content-approval feature gaps that were tracked under Maestro parity are now covered (see the roadmap). The point of Orchestra is that the hard parts (real concurrency, branch-local data, quorum, tenancy, distribution) are in the engine from day one, and the content-workflow conveniences are additive plugins on top.