Content-bound tasks¶
The optional Orchestra Content submodule (orchestra_content) binds a
process to a content entity and adds a task whose work is editing that entity.
It closes the gap with Maestro's signature step: a process that is about an
article, and a review step that opens that article's edit form.
How a process attaches to content¶
The engine kernel stays content-agnostic: it knows nothing about entities.
Instead orchestra_content owns a small side entity,
orchestra_attachment, that references the process instance and the entity
(entity_type_id + entity_id). The binding is queryable both ways, and the
kernel and the instance entity are untouched:
| Question | Resolved by |
|---|---|
| Which content is this run about? | AttachmentManager::getProcessInstanceEntity($instance) |
| Which runs are about this content? | AttachmentManager::getEntityProcessInstances($entity) |
Start a process already bound to an entity with
AttachmentManager::startForEntity($entity, $workflow_id).
Multiple attachments (keyed)¶
A process is rarely about just one thing: an order has a customer and an invoice, a publication has an author and an editor. So a run can have several entities attached, each under a key (the role it plays). Every call takes an optional key; the empty key is the primary subject and is what the single-argument calls use, so a one-entity workflow needs no key at all.
// Bind the order (primary), then the customer and the invoice under keys.
$attachments->attach($instance, $order);
$attachments->attach($instance, $customer, 'customer');
$attachments->attach($instance, $invoice, 'invoice');
$attachments->getProcessInstanceEntity($instance); // the order (primary)
$attachments->getProcessInstanceEntity($instance, 'customer'); // the customer
$attachments->getProcessInstanceEntities($instance); // all three, keyed by key
$attachments->getEntityProcessInstances($customer, 'customer'); // runs where it is the customer
$attachments->detach($instance, 'customer'); // unbind one key, run unaffected
A process binds one entity per key: attaching again under a key already
bound re-points it at the new entity rather than adding a second row, and a
different key coexists. detach() unbinds one key (the binding only; the run
continues), and deleting the instance clears them all. The key is a short machine name (a role); a consumer
that acts on "the attached entity" (the entity-edit task, the moderation task,
the Action task's attached target, and the ECA actions) has an Attachment
key setting to say which one, defaulting to the primary subject.
The entity-edit interaction¶
The entity_form interaction (in orchestra_content) makes editing the
attached entity the work of a step. Attach it to an
interaction task: the node parks the token
and creates a work item like any user task, reusing assignment, outcomes and the
inbox, and opening the task hands the assignee to a single screen showing the
attached entity's edit form with:
- a plain Save: persists the edits but leaves the task open, so the reviewer can edit across several sittings before deciding;
- an optional Comment: recorded with the decision (as
decision.commentalongsidedecision.outcome); - one save-and-decide button per outcome: saves the entity and completes the task with that outcome (which the engine routes on), in one action.
flowchart LR
c["Article created"] --> s([Start])
s --> r["Review<br/>type: interaction_task<br/>interaction: entity_form<br/>outcomes: approved, rejected"]
r -->|approved| p([Publish])
r -->|rejected| d([Send back])
Node config (in the Complete modeler or in YAML):
n_review:
type: interaction_task
config:
outcomes: 'approved,rejected'
payload_variable: decision
assignee_roles: editor
interaction_plugin: entity_form
interaction_settings: '{"form_operation":"edit","attachment_key":""}'
outcomes/payload_variable/completion_scope/assignee_*: the same keys a user task uses; the chosen outcome is recorded and routed on by the outgoing flow conditions.interaction_plugin: entity_formwith itsinteraction_settings:form_operation(which entity form mode to open, e.g.edit) andattachment_key(which attached entity to edit; empty is the primary subject).
Opening the task hands the assignee to the content route
(/orchestra/content/task), which renders the attached entity's form under the
assignee's identity. The interaction owns its surface, so there is no external
URL to configure.
The route carries the parked token and nothing else. Which entity to open and
which of its forms are read from the step's own settings, never from the
request: a form operation selects the form handler the entity type builds, so a
request that could name it could open any form that entity type declares,
including its delete form. The screen then re-applies the entity's own update
access, which building a form directly would otherwise bypass. A
form_operation the entity type does not declare answers 404 and is logged,
naming the step.
Same machinery as a user task
Because an interaction task is a parking human task, everything the inbox offers applies: claim, reassign, pooled vs. targeted assignment, and escalation timers. Only the work differs: an entity edit form instead of outcome buttons.
Worked example: edit then approve¶
- An article is created; a content workflow is started attached to it
(
startForEntity($article, 'content_review')). - The token reaches the interaction-task review node (its
entity_forminteraction), parks, and a work item appears in the editors' inbox. - An editor opens it: the article's edit form renders, with Save and approve / Save and reject buttons.
- They fix a typo and press Save and approve; the article is saved and the
task completes with
approved;decisionis set toapproved. - The outgoing flow condition routes the token to the publish branch.
Driving the moderation state¶
The companion orchestra_content_moderation submodule (requires core
Content Moderation) adds a moderation_transition task that sets the
attached entity's moderation_state. Put it on the branch that should reach a
state, so the graph expresses the mapping (approved → set published,
rejected → set draft) with no mapping table. Orchestra is the authority on
when the state changes, so it sets the state directly (a state the entity's
moderation workflow defines; an entity that is not moderated is skipped).
Starting a process for an entity¶
The entity_form task edits the entity the process is attached to, so
something has to start the process and bind it (deliberately not a global
save-watcher). Two ways:
- In code:
AttachmentManager::startForEntity($entity, $workflow_id). - From ECA: the
orchestra_content_ecasubmodule adds a "Start Orchestra process for this entity" action; wire it to an entity insert/update event (the event's own condition is the de-duplication) and creating the content starts and attaches the process, the "when to start" living in the model, where Maestro puts it. That submodule also adds a "load the process's attached entity" action, so a model reacting to an Orchestra event can act on the content (e.g. set its state via content_moderation's own ECA action).
A ready-to-run example¶
orchestra_content_moderation ships an example_content_review workflow
(review the entity → approve publishes it, reject sends it back to draft), and
orchestra_content_eca ships an example ECA model that starts that review when
an article is created. Enable the modules to get both.
Status and follow-ups¶
Shipped: the attachment (one per key, several per run), the entity_form
interaction and edit screen, the moderation_transition task
(orchestra_content_moderation), and the ECA start/attach + entity-exposure
actions with example configs (orchestra_content_eca).
For a generic automated step that runs any Drupal Action plugin (of which
moderation_transition is a special case), see Action tasks
(orchestra_action).