Skip to content

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"]

See Distributed execution.

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 implements AssignmentInterface to 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 an entity_form 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. See Content-bound tasks.
  • Content-moderation bridge (orchestra_content_moderation): a moderation_transition task sets the attached entity's moderation_state, so the approved branch publishes and the rejected branch sends back: the graph is the mapping.
  • Automated / named-function tasks (orchestra_action): an action task 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.