Skip to content

Adding links to Nexus signals#1593

Open
Evanthx wants to merge 9 commits into
mainfrom
signal-links
Open

Adding links to Nexus signals#1593
Evanthx wants to merge 9 commits into
mainfrom
signal-links

Conversation

@Evanthx

@Evanthx Evanthx commented Jun 11, 2026

Copy link
Copy Markdown

This adds links to Nexus signals - the receiving workflow has a link to the workflow sending the signal. and the sending workflow has a link to the receiving workflow.

@Evanthx Evanthx requested a review from a team as a code owner June 11, 2026 17:18
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment thread temporalio/nexus/_operation_context.py Outdated
Comment thread temporalio/nexus/_operation_context.py Outdated
Comment thread temporalio/nexus/_operation_context.py Outdated
Comment thread temporalio/client/_impl.py Outdated
Comment on lines +203 to +221
# If this signal-with-start is issued from inside a Nexus operation handler (but not as the
# nexus-backing workflow, whose links are handled separately by
# WorkflowRunOperationContext.start_workflow), capture the signal backlink the server
# returned so the caller workflow's Nexus history event links to the signaled event. A
# plain start does not capture a backlink: it only forwards the inbound links onto the
# start request.
nexus_ctx = self._try_nexus_start_operation_context()
if (
nexus_ctx is not None
and not temporalio.nexus._operation_context._in_nexus_backing_workflow_start_context()
and isinstance(
resp,
temporalio.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse,
)
):
# Server >= 1.31 with EnableCHASMSignalBacklinks returns signal_link pointing at
# the WorkflowExecutionSignaled event; older servers leave it unset.
if resp.HasField("signal_link"):
nexus_ctx._add_backlink(resp.signal_link)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if nexus_ctx._add_outbound_links() should be renamed to reflect that it's from workflows (e.g. nexus_ctx._add_workflow_backlinks() or something similar) and updated to handle this and be called here? It would also require removing the call in _start_nexus_backing_workflow

Seems like it might be more correct to just attach any back links to the nexus context if it exists, regardless of if the target workflow will complete the nexus operation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Renamed it to _add_backing_workflow_response_link. _add_backing_workflow_response_link has to construct a link if it's not set, but _add_response_link doesn't - so I'm not sure about combining them though.

Comment thread temporalio/client/_impl.py Outdated
Comment on lines +259 to +270
@@ -241,6 +260,14 @@ async def _build_start_workflow_execution_request(
req.on_conflict_options.attach_request_id = True
req.on_conflict_options.attach_completion_callbacks = True
req.on_conflict_options.attach_links = True
else:
# If this is a plain start_workflow issued from inside a Nexus operation handler
# (not the nexus-backing workflow, which already carries inbound links via
# input.links), forward the inbound Nexus task links so the started callee's
# WorkflowExecutionStarted event links back to the caller.
nexus_ctx = self._try_nexus_start_operation_context()
if nexus_ctx is not None:
req.links.extend(nexus_ctx._get_outgoing_request_links())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like this change overall, but wonder if the flags are not set correctly. In the backing_workflow_start_context case, we definitely want the callbacks attacked.

In any Nexus case, I think we want the request ID and the links attached.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

If I understand this right - the plain start_workflow runs under the user's own conflict policy, so those flags should already be set. The backing worflow start doesn't use the user's confict policy, so those have to be set. So I think this is correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Users supply the conflict policy in both cases. These flags indicate behavior for when that policy is WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING and a conflict is detected.

What separates a 'backing' workflow from a 'plain' workflow is that the backing workflow has callbacks associated that will complete the nexus operation with the result of the workflow.

In this new path, we want to attach the links and request ID. Since the TemporalNexusClient and WorkflowRunOperationContext should be in charge of correctly setting the callbacks, it's safe to set all the flags to true when this is invoked from an operation handler.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed and addressed!

Comment thread tests/nexus/test_signal_link_propagation.py Outdated
Comment thread tests/nexus/test_signal_link_propagation_e2e.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add tests that exercise this via standalone nexus operations.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So this did actually uncover a problem - _link_conversion.py was out of date. That's why I had trouble with this test.

Standalone Nexus operations (invoked via client.create_nexus_client(...)) receive a NexusOperation-variant inbound link in the server's canonical format …/nexus-operations/{op_id}/{run_id}/details. The SDK's parser only matched an older …/nexus-operations/{op_id}?runID=… form, so it silently dropped the link — no forward link propagated to the signaled callee.

Updated the regex/parser/formatter in _link_conversion.py to the canonical /{run_id}/details format so the link parses and the forward link now lands on the callee.

Comment thread tests/nexus/test_signal_link_propagation_e2e.py Outdated
@service_handler(service=SignalingService)
class SignalingServiceHandler:
@sync_operation
async def op(self, _ctx: StartOperationContext, input: str) -> str:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use a dataclass rather than a string split by ':' for input

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

@Evanthx Evanthx requested a review from VegetarianOrc June 16, 2026 03:41
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.

3 participants