Skip to content

Support framework-independent domain entity references in activity arguments #996

Open

Description

@chrom

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:

V2 uses a different serialization path:

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

  1. added
    authority:githubGitHub is the authoritative lifecycle record for this work
    kind:featureA public product capability or experience is requested
    priority:untriagedMaintainers have not assigned a priority
    status:triageAwaiting maintainer classification
    on Nov 3, 2026
  2. added
    intake:approvedCurrent issue title and body revision is approved for authority intake
    on Nov 3, 2026
  3. rmcdaniel commented on Nov 3, 2026

    @rmcdaniel
    Member

    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

    • V2 activity arguments go through the fixed Avro value schema (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".
    • Avro and CodecRegistry have no extension point, and we will not add one for this. The Avro value schema is the cross-SDK wire contract.
    • The worker decodes arguments and resolves method dependencies in ActivityEntryArguments before invoking the activity (RunActivityTask, LocalActivityExecutor). Replay never reaches that step. Resolution belongs there.

    Decisions for the first version

    1. Explicit on both sides. The workflow passes the domain object. A registered adapter turns it into a reference when the activity is scheduled. The engine also records which argument positions hold references, together with the payload. The worker resolves only those recorded positions. An ordinary array with the same shape is never resolved, even when the parameter type matches.
    2. Wire format. A reference is an ordinary Avro map with a logical type, identifier and reference format version. The value schema does not change. Arguments without references are byte-identical to today.
    3. Registration. Adapters and resolvers are registered by logical type name in workflows.v2 config, next to the existing type maps. Domain classes do not implement any Durable Workflow or Laravel interface.
    4. Resolution. Each activity attempt resolves the current persisted state through the application resolver before the activity is invoked. The resolver checks the declared parameter type and the trusted execution context. A tenant value inside a reference is never trusted on its own. Snapshots and revision pinning are left to the application.
    5. Failures. Unknown type, unsupported version and parameter type mismatch fail the attempt as non-retryable before the activity runs. Missing entity and authorization refusal raise documented exception types that the application can mark retryable or not. Resolver exceptions follow the activity's normal retry policy.
    6. No silent fallback. Unregistered objects keep failing with the current unsupported-value error. There is no PHP object serialization fallback.

    Scope

    • In: top-level activity arguments for in-process V2 workers, both queue and local activities.
    • Out of the first version: nested references inside arrays, collections, activity results, workflow arguments, and other SDKs. External workers receive the raw argument envelope from the server, so sdk-php would need its own implementation, tracked separately if there is demand.
    • Passing an identifier and loading the entity inside the activity remains the documented baseline.

    We also noticed that Serializer::normalizeForCodec converts Eloquent models to ModelIdentifier, 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.

  4. added
    priority:P3Lower-priority product improvement
    status:readyReady for implementation
    and removed
    priority:untriagedMaintainers have not assigned a priority
    status:triageAwaiting maintainer classification
    on Nov 3, 2026
  5. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    Member

    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.

  6. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    Member

    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.

  7. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    Member

    @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, and workflows.v2.entity_references registration. 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.

  8. added
    status:in-progressApproved work is actively being implemented or validated
    and removed
    status:readyReady for implementation
    on Nov 13, 2026
  9. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    Member

    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.

  10. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    Member

    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 binds entity_class so 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:workflowauthority:githubGitHub is the authoritative lifecycle record for this workintake:approvedCurrent issue title and body revision is approved for authority intakekind:featureA public product capability or experience is requestedpriority:P3Lower-priority product improvementstatus:in-progressApproved work is actively being implemented or validated

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions