Skip to content

Nexus: worker, workflow-backed operations, and workflow caller #813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 266 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
266 commits
Select commit Hold shift + click to select a range
2b34a17
Cleanup
dandavison Jun 22, 2025
99de490
Refactor test
dandavison Jun 22, 2025
7bca8e6
Failing test: request ID is not used for non-backing workflow
dandavison Jun 22, 2025
3344fee
Bug fix: wire request_id through as top-level start_workflow param
dandavison Jun 22, 2025
222dd9d
Rename: TemporalOperationContext
dandavison Jun 22, 2025
590af44
Rename: cancel_operation
dandavison Jun 22, 2025
8d61f18
Cleanup
dandavison Jun 22, 2025
051ea8d
Do not allow Nexus operation to set client used for starting workflow
dandavison Jun 22, 2025
48a9a79
Make task queue optional when starting workflows
dandavison Jun 22, 2025
2a1e5a5
Add nexus_task_poller_behavior
dandavison Jun 22, 2025
eba55cc
Handle PollShutdownError
dandavison Jun 22, 2025
b9799cb
Respond to upstream: default to async
dandavison Jun 23, 2025
943222e
Cleanup; changes from review comments
dandavison Jun 22, 2025
2224b8a
Respond to upstream: handler factory instead of sync_operation_handler
dandavison Jun 23, 2025
0626da7
Switch workflow_run_operation_handler to standard factory
dandavison Jun 23, 2025
220d0a4
Do not support passing client to cancel_operation
dandavison Jun 23, 2025
61ded24
RTU: bridge Rust
dandavison Jun 23, 2025
dcda5a5
Fix: make all methods `async def` on WorkflowRunOperationHandler
dandavison Jun 24, 2025
be1ad3f
Get rid of TypeGuard
dandavison Jun 24, 2025
93e19f6
Support passing result_type when getting workflow handle from token
dandavison Jun 24, 2025
24db857
Implement fetch_result handler
dandavison Jun 24, 2025
49721fa
Cleanup
dandavison Jun 24, 2025
903c3ae
Tests: clean up type annotation warnings
dandavison Jun 24, 2025
9e0f8a6
Improve type annotation warnings
dandavison Jun 24, 2025
a08b77e
Cleanup
dandavison Jun 24, 2025
5d742c1
Import nexus.handler.logger in worker
dandavison Jun 24, 2025
9dbe4a1
Do not issue warnings when user is not using type annotations
dandavison Jun 24, 2025
5532a14
Remove redundant validation
dandavison Jun 24, 2025
900987f
Respond to code review comments
dandavison Jun 24, 2025
70fafc1
Don't swallow exceptions when encoding failures
dandavison Jun 24, 2025
823c17e
Catch BaseException at top-level in worker
dandavison Jun 24, 2025
f617827
Fail worker on broken executor
dandavison Jun 24, 2025
679f8ba
Revert "Catch BaseException at top-level in worker"
dandavison Jun 24, 2025
9c7fee9
Cleanup
dandavison Jun 24, 2025
d35f9e4
Change context method name: .current() -> .get()
dandavison Jun 24, 2025
876a3df
Rename: TemporalNexusOperationContext
dandavison Jun 24, 2025
0541f50
Expose contextvar object directly
dandavison Jun 24, 2025
d25ba2f
Mark methods as private
dandavison Jun 24, 2025
c8a110c
Add run-time type check
dandavison Jun 24, 2025
6e3f0b5
Make start_workflow a static function
dandavison Jun 24, 2025
aeab540
Remove accidental exports
dandavison Jun 24, 2025
643f1fe
Docstrings
dandavison Jun 24, 2025
1185e30
Comment, cleanup
dandavison Jun 24, 2025
507f233
TODO
dandavison Jun 24, 2025
a766e67
TODOs
dandavison Jun 24, 2025
f060c79
Get rid of spurious type parameters
dandavison Jun 25, 2025
60a21ca
Add worker logging
dandavison Jun 25, 2025
5ffa77c
Type-level enforcement of the two ways to use WorkflowRunOperationHan…
dandavison Jun 25, 2025
fb8f22c
Respond to upstream: SyncOperation.from_callable
dandavison Jun 25, 2025
971a31f
-> WorkflowRunOperation.from_callable()
dandavison Jun 25, 2025
625bec7
TODO
dandavison Jun 25, 2025
7826bce
Parameterize workflow_run_operation tests
dandavison Jun 25, 2025
91d122b
Failing test case
dandavison Jun 25, 2025
9b3c0bf
Test: clean up imports
dandavison Jun 25, 2025
d82f36f
Respond to upstream: sync_operation_handler
dandavison Jun 25, 2025
cf5d3be
New workflow_run_operation_handler
dandavison Jun 25, 2025
7f6b8b8
Delete reference to obsolete __nexus_service_metadata__
dandavison Jun 26, 2025
021e733
TODO
dandavison Jun 26, 2025
ef0181b
Use get_callable_name utility
dandavison Jun 26, 2025
62dda51
Fix test: 'not an async def` message changed
dandavison Jun 26, 2025
50c076c
Refactor
dandavison Jun 26, 2025
c8d2992
Reorganize: temporalio.nexus.handler -> temporalo.nexus
dandavison Jun 26, 2025
6c31692
Fix signatures of start_method on workflow caller side
dandavison Jun 26, 2025
28e5ae2
`from temporalio import nexus` everywhere
dandavison Jun 26, 2025
621ddb0
Qualify client.WorkflowHandle in temporalio.nexus
dandavison Jun 26, 2025
4c99839
Fixup: no coverage for these
dandavison Jun 26, 2025
09f6837
Rename: nexus.WorkflowHandle
dandavison Jun 26, 2025
a5bb59a
nexus.WorkflowHandle.{to,from}_token()
dandavison Jun 26, 2025
82785f7
Respond to upstream: sync_operation_handler -> sync_operation
dandavison Jun 26, 2025
15f5c09
workflow_run_operation_handler -> workflow_run_operation
dandavison Jun 26, 2025
5d9ffc9
from nexus import workflow_run_operation
dandavison Jun 26, 2025
96c4b90
Respond to upstream: operation_handler is not in the public API
dandavison Jun 26, 2025
ee9c376
New nexus operation context API
dandavison Jun 26, 2025
b52fb56
Fix broken test
dandavison Jun 26, 2025
8504503
Fix another test
dandavison Jun 26, 2025
3704cc8
Move Failure tests utility
dandavison Jun 26, 2025
f3245fe
Fix test
dandavison Jun 26, 2025
0d2f915
RTU: relocate OperationError
dandavison Jun 26, 2025
36190e7
Copy get_types utility from nexusrpc
dandavison Jun 26, 2025
bc068b4
Fixup: eliminate references to WorkflowOperationToken
dandavison Jun 26, 2025
0806af4
Move start_workflow to WorkflowRunOperationContext
dandavison Jun 26, 2025
e287e9a
Move WorkflowRunOperationContext to _operation_context module
dandavison Jun 27, 2025
a56e332
Wire through additional context type in union
dandavison Jun 26, 2025
9c7bc6d
Eliminate unnecessary modeling of callable types
dandavison Jun 26, 2025
85052a8
Fix passing Nexus context headers/request ID from worker
dandavison Jun 26, 2025
7c279aa
Always passthrough nexusrpc
dandavison Jun 26, 2025
408f1b9
Revert disabling of sandbox for nexus workflow tests
dandavison Jun 26, 2025
f3966ea
Passthrough 3rd-party imports in tests helpers module
dandavison Jun 26, 2025
fd76792
Strengthen warning note
dandavison Jun 26, 2025
ad33f8f
Docstrings, comments
dandavison Jun 26, 2025
d76b08f
Type-level cleanup/evolution in workflow caller
dandavison Jun 26, 2025
1ad78bb
TODOs
dandavison Jun 27, 2025
98385fb
Move logger
dandavison Jun 27, 2025
366e00f
Separate Temporal context for each operation verb
dandavison Jun 27, 2025
df828de
Make Temporal context classes non-private
dandavison Jun 27, 2025
0637897
Use TemporalStartOperationContext instead of WorkflowRunOperationContext
dandavison Jun 27, 2025
9d3c2ab
Revert "Use TemporalStartOperationContext instead of WorkflowRunOpera…
dandavison Jun 27, 2025
184511d
Make WorkflowRunOperationContext subclass StartOperationContext
dandavison Jun 27, 2025
3d17ba7
Mark TemporalStartOperationContext as private
dandavison Jun 27, 2025
cc96faf
Handle OperationError consistently with HandlerError
dandavison Jun 27, 2025
3c94752
RTU: operation_id -> operation_token
dandavison Jun 27, 2025
201785d
Fix cancellation context bug
dandavison Jun 27, 2025
150e39d
RTU: Use nexusrpc.get_service_definition
dandavison Jun 28, 2025
7866b01
Docstring
dandavison Jun 29, 2025
aac8571
RTU get_operation_factory
dandavison Jun 29, 2025
691a18d
Workflow OperationError / HandlerError test
dandavison Jun 28, 2025
f51e9be
Convert nexus_handler_failure_info as nexusrpc.HandlerError
dandavison Jun 29, 2025
351e60e
RTU: Move HandlerError to root module
dandavison Jun 29, 2025
fa5a2c8
RTU: test is fixed by syncio.sync_operation
dandavison Jun 29, 2025
c766235
RTU: unskip test
dandavison Jun 29, 2025
f34d337
RTU: syncio tree
dandavison Jun 29, 2025
1e3cdca
Don't pass cause to HandlerError constructor
dandavison Jun 29, 2025
7ad0d0c
RTU: registration time enforcement of syncio/asyncio mistakes
dandavison Jun 30, 2025
6c84511
WIP
dandavison Jun 30, 2025
8a88b4f
RTU: Copy operation factory getter/setter from nexusrpc
dandavison Jun 30, 2025
e3d75b0
Use getters/setters
dandavison Jun 30, 2025
39abf89
Move no-type-annotations test to invalid usage test
dandavison Jun 30, 2025
9b4f714
Remove operations without type annotations
dandavison Jul 1, 2025
fb2f232
Split test
dandavison Jul 1, 2025
2b2583a
Test operations without type annotations
dandavison Jul 1, 2025
26118a6
Delete redundant test
dandavison Jul 1, 2025
cf396b7
Delete failing callable instance test
dandavison Jul 1, 2025
a7094de
Test error conversion
dandavison Jul 1, 2025
3d1979c
Translating Java assertions
dandavison Jul 1, 2025
133a395
Update test
dandavison Jul 1, 2025
789cf4d
Corrected Java output
dandavison Jul 2, 2025
1761374
Update test assertions
dandavison Jul 2, 2025
9e1bef9
Add timeout test
dandavison Jul 2, 2025
0b3ecd8
Install the Nexus SDK from GitHub
dandavison Jul 2, 2025
b920bb8
Update error tests
dandavison Jul 2, 2025
051f8e5
Edit TODOs
dandavison Jul 2, 2025
688baf8
Make a pass through prerelease TODOs
dandavison Jul 2, 2025
09ae187
Revert change to callable types
dandavison Jul 2, 2025
67a6437
Test start_workflow overloads
dandavison Jul 2, 2025
10fe421
Add additional overloads
dandavison Jul 3, 2025
07f3463
string name workflow
dandavison Jul 3, 2025
52ee7cf
Use a dataclass
dandavison Jul 3, 2025
f1a5702
More overloads
dandavison Jul 3, 2025
25d9a12
Fix mypy failures
dandavison Jul 3, 2025
5fc7d6c
Revert "Convert nexus_handler_failure_info as nexusrpc.HandlerError"
dandavison Jul 3, 2025
23a7260
Cleanup error test
dandavison Jul 3, 2025
4326d3a
Revert "Delete redundant test"
dandavison Jul 3, 2025
8c9d2f7
Evolve context API
dandavison Jul 3, 2025
172744e
Rename as temporalio.nexus.cancel_workflow
dandavison Jul 3, 2025
b508b6c
Fix test
dandavison Jul 3, 2025
ef157ad
Cleanup
dandavison Jul 3, 2025
665d9a1
Impprovements from code review comments
dandavison Jul 3, 2025
912dc23
Expose nexus.LoggerAdapter
dandavison Jul 3, 2025
426db10
Add outbound links for sync responses also
dandavison Jul 3, 2025
81b810a
Don't expose separate workflow.start_nexus_operation
dandavison Jul 3, 2025
0645434
Remove unnecessary type hint
dandavison Jul 3, 2025
9bb49c2
New Nexus client constructor
dandavison Jul 3, 2025
2713bbd
Remove unused test helper methods
dandavison Jul 3, 2025
f2dcb45
Clean up token type
dandavison Jul 3, 2025
13b7818
Refactor start timeout test
dandavison Jul 3, 2025
8f8e213
Cancellation timeout test
dandavison Jul 3, 2025
0b028b6
Create running_task for cancellation op handler
dandavison Jul 3, 2025
3aa03d8
Test creation of worker from ServiceHandler instances
dandavison Jul 3, 2025
44aa00f
Test creation of worker from programmatically-created ServiceHandler
dandavison Jul 3, 2025
b863610
Reapply "Convert nexus_handler_failure_info as nexusrpc.HandlerError"
dandavison Jul 3, 2025
6e16689
uv.lock
dandavison Jul 4, 2025
ec3554b
header -> headers
dandavison Jul 4, 2025
3f526c9
Rename utility
dandavison Jul 5, 2025
fece846
Rename callback types and methods
dandavison Jul 5, 2025
d76e859
Improvements from code review
dandavison Jul 6, 2025
2843357
Rename
dandavison Jul 6, 2025
60222c6
Bug fix: don't lose type info in nexus client calls
dandavison Jul 6, 2025
9a74493
Add test output
dandavison Jul 4, 2025
f3a296f
TEMP: raise
dandavison Jul 4, 2025
5b9021f
Make Python test more faithfully match Java test
dandavison Jul 4, 2025
36b87e4
Failure converter: copy missing OperationError field into proto
dandavison Jul 4, 2025
699275d
Don't use on-the-fly dict
dandavison Jul 4, 2025
df84d4c
Convert nexusrpc.HandlerError to failure proto directly instead of cr…
dandavison Jul 4, 2025
b51dace
DEV
dandavison Jul 5, 2025
3e0cc97
Fix serialization of nexusrpc.HandlerError as nexus Failure proto
dandavison Jul 5, 2025
c07bf53
Change HandlerError serialization
dandavison Jul 5, 2025
7facb2c
Combine implementations
dandavison Jul 5, 2025
3376420
Delete nexusrpc.OperationError -> temporalio.api.failure.v1.Failure c…
dandavison Jul 5, 2025
4bf4473
Bug fix in FailureConverter.from_failure
dandavison Jul 5, 2025
dc95ae1
Revert "Install the Nexus SDK from GitHub"
dandavison Jul 6, 2025
49b8e29
RTU: syncio module
dandavison Jul 6, 2025
df68927
Error test cases
dandavison Jul 6, 2025
627344e
Make error messages consistent
dandavison Jul 6, 2025
cf99e9a
Revert to serialize error chain as Java does
dandavison Jul 6, 2025
59470c1
Use a constant for Failure proto type
dandavison Jul 6, 2025
d19d781
DEV
dandavison Jul 6, 2025
c87038b
Add note
dandavison Jul 6, 2025
8f31739
Clean up test
dandavison Jul 6, 2025
b288dc3
Clean up test
dandavison Jul 6, 2025
05b2eb1
Use same code path to serialize OperationError and HandlerError
dandavison Jul 6, 2025
220e1b8
Tests
dandavison Jul 6, 2025
7e7c37c
Test: alter assertion to expected operation error message in first ch…
dandavison Jul 6, 2025
16d57ae
Hoist error message in the case of OperationError
dandavison Jul 6, 2025
eee2438
Revert "Test: alter assertion to expected operation error message in …
dandavison Jul 6, 2025
30a4c07
Test assertions
dandavison Jul 6, 2025
163ae07
RaiseNexusHandlerErrorNotFound
dandavison Jul 6, 2025
3f8bb02
Don't serialize retry behavior unless user set it
dandavison Jul 6, 2025
be6c623
Comment failing test assertion
dandavison Jul 6, 2025
c03fbcb
Cleanup
dandavison Jul 6, 2025
0b895e8
Remaining error tests pass
dandavison Jul 6, 2025
3627b3f
Revert "DEV"
dandavison Jul 6, 2025
a4890c7
Move error tests to separate file
dandavison Jul 6, 2025
965ce79
Fixup client types
dandavison Jul 6, 2025
1f6cfd9
Test nexus operation retries
dandavison Jul 6, 2025
f084e66
Reorganize tests
dandavison Jul 6, 2025
c6667db
Add WorkflowHandle[OutputT] to union of output types on client
dandavison Jul 7, 2025
a8fdc3f
Overloads on NexusClient
dandavison Jul 7, 2025
5a562ee
Error tests
dandavison Jul 7, 2025
29e2b2d
Make test_handler pass
dandavison Jul 7, 2025
7c77425
Cleanup
dandavison Jul 7, 2025
2e366ba
Make temporal context non-Optional on WorkflowRunOperationContext
dandavison Jul 7, 2025
6046dd9
Underscore prefix on _temporal_context
dandavison Jul 7, 2025
051c9bb
Add links to callbacks
dandavison Jul 7, 2025
0dbd94b
Throw on confict_policy USE_EXISTING
dandavison Jul 7, 2025
ef48e75
Improvements from code review
dandavison Jul 7, 2025
d42b3f6
Don't use computed property when serializing
dandavison Jul 7, 2025
3143860
Delete from pending_nexus_operations dict
dandavison Jul 7, 2025
476a1c1
Delete redundant .result() method
dandavison Jul 7, 2025
fa5af60
Call to_payload before mutating outbound proto structs
dandavison Jul 7, 2025
352e8a0
Require kwargs for both service and endpoint (punt on positional)
dandavison Jul 7, 2025
6a3071b
Reapply "Install the Nexus SDK from GitHub"
dandavison Jul 7, 2025
fe2a6de
Fake a type in a test
dandavison Jul 7, 2025
9934c5e
Get rid of post-init logic on interceptor input
dandavison Jul 7, 2025
d66df7a
Placeholder README content
dandavison Jul 7, 2025
2fabfa1
Install nexusrpc from main
dandavison Jul 7, 2025
c1f7661
Expose StartNexusOperationInput in worker module
dandavison Jul 7, 2025
90a2449
Rearrange exception handling to guarantee task removal from dict
dandavison Jul 7, 2025
d5e9f88
Do not handle cancel-before-start
dandavison Jul 7, 2025
07ca831
Fixups from self-review
dandavison Jul 7, 2025
9189a16
Cleanup
dandavison Jul 7, 2025
c967aa5
DEV: local sdk
dandavison Jul 7, 2025
43660f4
Respond to upstream: retryable_override instead of enum
dandavison Jul 7, 2025
399c1ec
Revert "DEV: local sdk"
dandavison Jul 7, 2025
12f98a1
Install nexusrpc from PyPi
dandavison Jul 7, 2025
b6e084d
Don't expose underscore-prefixed context variables
dandavison Jul 7, 2025
63c21fe
Cleanup
dandavison Jul 7, 2025
9afd28f
Nexus error chain workaround (#944)
dandavison Jul 7, 2025
3ea025d
Upgrade pyright
dandavison Jul 7, 2025
c00dbab
Add type checking test
dandavison Jul 7, 2025
12071e0
Fix type errors
dandavison Jul 7, 2025
98d561d
README
dandavison Jul 7, 2025
c073963
Format docstrings
dandavison Jul 8, 2025
d5864fe
Fix Rust lint errors
dandavison Jul 8, 2025
3783ac5
Point to Nexus sample in branch
dandavison Jul 8, 2025
f744680
Skip tests under Java test server
dandavison Jul 8, 2025
aa3aa23
Use sphinx format for a docstring
dandavison Jul 8, 2025
57eee56
Remove junk
dandavison Jul 8, 2025
eae3197
Add warning notices to Nexus APIs
dandavison Jul 8, 2025
917873c
Skip test on windows
dandavison Jul 8, 2025
02a1ca1
Skip the test on all platforms
dandavison Jul 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ informal introduction to the features and their implementation.
- [Heartbeating and Cancellation](#heartbeating-and-cancellation)
- [Worker Shutdown](#worker-shutdown)
- [Testing](#testing-1)
- [Nexus](#nexus)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see this section

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bump, we should add this section showing how to make simple Nexus Python operation and how to call Nexus operations from workflows.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add README docs shortly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nexus README content exists now

- [Workflow Replay](#workflow-replay)
- [Observability](#observability)
- [Metrics](#metrics)
Expand Down Expand Up @@ -1308,6 +1309,113 @@ affect calls activity code might make to functions on the `temporalio.activity`
* `cancel()` can be invoked to simulate a cancellation of the activity
* `worker_shutdown()` can be invoked to simulate a worker shutdown during execution of the activity


### Nexus

⚠️ **Nexus support is currently at an experimental release stage. Backwards-incompatible changes are anticipated until a stable release is announced.** ⚠️

[Nexus](https://github.com/nexus-rpc/) is a synchronous RPC protocol. Arbitrary duration operations that can respond
asynchronously are modeled on top of a set of pre-defined synchronous RPCs.

Temporal supports calling Nexus operations **from a workflow**. See https://docs.temporal.io/nexus. There is no support
currently for calling a Nexus operation from non-workflow code.

To get started quickly using Nexus with Temporal, see the Python Nexus sample:
https://github.com/temporalio/samples-python/tree/nexus/hello_nexus.


Two types of Nexus operation are supported, each using a decorator:

- `@temporalio.nexus.workflow_run_operation`: a Nexus operation that is backed by a Temporal workflow. The operation
handler you write will start the handler workflow and then respond with a token indicating that the handler workflow
is in progress. When the handler workflow completes, Temporal server will automatically deliver the result (success or
failure) to the caller workflow.
- `@nexusrpc.handler.sync_operation`: an operation that responds synchronously. It may be `def` or `async def` and it
may do network I/O, but it must respond within 10 seconds.

The following steps are an overview of the [Python Nexus sample](
https://github.com/temporalio/samples-python/tree/nexus/hello_nexus).

1. Create the caller and handler namespaces, and the Nexus endpoint. For example,
```
temporal operator namespace create --namespace my-handler-namespace
temporal operator namespace create --namespace my-caller-namespace

temporal operator nexus endpoint create \
--name my-nexus-endpoint \
--target-namespace my-handler-namespace \
--target-task-queue my-handler-task-queue
```

2. Define your service contract. This specifies the names and input/output types of your operations. You will use this
to refer to the operations when calling them from a workflow.
```python
@nexusrpc.service
class MyNexusService:
my_sync_operation: nexusrpc.Operation[MyInput, MyOutput]
my_workflow_run_operation: nexusrpc.Operation[MyInput, MyOutput]
```

3. Implement your operation handlers in a service handler:
```python
@service_handler(service=MyNexusService)
class MyNexusServiceHandler:
@sync_operation
async def my_sync_operation(
self, ctx: StartOperationContext, input: MyInput
) -> MyOutput:
return MyOutput(message=f"Hello {input.name} from sync operation!")

@workflow_run_operation
async def my_workflow_run_operation(
self, ctx: WorkflowRunOperationContext, input: MyInput
) -> nexus.WorkflowHandle[MyOutput]:
return await ctx.start_workflow(
WorkflowStartedByNexusOperation.run,
input,
id=str(uuid.uuid4()),
)
```

4. Register your service handler with a Temporal worker.
```python
client = await Client.connect("localhost:7233", namespace="my-handler-namespace")
worker = Worker(
client,
task_queue="my-handler-task-queue",
workflows=[WorkflowStartedByNexusOperation],
nexus_service_handlers=[MyNexusServiceHandler()],
)
await worker.run()
```

5. Call your Nexus operations from your caller workflow.
```python
@workflow.defn
class CallerWorkflow:
def __init__(self):
self.nexus_client = workflow.create_nexus_client(
service=MyNexusService, endpoint="my-nexus-endpoint"
)

@workflow.run
async def run(self, name: str) -> tuple[MyOutput, MyOutput]:
# Start the Nexus operation and wait for the result in one go, using execute_operation.
wf_result = await self.nexus_client.execute_operation(
MyNexusService.my_workflow_run_operation,
MyInput(name),
)
# Or alternatively, obtain the operation handle using start_operation,
# and then use it to get the result:
sync_operation_handle = await self.nexus_client.start_operation(
MyNexusService.my_sync_operation,
MyInput(name),
)
sync_result = await sync_operation_handle
return sync_result, wf_result
```


### Workflow Replay

Given a workflow's history, it can be replayed locally to check for things like non-determinism errors. For example,
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ keywords = [
"workflow",
]
dependencies = [
"nexus-rpc>=1.1.0",
"protobuf>=3.20,<6",
"python-dateutil>=2.8.2,<3 ; python_version < '3.11'",
"types-protobuf>=3.20",
Expand Down Expand Up @@ -44,7 +45,7 @@ dev = [
"psutil>=5.9.3,<6",
"pydocstyle>=6.3.0,<7",
"pydoctor>=24.11.1,<25",
"pyright==1.1.377",
"pyright==1.1.402",
"pytest~=7.4",
"pytest-asyncio>=0.21,<0.22",
"pytest-timeout~=2.2",
Expand All @@ -53,6 +54,8 @@ dev = [
"twine>=4.0.1,<5",
"ruff>=0.5.0,<0.6",
"maturin>=1.8.2",
"pytest-cov>=6.1.1",
"httpx>=0.28.1",
"pytest-pretty>=1.3.0",
]

Expand Down Expand Up @@ -162,6 +165,7 @@ exclude = [
"tests/worker/workflow_sandbox/testmodules/proto",
"temporalio/bridge/worker.py",
"temporalio/contrib/opentelemetry.py",
"temporalio/contrib/pydantic.py",
"temporalio/converter.py",
"temporalio/testing/_workflow.py",
"temporalio/worker/_activity.py",
Expand All @@ -173,6 +177,10 @@ exclude = [
"tests/api/test_grpc_stub.py",
"tests/conftest.py",
"tests/contrib/test_opentelemetry.py",
"tests/contrib/pydantic/models.py",
"tests/contrib/pydantic/models_2.py",
"tests/contrib/pydantic/test_pydantic.py",
"tests/contrib/pydantic/workflows.py",
"tests/test_converter.py",
"tests/test_service.py",
"tests/test_workflow.py",
Expand Down
32 changes: 31 additions & 1 deletion temporalio/bridge/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use temporal_sdk_core_api::worker::{
};
use temporal_sdk_core_api::Worker;
use temporal_sdk_core_protos::coresdk::workflow_completion::WorkflowActivationCompletion;
use temporal_sdk_core_protos::coresdk::{ActivityHeartbeat, ActivityTaskCompletion};
use temporal_sdk_core_protos::coresdk::{ActivityHeartbeat, ActivityTaskCompletion, nexus::NexusTaskCompletion};
use temporal_sdk_core_protos::temporal::api::history::v1::History;
use tokio::sync::mpsc::{channel, Sender};
use tokio_stream::wrappers::ReceiverStream;
Expand Down Expand Up @@ -60,6 +60,7 @@ pub struct WorkerConfig {
graceful_shutdown_period_millis: u64,
nondeterminism_as_workflow_fail: bool,
nondeterminism_as_workflow_fail_for_types: HashSet<String>,
nexus_task_poller_behavior: PollerBehavior,
}

#[derive(FromPyObject)]
Expand Down Expand Up @@ -565,6 +566,18 @@ impl WorkerRef {
})
}

fn poll_nexus_task<'p>(&self, py: Python<'p>) -> PyResult<Bound<'p, PyAny>> {
let worker = self.worker.as_ref().unwrap().clone();
self.runtime.future_into_py(py, async move {
let bytes = match worker.poll_nexus_task().await {
Ok(task) => task.encode_to_vec(),
Err(PollError::ShutDown) => return Err(PollShutdownError::new_err(())),
Err(err) => return Err(PyRuntimeError::new_err(format!("Poll failure: {err}"))),
};
Ok(bytes)
})
}

fn complete_workflow_activation<'p>(
&self,
py: Python<'p>,
Expand Down Expand Up @@ -599,6 +612,22 @@ impl WorkerRef {
})
}

fn complete_nexus_task<'p>(&self,
py: Python<'p>,
proto: &Bound<'_, PyBytes>,
) -> PyResult<Bound<'p, PyAny>> {
let worker = self.worker.as_ref().unwrap().clone();
let completion = NexusTaskCompletion::decode(proto.as_bytes())
.map_err(|err| PyValueError::new_err(format!("Invalid proto: {err}")))?;
self.runtime.future_into_py(py, async move {
worker
.complete_nexus_task(completion)
.await
.context("Completion failure")
.map_err(Into::into)
})
}

fn record_activity_heartbeat(&self, proto: &Bound<'_, PyBytes>) -> PyResult<()> {
enter_sync!(self.runtime);
let heartbeat = ActivityHeartbeat::decode(proto.as_bytes())
Expand Down Expand Up @@ -696,6 +725,7 @@ fn convert_worker_config(
})
.collect::<HashMap<String, HashSet<WorkflowErrorType>>>(),
)
.nexus_task_poller_behavior(conf.nexus_task_poller_behavior)
.build()
.map_err(|err| PyValueError::new_err(format!("Invalid worker config: {err}")))
}
Expand Down
18 changes: 17 additions & 1 deletion temporalio/bridge/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import temporalio.bridge.client
import temporalio.bridge.proto
import temporalio.bridge.proto.activity_task
import temporalio.bridge.proto.nexus
import temporalio.bridge.proto.workflow_activation
import temporalio.bridge.proto.workflow_completion
import temporalio.bridge.runtime
Expand All @@ -35,7 +36,7 @@
from temporalio.bridge.temporal_sdk_bridge import (
CustomSlotSupplier as BridgeCustomSlotSupplier,
)
from temporalio.bridge.temporal_sdk_bridge import PollShutdownError
from temporalio.bridge.temporal_sdk_bridge import PollShutdownError # type: ignore


@dataclass
Expand All @@ -60,6 +61,7 @@ class WorkerConfig:
graceful_shutdown_period_millis: int
nondeterminism_as_workflow_fail: bool
nondeterminism_as_workflow_fail_for_types: Set[str]
nexus_task_poller_behavior: PollerBehavior


@dataclass
Expand Down Expand Up @@ -216,6 +218,14 @@ async def poll_activity_task(
await self._ref.poll_activity_task()
)

async def poll_nexus_task(
self,
) -> temporalio.bridge.proto.nexus.NexusTask:
"""Poll for a nexus task."""
return temporalio.bridge.proto.nexus.NexusTask.FromString(
await self._ref.poll_nexus_task()
)

async def complete_workflow_activation(
self,
comp: temporalio.bridge.proto.workflow_completion.WorkflowActivationCompletion,
Expand All @@ -229,6 +239,12 @@ async def complete_activity_task(
"""Complete an activity task."""
await self._ref.complete_activity_task(comp.SerializeToString())

async def complete_nexus_task(
self, comp: temporalio.bridge.proto.nexus.NexusTaskCompletion
) -> None:
"""Complete a nexus task."""
await self._ref.complete_nexus_task(comp.SerializeToString())

def record_activity_heartbeat(
self, comp: temporalio.bridge.proto.ActivityHeartbeat
) -> None:
Expand Down
61 changes: 58 additions & 3 deletions temporalio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,9 +464,17 @@ async def start_workflow(
rpc_metadata: Mapping[str, str] = {},
rpc_timeout: Optional[timedelta] = None,
request_eager_start: bool = False,
stack_level: int = 2,
priority: temporalio.common.Priority = temporalio.common.Priority.default,
versioning_override: Optional[temporalio.common.VersioningOverride] = None,
# The following options should not be considered part of the public API. They
# are deliberately not exposed in overloads, and are not subject to any
# backwards compatibility guarantees.
callbacks: Sequence[Callback] = [],
workflow_event_links: Sequence[
temporalio.api.common.v1.Link.WorkflowEvent
] = [],
request_id: Optional[str] = None,
stack_level: int = 2,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are also missing the conflict options which are required for USE_EXISTING to make sense for workflow callers. Can be done in a followup PR since this was functionality that was added as a second step for Go and Java.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would for now throw if a user tries to set USE_EXISTING in the workflow run operation handler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done:

        if (
            id_conflict_policy
            == temporalio.common.WorkflowIDConflictPolicy.USE_EXISTING
        ):
            raise RuntimeError(
                "WorkflowIDConflictPolicy.USE_EXISTING is not yet supported when starting a workflow "
                "that backs a Nexus operation (Python SDK Nexus support is at Pre-release stage)."
            )

) -> WorkflowHandle[Any, Any]:
"""Start a workflow and return its handle.

Expand Down Expand Up @@ -529,7 +537,6 @@ async def start_workflow(
name, result_type_from_type_hint = (
temporalio.workflow._Definition.get_name_and_result_type(workflow)
)

return await self._impl.start_workflow(
StartWorkflowInput(
workflow=name,
Expand Down Expand Up @@ -557,6 +564,9 @@ async def start_workflow(
rpc_timeout=rpc_timeout,
request_eager_start=request_eager_start,
priority=priority,
callbacks=callbacks,
workflow_event_links=workflow_event_links,
request_id=request_id,
)
)

Expand Down Expand Up @@ -5193,6 +5203,10 @@ class StartWorkflowInput:
rpc_timeout: Optional[timedelta]
request_eager_start: bool
priority: temporalio.common.Priority
# The following options are experimental and unstable.
callbacks: Sequence[Callback]
workflow_event_links: Sequence[temporalio.api.common.v1.Link.WorkflowEvent]
request_id: Optional[str]
versioning_override: Optional[temporalio.common.VersioningOverride] = None


Expand Down Expand Up @@ -5807,8 +5821,30 @@ async def _build_start_workflow_execution_request(
self, input: StartWorkflowInput
) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest:
req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest()
req.request_eager_execution = input.request_eager_start
await self._populate_start_workflow_execution_request(req, input)
# _populate_start_workflow_execution_request is used for both StartWorkflowInput
# and UpdateWithStartStartWorkflowInput. UpdateWithStartStartWorkflowInput does
# not have the following two fields so they are handled here.
req.request_eager_execution = input.request_eager_start
if input.request_id:
req.request_id = input.request_id

links = [
temporalio.api.common.v1.Link(workflow_event=link)
for link in input.workflow_event_links
]
req.completion_callbacks.extend(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put the links on the callbacks so they can be associated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

temporalio.api.common.v1.Callback(
nexus=temporalio.api.common.v1.Callback.Nexus(
url=callback.url,
header=callback.headers,
),
links=links,
)
for callback in input.callbacks
)
# Links are duplicated on request for compatibility with older server versions.
req.links.extend(links)
return req

async def _build_signal_with_start_workflow_execution_request(
Expand Down Expand Up @@ -7231,6 +7267,25 @@ def api_key(self, value: Optional[str]) -> None:
self.service_client.update_api_key(value)


@dataclass(frozen=True)
class NexusCallback:
"""Nexus callback to attach to events such as workflow completion.

.. warning::
This API is experimental and unstable.
"""

url: str
"""Callback URL."""

headers: Mapping[str, str]
"""Header to attach to callback request."""


# Intended to become a union of callback types
Callback = NexusCallback
Comment on lines +7285 to +7286
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we wait until there is need for a union? It's easy to build a union at that time, but for now, a single-value alias seems unnecessary API surface. (pedantic and unimportant, non-blocking)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes the types clearer in start_workflow, i.e. that they're not nexus-specific. Believe @bergundy wanted it this way but yes totally open to changing it later if we don't want this.



async def _encode_user_metadata(
converter: temporalio.converter.DataConverter,
summary: Optional[Union[str, temporalio.api.common.v1.Payload]],
Expand Down
Loading
Loading