Skip to content

Support dynamic (catch-all) workflows - #774

Open
datashaman wants to merge 2 commits into
temporalio:masterfrom
datashaman:feat-dynamic-workflow
Open

Support dynamic (catch-all) workflows#774
datashaman wants to merge 2 commits into
temporalio:masterfrom
datashaman:feat-dynamic-workflow

Conversation

@datashaman

@datashaman datashaman commented Jul 10, 2026

Copy link
Copy Markdown

What

Adds Dynamic (catch-all) Workflow support — a workflow invoked when the worker receives a type name that is not statically registered. PHP and TypeScript are the only SDKs without it (Go/Java/Python/.NET/Ruby all have it).

  • #[WorkflowMethod(dynamic: true)] — declares the dynamic workflow. The handler reads the real type via Workflow::getInfo() and receives the raw args.
  • WorkflowPrototype::isDynamic() — set by WorkflowReader from the attribute.
  • StartWorkflow — when workflows->find($type) misses, falls back to the registered dynamic prototype (mirrors Python's self._workflows.get(type, self._dynamic_workflow)).
  • GetWorkerInfo — advertises dynamic per workflow, so the RoadRunner temporal plugin can register a Go dynamic-workflow proxy.
  • WorkflowCollection — enforces at most one dynamic workflow per worker (Go panics, Python raises TypeError; here a LogicException), since a second catch-all would make dispatch ambiguous.

Why

Enables applications that author workflows at runtime (e.g. UI-driven pipeline/automation builders) to give each workflow its own type name — real identity in the Web UI — while one PHP handler interprets it, with no codegen or per-workflow deploy.

Integration status

The host-side dependencies are complete:

  • temporalio/sdk-go#2449 fixed factory-registered dynamic workflow execution and shipped in go.temporal.io/sdk v1.47.0.
  • temporalio/roadrunner-temporal#784 registers the shared factory proxy via RegisterDynamicWorkflow when this dynamic field is set. It merged on August 6, 2026, and pins Go SDK v1.47.0. The change is not yet included in a tagged roadrunner-temporal release or released RoadRunner binary.

This PR (the PHP declaration + dispatch) remains self-contained and unit-tested independently of the RoadRunner release cycle.

Testing

  • New tests/Unit/Declaration coverage, run against both the attribute and annotation readers:
    • testDynamicWorkflow#[WorkflowMethod(dynamic: true)] sets isDynamic().
    • testMultipleDynamicWorkflowsAreRejected — a second dynamic workflow on a worker throws.
  • Full unit suite green (composer run test:unit, 692 tests). composer run cs:diff clean. composer run psalm reports no new issues in the changed files.
  • Validated end-to-end with a custom rr containing the now-merged roadrunner-temporal#784 implementation and Go SDK v1.47.0: starting an unregistered type (pipeline-blog-publish) is caught by the dynamic handler, which reads the real type via Workflow::getInfo() and completes.

Automated end-to-end coverage can use the standard test binary once a RoadRunner release includes the merged plugin change.

Backwards compatibility

Additive. dynamic defaults to false, so existing workflows and workers behave exactly as before.

@datashaman
datashaman force-pushed the feat-dynamic-workflow branch from 6041218 to 17b1122 Compare July 10, 2026 12:32
@datashaman
datashaman force-pushed the feat-dynamic-workflow branch from 17b1122 to 7eea800 Compare July 30, 2026 12:43
@datashaman
datashaman marked this pull request as ready for review July 30, 2026 12:59
@datashaman
datashaman requested review from a team, roxblnfk and wolfy-j as code owners July 30, 2026 12:59
Add `#[WorkflowMethod(dynamic: true)]` to declare a dynamic (catch-all)
workflow — invoked when the worker receives a workflow whose type name is
not statically registered. WorkflowReader flags the prototype; StartWorkflow
falls back to the dynamic prototype when no named workflow matches; and the
GetWorkerInfo handshake advertises `dynamic` so the RoadRunner temporal
plugin can register a Go dynamic-workflow proxy for it.

As in the other SDKs (Go panics, Python raises), at most one dynamic workflow
may be registered per worker — WorkflowCollection enforces this.

Enables Dynamic Workflow support in the PHP SDK (PHP and TypeScript are the
only SDKs without it).

End-to-end dispatch also requires temporalio/roadrunner-temporal#784 (register
the dynamic proxy) and temporalio/sdk-go#2449 (execute a factory-registered
dynamic workflow).
@datashaman
datashaman force-pushed the feat-dynamic-workflow branch from 7eea800 to f51c350 Compare August 8, 2026 04:21
@datashaman

Copy link
Copy Markdown
Author

The RoadRunner side is now merged in temporalio/roadrunner-temporal#784, including the required Go SDK v1.47.0 update.

I’ve also added the missing PHP-side runtime and regression coverage here: raw ValuesInterface arguments for dynamic handlers, preservation of the actual workflow type, named-workflow precedence, per-worker registration, handshake metadata, and the no-fallback failure case. The full unit suite passes (1,185 tests / 3,397 assertions).

This PR is ready for review and can be merged independently. The only remaining dependency is a new roadrunner-temporal/RoadRunner release containing #784 before the feature can be used with an official binary; end-to-end behavior has already been validated with a custom build.

Please let me know if there's anything else I can add.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant