Skip to content

Implement GetWorkflowExecutionResult#10173

Open
long-nt-tran wants to merge 11 commits into
temporalio:mainfrom
long-nt-tran:get-workflow-result
Open

Implement GetWorkflowExecutionResult#10173
long-nt-tran wants to merge 11 commits into
temporalio:mainfrom
long-nt-tran:get-workflow-result

Conversation

@long-nt-tran
Copy link
Copy Markdown
Contributor

@long-nt-tran long-nt-tran commented May 4, 2026

What changed?

Server PR for API counterpart temporalio/api#778

This API allows users to get the result of a workflow execution and optionally attach callbacks to be fired when the workflow completes (if the workflow is running). It follows continue-as-new chain up to GetWorkflowExecutionResultMaxCANChainDepth iterations (for now defined to be 100 in dynamic config).

A lot of boilerplate from making a new API (proto + mock generated lines). Business logic to note are in:

  • Frontend handler: service/frontend/workflow_handler.go
  • History handler: service/history/handler.go
  • Actual API Invoke(...) implementation: service/history/api/getworkflowexecutionresult/api.go

Why?

Allows Nexus handler to map to this operation more ergonomically for callers.

How did you test it?

  • built
  • run locally and tested manually
  • covered by existing tests
  • added new unit test(s)
  • added new functional test(s)

Potential risks

New API -- need more test across repos (load testing, etc...). Gated behind dynamic config.

@long-nt-tran long-nt-tran force-pushed the get-workflow-result branch 10 times, most recently from 71e932b to 9ec54e6 Compare May 6, 2026 21:40
@long-nt-tran long-nt-tran force-pushed the get-workflow-result branch from 9ec54e6 to 807f96b Compare May 7, 2026 22:19
@long-nt-tran long-nt-tran marked this pull request as ready for review May 7, 2026 22:43
@long-nt-tran long-nt-tran requested review from a team as code owners May 7, 2026 22:43
Comment thread tests/nexus_test_base.go
Comment on lines +47 to +48
req.Service = cmp.Or(req.Service, "test-service")
req.Operation = cmp.Or(req.Operation, "test-operation")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

opportunistic refactor from nexus_standalone_test.go, but maybe slightly overboard with still keeping these defaults here :) LMK if folks prefer to not have defaults at all

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.

works for me 👍

Comment on lines +426 to +434
leafErr := pollResp.GetFailure()
for leafErr.GetCause() != nil {
leafErr = leafErr.GetCause()
}
protorequire.ProtoEqual(s.T(),
&failurepb.Failure{Message: intentionalFailureMsg},
leafErr,
protorequire.IgnoreFields("source", "stack_trace", "encoded_attributes", "application_failure_info"),
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this seemed slightly clunky to me but I wasn't sure there was another way to unwrap and assert on this base error, LMK if there is

Comment thread service/history/api/getworkflowexecutionresult/api.go
Comment thread common/api/metadata.go Outdated

// Walk the CAN chain until we reach the head. At head, we will return the result of the head's workflow run,
// and optionally attach callbacks if hasCallbacks.
for range maxCANChainDepth {
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 was what I did in my prototype, but I question if this is the right approach to have an arb. limit here. I wonder if we could be smarter but it would require a larger effort. I was thinking if we only support targeting the latest run ID then we don't need to tackle this problem yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

commenting for posterity per offline discussion: will always get result for latest (empty) run id for now to make this simpler

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.

should we remove the code if it's effectively dead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this code is already deleted

Comment thread tests/nexus_test_base.go
Comment on lines +47 to +48
req.Service = cmp.Or(req.Service, "test-service")
req.Operation = cmp.Or(req.Operation, "test-operation")
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.

works for me 👍

Comment thread service/history/handler.go Outdated
Comment on lines +932 to +934
if namespaceID == "" {
return nil, h.convertError(errNamespaceNotSet)
}
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 is redundant; ValidateNamespaceUUID already contains that check.

Comment thread tests/links_test.go Outdated
Comment on lines +172 to +183
history := env.SdkClient().GetWorkflowHistory(env.Context(), run.GetID(), "", false, enumspb.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
foundEvent := false
for history.HasNext() {
event, err := history.Next()
s.NoError(err)
if event.EventType != enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_OPTIONS_UPDATED {
continue
}
foundEvent = true
protorequire.ProtoSliceEqual(s.T(), links, event.Links)
}
s.True(foundEvent)
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 use HistoryRequire's RequireHistoryEvent

Comment thread service/frontend/workflow_handler.go Outdated
if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil {
return nil, err
}

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.

WDYT about adding this?

if req.GetExecution().GetRunId() != "" {
    return nil, serviceerror.NewInvalidArgument("RunId is not supported")
}

// Write path: there are completion callbacks or links, and this workflow execution is running:
// 1. Attach callbacks and links via AddWorkflowExecutionOptionsUpdatedEvent
// 2. Set response for caller with backlinks
// 3. Return with Noop=false so the attachments get persisted.
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.

create comments in this closure!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I assumed you meant Return with Noop=false so the attachments get persisted. right above where it is

Comment thread tests/nexus_get_workflow_result_test.go Outdated
testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true),
testcore.WithDynamicConfig(nexusoperation.Enabled, true),
)
}
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.

inline if only used once?

Comment thread tests/nexus_get_workflow_result_test.go Outdated
}
}

func newGetResultNexusEnv(t *testing.T, opts ...testcore.TestOption) *NexusTestEnv {
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 other suites often follow the pattern of having a newTestEnv method

Comment thread tests/nexus_get_workflow_result_test.go Outdated
// runViaCallerWF runs the scenario via a caller workflow that uses
// workflow.NewNexusClient to invoke the operation. The caller workflow itself is
// opaque about what target it's querying; that's wired into the handler.
func runViaCallerWF(s *NexusGetWorkflowResultTestSuite, spec *targetWFSpec) {
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 can be a method, too (no?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was running tests via a runner so had this as a helper so i pass down the right s. Elevated this and runViaStandalone(...) to be suite test functions, copying pattern from temporal/tests/task_queue_stats_test.go

Comment thread tests/nexus_get_workflow_result_test.go Outdated
// Each test runs both WorkflowWrapped and Standalone subtests.
// =============================================================================

func (s *NexusGetWorkflowResultTestSuite) TestGetResult() {
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.

check out temporal/tests/task_queue_stats_test.go to do this a little better IMO by leveraging suites fully

)
}
return resp, nil
}
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 some of these code paths aren't tested; might be good to do (unit test is likely easier than func test)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added service/history/api/getworkflowexecutionresult/api_test.go

@long-nt-tran long-nt-tran requested a review from stephanos May 15, 2026 15:13
if err := validateRequestId(&request.RequestId, wh.config.MaxIDLengthLimit()); err != nil {
return nil, err
}
if request.GetExecution().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.

Why not support getting the result of a specific run id?

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.

Because if the workflow run continued as new we would have to follow continue as new errors to get the result

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 we want to support the ability to follow continue as new errors. Also there's no guarantee that they want the result for the most recent run of a workflow. The user might not know if the run that they want was the most recent instance of a workflow.

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.

4 participants