Skip to content

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.comment alongside decision.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_form with its interaction_settings: form_operation (which entity form mode to open, e.g. edit) and attachment_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

  1. An article is created; a content workflow is started attached to it (startForEntity($article, 'content_review')).
  2. The token reaches the interaction-task review node (its entity_form interaction), parks, and a work item appears in the editors' inbox.
  3. An editor opens it: the article's edit form renders, with Save and approve / Save and reject buttons.
  4. They fix a typo and press Save and approve; the article is saved and the task completes with approved; decision is set to approved.
  5. 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_eca submodule 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).