Support dynamic (catch-all) workflows - #774
Conversation
6041218 to
17b1122
Compare
17b1122 to
7eea800
Compare
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).
7eea800 to
f51c350
Compare
|
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 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. |
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 viaWorkflow::getInfo()and receives the raw args.WorkflowPrototype::isDynamic()— set byWorkflowReaderfrom the attribute.StartWorkflow— whenworkflows->find($type)misses, falls back to the registered dynamic prototype (mirrors Python'sself._workflows.get(type, self._dynamic_workflow)).GetWorkerInfo— advertisesdynamicper 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 raisesTypeError; here aLogicException), 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:
go.temporal.io/sdk v1.47.0.RegisterDynamicWorkflowwhen thisdynamicfield is set. It merged on August 6, 2026, and pins Go SDK v1.47.0. The change is not yet included in a taggedroadrunner-temporalrelease or released RoadRunner binary.This PR (the PHP declaration + dispatch) remains self-contained and unit-tested independently of the RoadRunner release cycle.
Testing
tests/Unit/Declarationcoverage, run against both the attribute and annotation readers:testDynamicWorkflow—#[WorkflowMethod(dynamic: true)]setsisDynamic().testMultipleDynamicWorkflowsAreRejected— a second dynamic workflow on a worker throws.composer run test:unit, 692 tests).composer run cs:diffclean.composer run psalmreports no new issues in the changed files.rrcontaining 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 viaWorkflow::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.
dynamicdefaults tofalse, so existing workflows and workers behave exactly as before.