Repository navigation
Description
Primary owning public component
Workflow engine
User problem
In our hexagonal architecture, domain entities are plain PHP objects and must not depend on Laravel, Eloquent, or a persistence framework.
We would like to pass a domain entity to an activity and have it restored by the receiving worker through an application-owned repository, similar to the Eloquent model experience in the classic Laravel API.
Currently, V2 activity arguments and the standalone PHP SDK use a portable Avro value contract. Arbitrary domain objects cannot be passed directly. Applications can pass an entity ID and load it inside every activity, but they must implement the adaptation and resolution logic themselves.
A reusable, opt-in contract would allow applications to pass domain entity references without introducing framework dependencies into the domain layer.
This request concerns references to persisted entities. Passing an immutable DTO or a snapshot is a separate use case.
Proposed public contract
Introduce an opt-in mechanism for adapting registered domain entities to portable references and resolving those references when an activity starts.
- An application registers an adapter for each supported domain type. The adapter converts an entity to a reference containing a stable logical type, identifier, and reference format version.
- The domain entity does not have to implement a Durable Workflow or Laravel interface. The adapter and resolver belong to the application or infrastructure layer.
- References use existing portable payload types. They do not contain serialized PHP objects, executable data, credentials, or arbitrary object graphs.
- Resolution is explicit: a matching array alone must not trigger a repository lookup. The worker resolves registered references through an application-owned port or repository before invoking the activity.
- The resolver validates the type, identifier, version, expected parameter type, and scope. Tenant authorization comes from trusted execution context; a tenant ID supplied in a reference must not be trusted by itself.
- Workflow history stores the reference. Workflow replay must not resolve the entity or access a repository.
- By default, each activity attempt resolves the current persisted state. A snapshot or a revision-pinned reference has to be requested explicitly and supported by the application.
- Missing entities, unsupported reference versions, unknown types, authorization failures, and resolver errors produce clear, documented failures with defined retry behavior.
- Existing payloads remain compatible. Unregistered objects must not silently fall back to PHP object serialization.
The exact interface names and registration API are open for discussion.
Acceptance criteria
- A plain PHP domain entity can be adapted and resolved without importing Laravel or Eloquent into the domain layer.
- An integration test passes a registered entity reference to an activity and receives the resolved domain object in the declared parameter.
- The persisted activity argument contains a portable reference, not the entity's complete state.
- Workflow replay does not invoke the resolver or access a repository.
- Tests demonstrate and document what state is loaded on an activity retry.
- Tests cover missing entities, unsupported reference versions, unknown types, resolver failures, and unauthorized or mismatched tenant scope.
- Ordinary arrays cannot accidentally trigger resolution, and unsupported objects fail clearly.
- Existing V2 payloads, histories, and classic Laravel model serialization remain compatible.
- Documentation explains registration, authorization, reference versus snapshot semantics, retries, and the alternative of passing an ID explicitly.
- The initial scope explicitly states whether nested references, collections, activity results, and other SDK languages are supported.
Supporting public evidence
No response
Dependencies and affected repositories
The classic Laravel API already has a related model-reference mechanism:
- The model serialization test verifies that activity and workflow jobs serialize a Laravel ModelIdentifier and restore the model with the original ID:
https://github.com/durable-workflow/workflow/blob/2.17.1/tests/Unit/Traits/SerializesModelsTest.php - An example workflow passes a User model to an activity:
https://github.com/durable-workflow/workflow/blob/2.17.1/tests/Fixtures/TestModelWorkflow.php
V2 uses a different serialization path:
- Activity arguments are serialized through the V2 codec:
https://github.com/durable-workflow/workflow/blob/2.17.1/src/V2/Support/WorkflowExecutor.php#L2503-L2507 - The Avro serializer supports portable values and specific wrappers, but rejects arbitrary PHP objects:
https://github.com/durable-workflow/workflow/blob/2.17.1/src/Serializers/Avro.php - Avro is the registered V2 payload codec:
https://github.com/durable-workflow/workflow/blob/2.17.1/src/Serializers/CodecRegistry.php - The standalone PHP SDK restricts payload codecs to its Avro implementation:
https://github.com/durable-workflow/sdk-php/blob/3.8.2/src/Client.php
These links describe current behavior; they do not establish that a public domain-reference extension API already exists.
Public intake checks
- I searched open and closed GitHub issues for an existing request.
- This request contains only public product contracts and public-safe rationale.
Activity
rmcdaniel commentedon Nov 3, 2026
Thanks for the detailed request. We accept it as an opt-in V2 feature. Priority is P3, so it is scheduled behind the open P2 defects. The open design points are decided below.
What we found in the current code
src/Serializers/Avro.php). That schema has no type tag. A map that looks like a reference decodes as an ordinary array, so the payload alone cannot say "this is a reference".AvroandCodecRegistryhave no extension point, and we will not add one for this. The Avro value schema is the cross-SDK wire contract.ActivityEntryArgumentsbefore invoking the activity (RunActivityTask,LocalActivityExecutor). Replay never reaches that step. Resolution belongs there.Decisions for the first version
workflows.v2config, next to the existing type maps. Domain classes do not implement any Durable Workflow or Laravel interface.Scope
We also noticed that
Serializer::normalizeForCodecconverts Eloquent models toModelIdentifier, which the Avro codec then rejects. We are checking whether Eloquent models passed as V2 activity arguments fail today, and will track that as a separate defect if confirmed.rmcdaniel commentedon Nov 4, 2026
Owner: Workflow. The approved scope is top-level embedded activity arguments only, using an app-registered logical type adapter/resolver and explicit argument-position metadata, with ordinary Avro maps on the wire. No source branch overlaps #1030.
Next is the adapter/resolver API contract and a failing ordinary queue/local activity control. Capture identity at schedule time, resolve current application state once per actual attempt immediately before invocation, and perform no repository reads during workflow replay. Preserve normal argument arrays as arrays; reject unregistered objects and mismatched type/version/parameter shape. Missing/authorization retry policy stays application-owned as accepted. Nested references, results, workflow arguments and other SDKs remain outside this slice. I will return that concrete source contract before adding a new serialization mechanism.
rmcdaniel commentedon Nov 11, 2026
Owner: Workflow. Still accepted at P3, behind the open P2 defects and #1063. The design decisions above stand: top-level embedded activity arguments only, an app-registered adapter and resolver, and argument positions recorded at schedule time. The next action is a draft PR with that API and a failing queue and local-activity test.
rmcdaniel commentedon Nov 13, 2026
@chrom: draft PR #1126 is up with the adapter/resolver API and failing tests for the decided first version. It covers
EntityReferenceAdapter/EntityReferenceResolver, the portable{type, id, version}reference, recorded argument positions, typed failures, andworkflows.v2.entity_referencesregistration. The interface names were open in your request. If the shape doesn't fit your hexagonal setup (for example, if you'd rather have one port per type than separate adapter and resolver classes), say so on the PR before the implementation lands. Next action: Workflow implements it on #1126.rmcdaniel commentedon Nov 13, 2026
Workflow owns implementation on draft #1126 (current head e0fb950). I reviewed the draft API and its intentionally failing first cases; contributor feedback has not changed the accepted embedded-only scope.
Next source action after #863's active Security corrections: adapt only registered top-level activity arguments before Avro encoding, persist argument positions, then resolve with trusted attempt context for queue and local activities. Add plain-map, version/type mismatch, per-retry reload and local-activity controls while keeping replay resolver-free. This is an actionable implementation queue item, not blocked on contributor review.
rmcdaniel commentedon Nov 13, 2026
Workflow pushed the queued implementation in draft1126 at
9deb8787. Real attempts resolve authorized current state outside claim transactions; retries reload, replay does not access the repository, ordinary reference-shaped maps remain values, and recovery/export/import retain explicit positions. The draft config now bindsentity_classso argument checks precede lookup while Laravel dependency injection remains available. Focused checks pass18 unit cases/33 assertions and14 MySQL cases/49 assertions, plus ECS/PHPStan and boundary/credential checks. Required CI/corpus and full-cycle qualification are pending.The local control found a concrete implementation dependency: local callbacks currently run inside the hosting workflow SQL transaction (observed resolver transaction depth1). The increment refuses local entity references before lookup/scheduling instead of performing application I/O under that lock. Accepted local support remains unfinished and Workflow-owned. Next implementation action is to separate preparation, outside-transaction resolution/callback, and ownership-fenced outcome recording, then retain cold recovery/retry and replay controls. Draft1126 stays open; no native capability or delivered end-to-end claim is made. #1129 remains the immediate qualification/release priority.