Skip to content

Add Temporal Operation Handler#1503

Open
VegetarianOrc wants to merge 12 commits into
mainfrom
amazzeo/temporal-nexus-operation-handler
Open

Add Temporal Operation Handler#1503
VegetarianOrc wants to merge 12 commits into
mainfrom
amazzeo/temporal-nexus-operation-handler

Conversation

@VegetarianOrc
Copy link
Copy Markdown
Contributor

What was changed

Added support for Temporal-backed Nexus operation handlers.

This introduces @temporalio.nexus.temporal_operation, TemporalNexusClient, and TemporalOperationResult, allowing Nexus operation handlers to either return a synchronous result or start a Temporal workflow as the async backing operation. The branch also adds token parsing support for generic operation tokens, cancellation support for Temporal-backed workflow operations, Nexus workflow client overloads for the new operation shape, and focused runtime/type coverage.

Why?

This gives Nexus operation handlers a first-class way to interact with Temporal while preserving Nexus async operation semantics, workflow linking/callback behavior, cancellation handling, and typed workflow-start ergonomics.

Checklist

  1. How was this tested:

    • Existing tests
    • Added type tests
    • Added tests for temporal operation
  2. Any docs updates needed?

Yes. The Nexus docs should cover @temporalio.nexus.temporal_operation, TemporalNexusClient.start_workflow,
TemporalOperationResult.sync, and async workflow-backed operation behavior.

@VegetarianOrc VegetarianOrc force-pushed the amazzeo/temporal-nexus-operation-handler branch from 26c52b8 to b0dd232 Compare May 11, 2026 17:29
@VegetarianOrc VegetarianOrc marked this pull request as ready for review May 12, 2026 21:44
@VegetarianOrc VegetarianOrc requested a review from a team as a code owner May 12, 2026 21:44
Copy link
Copy Markdown

@Evanthx Evanthx left a comment

Choose a reason for hiding this comment

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

Found a few nitpicks!

_input: None,
) -> nexus.TemporalOperationResult[None]:
return await client.start_workflow(
BlockingWorkflow.run, id=f"blocking-{uuid.uuid4}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should there be parenthesis after the uuid4? Same for two cases just below, lines 78 and 81.


match operation_token.type:
case OperationTokenType.WORKFLOW:
await self.cancel_workflow_run(ctx, operation_token.workflow_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If this doesn't match, should we raise an error or something to let the user know? I'm not sure of the desired behavior if that's the case.

self._started_async = True
try:
yield
except:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should this be except Exception?

namespace = token_details.get("ns")
if not isinstance(namespace, str) or not namespace:
raise TypeError(
f"invalid token: expected namespace to be a string or null, got {type(namespace)}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The error message says null is OK, but null isn't a string so will fail the if statement, won't it?

class OperationTokenType(IntEnum):
"""Type discriminator for Nexus operation tokens."""

WORKFLOW = 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should there be an UNSPECIFIED of 0? The other int enums have it - it looks like default from protobuf is zero so if it isn't specified for some reason it will be set to zero


def __init__(self) -> None:
"""Initialize the client wrapper from the active Nexus operation context."""
self._temporal_context = _TemporalStartOperationContext.get()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should you make sure the get returned a valid context?

def _reserve_async_start(self) -> Iterator[None]:
if self._started_async:
raise HandlerError(
"Only one async operation can be started per operation handler invocation. Use TemporalNexusClient.client for additional workflow interactions",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you can only call one async per operation, should this check be in the operation? Otherwise I can just get the client and call multiple async operations, can't I?

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.

2 participants