Problem/Motivation
Nothing outside AttachmentManager can bind a content entity to a run. The orchestra_content_eca submodule ships an action that loads the attached entity and one that starts a process for an entity, and there is no task type and no action that attaches one, so a binding can only be made from PHP, at start.
That leaves a run unable to bind something it produced itself. A process that creates an invoice at its fourth step, or that computes which document a reviewer should work on, has no way to record either as an attachment, and the steps that read attachments - an entity-form interaction, a moderation transition, an action targeting the attached entity - cannot be pointed at it.
Separately, AttachmentManager::startForEntity() has outlived its reason. Since ProcessControlInterface::start() gained an on_started callable it is a wrapper around one expression a caller can write itself, and the whole of what it buys is documentation of the ordering. What it costs is the attachment manager injecting the workflow engine, which appears in exactly one line of the class, so the thing whose job is bindings carries the thing whose job is running workflows.
Proposed resolution
A task type in orchestra_content that binds an entity to the instance its token belongs to. It takes the entity type, the id to bind, read from a process variable the model computed, and the attachment key to bind under, so a run can carry several attachments each in its own role.
It follows the action task's shape for failure. With a payload variable configured, a binding that cannot be made is caught and the variable set to failure, and to success otherwise, so an outgoing flow condition routes to an error path rather than letting the run walk into a later step whose attachment is not there. Without a payload variable the failure propagates and the engine dead-letters it, so a misconfigured node is never swallowed.
The entity type is chosen from the content entity types the site has rather than spelled into a textfield, so a type that does not exist never reaches the configuration. Whichever type a node settles on belongs to some module, and the walk that records a workflow's module dependencies reads plugin ids, which an entity type is not, so the module answers for its own vocabulary through hook_orchestra_workflow_module_dependencies() and that module can no longer be uninstalled out from under a run that will need it.
The attachment key's width becomes one fact. Three ends have to agree on how long a key may be - the field definition, the manager's refusal, and the settings form - and each spelled out 64, with the manager's copy private and its docblock claiming to be the field's width without deriving it. The width moves onto AttachmentInterface, where the field it describes lives, and the ends read it. The Action task keeps its own copy of the key element, because it only optionally depends on Orchestra Content, so it asks for the width by name rather than restating it.
A variable holding something that is not an id is refused where it is read. A variable holds whatever was put in it, and the likeliest thing to point one at by mistake is a webform's captured values, which is an array. That was handed to the storage, where loadMultiple() flips the ids it is given: a value that is neither an integer nor a string warns and is dropped, and an array reaches PHP's own "cannot access offset of type array on array". Nothing was bound either way, but that is the message that reached the incident, naming neither the step nor the variable. Both the attach task and the Action task's variable target now say which variable was wrong and how.
startForEntity() goes, pre-1.0 and without a deprecation. Its ordering note moves onto attach(), where every caller reads it rather than only the ones who picked the wrapper, and carries the call it describes. The ECA action composes the engine and the manager itself; the tests do it through a trait, since the convenience is theirs to want and not the shipped API's.
A binding is security neutral, and the manager contract now says so once: attaching records which entity a run is about and nothing more, reading nothing from the content, writing nothing to it and saving nothing, so there is no operation to permit or refuse. Access to an attached entity is decided where one is acted on.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and write the code on its merge request. I reviewed both before posting.)
Issue fork orchestra-3622737
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #4
mably commentedComment #6
mably commentedComment #7
mably commentedComment #8
mably commentedComment #10
mably commented