Skip to content

NEXUS-485: Support Workflow Update as a Nexus Operation#2945

Draft
mavemuri wants to merge 3 commits into
mainfrom
updates-nexus-op
Draft

NEXUS-485: Support Workflow Update as a Nexus Operation#2945
mavemuri wants to merge 3 commits into
mainfrom
updates-nexus-op

Conversation

@mavemuri

@mavemuri mavemuri commented Jul 10, 2026

Copy link
Copy Markdown

What was changed

Adds support for Workflow Update as a Nexus operation

Why?

Part of effort to expose all Temporal primitives via Nexus operations

Checklist

  1. Closes NEXUS-485

  2. How was this tested:

  • added integration tests
  1. Any docs updates needed?
  • TBD

@mavemuri mavemuri force-pushed the updates-nexus-op branch 2 times, most recently from 7852a29 to 93ee1a9 Compare July 10, 2026 03:36
if (commonLink.hasNexusOperation()) {
return nexusOperationToNexusLink(commonLink.getNexusOperation());
}
// check for workflow link only if a more specific variant is not set

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.

Nit: I don't think this comment makes sense, we are just checking variants here and there can only be one

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.

fixed - misread it as hasWorkflowID initially

import org.junit.ClassRule;
import org.junit.Test;

public class UpdateWorkflowOperationTest extends BaseNexusTest {

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.

Shouldn't need to inherit from BaseNexusTest, we should probably mark it as deprecated or remove it. (not saying do that in this PR)

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.

Any reason for removing it? looks useful as a utility

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.

All the utility is already in the SDK rule

effectiveOptsBuilder.setUpdateId(requestId);
}
if (options.getWaitForStage() == null) {
effectiveOptsBuilder.setWaitForStage(WorkflowUpdateStage.ACCEPTED);

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.

Oh WaitForStage should be explicitly set, that is the clients behaviour so we should try to match that here

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.

Ah ok wanted to see if we could make it easier with a default callout - stricter makes sense, will change

if (input.getUpdateId() != null) {
optionsBuilder.setUpdateId(input.getUpdateId());
}
return client.startWorkflowUpdate(

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.

The Nexus client wasn't intended to take the stubs. The proposed API was to take the unbound method reference like all new Java SDK APIs like

  public <T, A1, R> TemporalOperationResult<R> updateWorkflow(
      String workflowId,
      Class<T> workflowClass,
       unctions.Func2<T, A1, R>> updateMethod,
      A1 arg);

We should discuss if the proposed API does not work

// only set for updates
private final String updateId;

public OperationToken(

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.

Could consider splitting the OperationToken into different Java types for the different t. Should be possible using jackson. Although I would be fine defering this until we have activities here as well and then doing the refactor then

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.

Thought about it with a base class that does common encode/decodes but left it as overloads give hints and the remaining is mostly similar
But agree on maybe changing after activities- if changing here, will track in other SDKs too for consistency

* cancel.
*/
@Experimental
public final class CancelUpdateWorkflowExecutionInput {

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.

Suggested change
public final class CancelUpdateWorkflowExecutionInput {
public final class CancelUpdateWorkflowInput {

I thought we would call it CancelUpdateWorkflowInput ,open to other ideas though, regardless we should make sure we are consistent across all the SDKs and docs here.

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.

makes sense, fixed


/** Returns the run ID extracted from the operation token, or null if not present. */
@Nullable
public String getRunId() {

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 think for CancelUpdateWorkflowInput this is always non null

@mavemuri mavemuri Jul 11, 2026

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.

There are 2 cases -

  1. Right now, we are generating it based on the input - if user provides no runID, there wont be one in the token
    Not sure if we need to grab it from a handle either - intent of user could be that they want to just run an update on current workflow run - but for cancel its not clear if the cancel should also "preserve" that original intent - i thought it should
    eg. run update- without specifying runID- while workflow is on runID1, get token. Now, when workflow is on runID2, still want to cancel the update => the current token will work
    OTOH, if they specifically had a runID, then it wont work but that is stricter and is fully consistent with what they need
    This is consistent across the go/python/java SDK PRs
    But we can discuss if this preserving original intent means getting runID via handle if empty- or if thats decided already
  2. user just wants to cancel it, crafts the encoded token themselves without runID to cancel current one. Required for above case as well if token was initially minted with a specific runID

changed it to empty string default but i think its still same functionally

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.

We should not be generating based on the input, we should generate based on the output of the update response. Updates do not move between runs so please use the run ID of the update.

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 also make sure you fix this in Go as well this needs to encode where the workflow update actually is so this is blocker for the Go SDK as well

if (nexusOperationMetadata.operationCompleted) {
try {
R value = handle.getResult();
return TemporalOperationResult.sync(value);

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.

This vranch doesn't call asyncOperationStarted.set(false) - does it need to? It looks like it is left set to true.

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.

The asyncOperationStarted is more of a guard against multiple invocations using the same ephemeral nexusClient. Whether it completed sync or async is the result and isnt tied to how the req was made - because in either case, request itself was always an asyncReq
Only case where it resets to false is if the op can be retried safely- due to real issues like op failing due to invalid params etc

private <R> void checkNexusUpdateOptionsValid(UpdateOptions<R> options) {
if (options.getWaitForStage() != WorkflowUpdateStage.ACCEPTED) {
throw new HandlerException(
HandlerException.ErrorType.BAD_REQUEST,

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.

Go returns this as a nexus.OperationError - should this be consistent?

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.

agree, fixed

URLEncoder.encode(w.getWorkflowId(), StandardCharsets.UTF_8.toString())
.replace("+", "%20"); // handle workflowIds supporting spaces
String runId = URLEncoder.encode(w.getRunId(), StandardCharsets.UTF_8.toString());
String url = String.format(linkPathFormat, namespace, workflowId, runId);

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.

String.format can throw IllegalFormatException which is uncaught - should you just catch Exception instead of limiting it?

.setWorkflowId(workflowID)
.setRunId(runID));
} catch (Exception e) {
log.error("Failed to parse Nexus link URL", e);

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.

Might still want to log the bad URL, like you did above

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