Skip to content

Support dynamic (catch-all) workflows#774

Draft
datashaman wants to merge 1 commit into
temporalio:masterfrom
datashaman:feat-dynamic-workflow
Draft

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

Conversation

@datashaman

Copy link
Copy Markdown

Draft — the PHP side is complete and unit-tested, but end-to-end dispatch depends on temporalio/roadrunner-temporal#784 (register the dynamic proxy) and temporalio/sdk-go#2449 (execute a factory-registered dynamic workflow). Opening now to start the conversation and cross-link the chain.

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 (Support dynamic (catch-all) workflows roadrunner-temporal#784).
  • 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.

Dependencies

End-to-end dispatch needs the rest of the chain:

This PR (the PHP declaration + dispatch) is self-contained and unit-tested independently of those.

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 (roadrunner-temporal#784 + go-sdk#2449): starting an unregistered type (pipeline-blog-publish) is caught by the dynamic handler, which reads the real type via Workflow::getInfo() and completes.

Backwards compatibility

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

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 6041218 to 17b1122 Compare July 10, 2026 12:32
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