From a6805ab7a92e14d2729d81144d78524d498ec66d Mon Sep 17 00:00:00 2001 From: "microsoft-agent-framework-for-go[bot]" <324824725+microsoft-agent-framework-for-go[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2026 05:28:46 +0000 Subject: [PATCH 1/3] workflow/inproc: add subworkflow output forwarding gates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- workflow/inproc/subworkflow.go | 46 ++++++++++++++++++++++++----- workflow/inproc/subworkflow_test.go | 43 +++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/workflow/inproc/subworkflow.go b/workflow/inproc/subworkflow.go index 6a4ade9e..5c706173 100644 --- a/workflow/inproc/subworkflow.go +++ b/workflow/inproc/subworkflow.go @@ -23,9 +23,31 @@ const ( subworkflowImplementationID = "workflow/subworkflow.Binding" ) +// SubworkflowConfig configures how a hosted subworkflow forwards its outputs. +type SubworkflowConfig struct { + // AutoSendMessageHandlerResultObject controls whether each output of the + // subworkflow is sent as a message to the host's downstream executors. + // Nil means true. + AutoSendMessageHandlerResultObject *bool + + // AutoYieldOutputHandlerResultObject controls whether each output of the + // subworkflow is also yielded as an output of the parent workflow. When + // false, the host does not declare the subworkflow's output types as yielded. + // Nil means true. + AutoYieldOutputHandlerResultObject *bool +} + // BindSubworkflowAsExecutor returns an [workflow.ExecutorBinding] that hosts wf -// as a subworkflow with the supplied workflow-unique executor ID. +// as a subworkflow with the supplied workflow-unique executor ID. Subworkflow +// outputs are both sent to downstream executors and yielded as parent outputs. func BindSubworkflowAsExecutor(wf *workflow.Workflow, id string) workflow.ExecutorBinding { + return BindSubworkflowAsExecutorWithConfig(wf, id, SubworkflowConfig{}) +} + +// BindSubworkflowAsExecutorWithConfig is like [BindSubworkflowAsExecutor] but +// lets the caller choose whether subworkflow outputs are sent as messages +// and/or yielded as outputs of the parent workflow. +func BindSubworkflowAsExecutorWithConfig(wf *workflow.Workflow, id string, cfg SubworkflowConfig) workflow.ExecutorBinding { if wf == nil { panic("subworkflow: cannot bind nil Workflow as executor") } @@ -42,12 +64,12 @@ func BindSubworkflowAsExecutor(wf *workflow.Workflow, id string) workflow.Execut RawValue: wf, SupportsConcurrentSharedExecution: true, NewExecutorFunc: func(sessionID string) (*workflow.Executor, error) { - return newSubworkflowExecutor(id, wf, sessionID, ownershipToken) + return newSubworkflowExecutor(id, wf, sessionID, ownershipToken, cfg) }, } } -func newSubworkflowExecutor(id string, wf *workflow.Workflow, sessionID string, ownershipToken any) (*workflow.Executor, error) { +func newSubworkflowExecutor(id string, wf *workflow.Workflow, sessionID string, ownershipToken any, cfg SubworkflowConfig) (*workflow.Executor, error) { protocol, err := wf.DescribeProtocol() if err != nil { return nil, err @@ -58,6 +80,8 @@ func newSubworkflowExecutor(id string, wf *workflow.Workflow, sessionID string, sessionID: sessionID, ownershipToken: ownershipToken, protocol: protocol, + autoSend: cfg.AutoSendMessageHandlerResultObject == nil || *cfg.AutoSendMessageHandlerResultObject, + autoYield: cfg.AutoYieldOutputHandlerResultObject == nil || *cfg.AutoYieldOutputHandlerResultObject, } return host.executor(), nil } @@ -68,6 +92,8 @@ type subworkflowHostExecutor struct { sessionID string ownershipToken any protocol workflow.ProtocolDescriptor + autoSend bool + autoYield bool joinContext *runnerContext runner *runner @@ -198,7 +224,9 @@ func (h *subworkflowHostExecutor) ensureRunSendMessage(ctx *workflow.Context, in func (h *subworkflowHostExecutor) configureProtocol(pb *workflow.ProtocolBuilder) (*workflow.ProtocolBuilder, error) { pb.SendsMessageType(h.protocol.Yields...) - pb.YieldsOutputType(h.protocol.Yields...) + if h.autoYield { + pb.YieldsOutputType(h.protocol.Yields...) + } pb.RouteBuilder.AddCatchAll(h.queueExternalMessage) return pb, nil } @@ -277,10 +305,14 @@ func (h *subworkflowHostExecutor) forwardWorkflowEvent(ctx *workflow.Context, ev return nil } if h.joinContext != nil { - if err := h.joinContext.sendMessage(ctx, h.id, "", event.Output); err != nil { - return err + if h.autoSend { + if err := h.joinContext.sendMessage(ctx, h.id, "", event.Output); err != nil { + return err + } + } + if h.autoYield { + return h.yieldOutput(ctx, event.Output) } - return h.yieldOutput(ctx, event.Output) } return nil case workflow.RequestHaltEvent: diff --git a/workflow/inproc/subworkflow_test.go b/workflow/inproc/subworkflow_test.go index 6c28d496..f3e3ef63 100644 --- a/workflow/inproc/subworkflow_test.go +++ b/workflow/inproc/subworkflow_test.go @@ -61,6 +61,49 @@ func TestSubworkflowBinding_ForwardsOutputsAsParentOutputsAndMessages(t *testing } } +func TestSubworkflowBindingWithConfig_OutputForwardingGates(t *testing.T) { + tests := []struct { + name string + cfg inproc.SubworkflowConfig + wantSink int + wantYield int + }{ + {"default", inproc.SubworkflowConfig{}, 1, 1}, + {"no send", inproc.SubworkflowConfig{AutoSendMessageHandlerResultObject: new(false)}, 0, 1}, + {"no yield", inproc.SubworkflowConfig{AutoYieldOutputHandlerResultObject: new(false)}, 1, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + childStart := workflow.NewExecutor("child-start", func(in textMessage) dataMessage { + return dataMessage{Bytes: []byte(in.Text)} + }).Bind() + child, err := workflow.NewBuilder(childStart).WithOutputFrom(childStart).Build() + if err != nil { + t.Fatalf("Build child: %v", err) + } + + host := inproc.BindSubworkflowAsExecutorWithConfig(child, "child", tt.cfg) + sinkCount := 0 + sink := workflow.NewExecutor("sink", func(dataMessage) { sinkCount++ }).Bind() + parent, err := workflow.NewBuilder(host).AddEdge(host, sink).WithOutputFrom(host).Build() + if err != nil { + t.Fatalf("Build parent: %v", err) + } + + run, err := inproc.Lockstep.Run(t.Context(), parent, textMessage{Text: "abc"}) + if err != nil { + t.Fatalf("Run: %v", err) + } + if sinkCount != tt.wantSink { + t.Fatalf("sink messages = %d, want %d", sinkCount, tt.wantSink) + } + if got := len(outputEvents(slicesCollect(run.OutgoingEvents()))); got != tt.wantYield { + t.Fatalf("output count = %d, want %d", got, tt.wantYield) + } + }) + } +} + func TestSubworkflowBinding_ForwardsWorkflowWarningAsWorkflowWarning(t *testing.T) { childStart := workflow.NewExecutor("child-start", func(ctx *workflow.Context, _ textMessage) error { return ctx.AddEvent(workflow.WorkflowWarningEvent{Message: "child warning"}) From 0cc4f90d3796c0b1162eecad79f223924f9ac0af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Nov 2026 14:47:26 +0000 Subject: [PATCH 2/3] Break subworkflow binding API for config Co-authored-by: qmuntal <3747816+qmuntal@users.noreply.github.com> --- _catalog/dotnet-go-sdk-symbol-mapping.json | 8 ++--- .../01-start-here/05_subworkflow/main.go | 2 +- .../nested_order_processing/main.go | 6 ++-- .../subworkflows/request_interception/main.go | 2 +- workflow/inproc/checkpoint_test.go | 2 +- workflow/inproc/sample_resource_test.go | 2 +- workflow/inproc/sample_test.go | 4 +-- workflow/inproc/subworkflow.go | 14 +++------ workflow/inproc/subworkflow_test.go | 30 +++++++++---------- 9 files changed, 32 insertions(+), 38 deletions(-) diff --git a/_catalog/dotnet-go-sdk-symbol-mapping.json b/_catalog/dotnet-go-sdk-symbol-mapping.json index e5e8d0e6..e36893cb 100644 --- a/_catalog/dotnet-go-sdk-symbol-mapping.json +++ b/_catalog/dotnet-go-sdk-symbol-mapping.json @@ -20071,7 +20071,7 @@ "note": "Go binds the port and accepts wrapped requests, but has no allowWrappedRequests=false setting." }, "BindAsExecutor(Workflow, string, ExecutorOptions) -> ExecutorBinding": { - "go": "inproc.BindSubworkflowAsExecutor(wf, id)", + "go": "inproc.BindSubworkflowAsExecutor(wf, id, inproc.SubworkflowConfig{})", "go_symbols": [ "workflow/inproc.BindSubworkflowAsExecutor" ], @@ -20310,7 +20310,7 @@ "note": "BindNewExecutorFunc covers the explicit-ID factory alias; its callback takes session ID before executor ID." }, "ConfigureSubWorkflow(Workflow, string, ExecutorOptions) -> ExecutorBinding": { - "go": "inproc.BindSubworkflowAsExecutor(wf, id)", + "go": "inproc.BindSubworkflowAsExecutor(wf, id, inproc.SubworkflowConfig{})", "go_symbols": [ "workflow/inproc.BindSubworkflowAsExecutor" ], @@ -24002,7 +24002,7 @@ "area": "workflows", "assembly": "Microsoft.Agents.AI.Workflows", "mapping": { - "go": "inproc.BindSubworkflowAsExecutor(wf, id)", + "go": "inproc.BindSubworkflowAsExecutor(wf, id, inproc.SubworkflowConfig{})", "go_symbols": [ "workflow/inproc.BindSubworkflowAsExecutor", "workflow.ExecutorBinding" @@ -24115,7 +24115,7 @@ }, "constructors": { "SubworkflowBinding(Workflow, string, ExecutorOptions)": { - "go": "inproc.BindSubworkflowAsExecutor(wf, id)", + "go": "inproc.BindSubworkflowAsExecutor(wf, id, inproc.SubworkflowConfig{})", "go_symbols": [ "workflow/inproc.BindSubworkflowAsExecutor" ], diff --git a/examples/03-workflows/01-start-here/05_subworkflow/main.go b/examples/03-workflows/01-start-here/05_subworkflow/main.go index 335bfdb4..e9a6556c 100644 --- a/examples/03-workflows/01-start-here/05_subworkflow/main.go +++ b/examples/03-workflows/01-start-here/05_subworkflow/main.go @@ -44,7 +44,7 @@ func main() { prefix := textTransformExecutor("PrefixExecutor", func(input string) string { return "INPUT: " + input }) - textProcessingExecutor := inproc.BindSubworkflowAsExecutor(textProcessing, "TextProcessingSubWorkflow") + textProcessingExecutor := inproc.BindSubworkflowAsExecutor(textProcessing, "TextProcessingSubWorkflow", inproc.SubworkflowConfig{}) postProcess := textTransformExecutor("PostProcessExecutor", func(input string) string { return "[FINAL] " + input + " [END]" }) diff --git a/examples/03-workflows/subworkflows/nested_order_processing/main.go b/examples/03-workflows/subworkflows/nested_order_processing/main.go index fdc5783b..f0848e47 100644 --- a/examples/03-workflows/subworkflows/nested_order_processing/main.go +++ b/examples/03-workflows/subworkflows/nested_order_processing/main.go @@ -112,7 +112,7 @@ func buildPaymentWorkflow(fraudCheckWorkflow *workflow.Workflow) *workflow.Workf demo.Assistantf(" [Payment/ValidatePayment] Validated payment for %s ($%.2f)", order.OrderID, float64(order.Amount)/100) return order }).Bind() - fraudCheck := inproc.BindSubworkflowAsExecutor(fraudCheckWorkflow, "FraudCheck") + fraudCheck := inproc.BindSubworkflowAsExecutor(fraudCheckWorkflow, "FraudCheck", inproc.SubworkflowConfig{}) chargePayment := workflow.NewExecutor("ChargePayment", func(order OrderInfo) OrderInfo { order.PaymentTransactionID = "TXN-ORD-001" demo.Assistantf(" [Payment/ChargePayment] Payment processed: %s", order.PaymentTransactionID) @@ -153,8 +153,8 @@ func buildOrderProcessingWorkflow(paymentWorkflow *workflow.Workflow, shippingWo demo.Assistantf("[OrderReceived] Processing order %q", orderID) return OrderInfo{OrderID: orderID, Amount: 9999} }).Bind() - payment := inproc.BindSubworkflowAsExecutor(paymentWorkflow, "Payment") - shipping := inproc.BindSubworkflowAsExecutor(shippingWorkflow, "Shipping") + payment := inproc.BindSubworkflowAsExecutor(paymentWorkflow, "Payment", inproc.SubworkflowConfig{}) + shipping := inproc.BindSubworkflowAsExecutor(shippingWorkflow, "Shipping", inproc.SubworkflowConfig{}) orderCompleted := workflow.NewExecutor("OrderCompleted", func(order OrderInfo) string { demo.Assistantf("[OrderCompleted] Payment: %s", order.PaymentTransactionID) demo.Assistantf("[OrderCompleted] Shipping: %s - %s", order.Carrier, order.TrackingNumber) diff --git a/examples/03-workflows/subworkflows/request_interception/main.go b/examples/03-workflows/subworkflows/request_interception/main.go index 21c04e14..e8b97c6e 100644 --- a/examples/03-workflows/subworkflows/request_interception/main.go +++ b/examples/03-workflows/subworkflows/request_interception/main.go @@ -95,7 +95,7 @@ func buildWorkflow() *workflow.Workflow { child := buildChild(approvePort) const hostID = "ApprovalSubworkflow" - host := inproc.BindSubworkflowAsExecutor(child, hostID) + host := inproc.BindSubworkflowAsExecutor(child, hostID, inproc.SubworkflowConfig{}) // A nested request port is surfaced to the parent under a qualified ID of the // form ".", so the escalation port must match it. diff --git a/workflow/inproc/checkpoint_test.go b/workflow/inproc/checkpoint_test.go index 73f7cea0..d287ed71 100644 --- a/workflow/inproc/checkpoint_test.go +++ b/workflow/inproc/checkpoint_test.go @@ -317,7 +317,7 @@ func TestCheckpointResume_StreamingPreservesNestedFanInMessages(t *testing.T) { for _, environment := range checkpointTestEnvironments() { t.Run(environment.name, func(t *testing.T) { buildOuter := func() *workflow.Workflow { - host := inproc.BindSubworkflowAsExecutor(checkpointBarrierWorkflow(t, "InnerApproval", "InnerSink", false), "InnerSubworkflow") + host := inproc.BindSubworkflowAsExecutor(checkpointBarrierWorkflow(t, "InnerApproval", "InnerSink", false), "InnerSubworkflow", inproc.SubworkflowConfig{}) port := workflow.RequestPort{ID: "ForwardedInnerApproval", Request: reflect.TypeFor[checkpointApprovalRequest](), Response: reflect.TypeFor[checkpointApprovalReply]()} wf, err := workflow.NewBuilder(host).AddEdge(host, port.Bind(), workflow.WithEdgeCondition(externalRequestOnly)). AddEdge(port.Bind(), host, workflow.WithEdgeCondition(externalResponseOnly)).Build() diff --git a/workflow/inproc/sample_resource_test.go b/workflow/inproc/sample_resource_test.go index af55a1a4..954d37cc 100644 --- a/workflow/inproc/sample_resource_test.go +++ b/workflow/inproc/sample_resource_test.go @@ -202,7 +202,7 @@ func sampleResourceWorkflow(t *testing.T) *workflow.Workflow { }) return pb, nil }) - host := inproc.BindSubworkflowAsExecutor(sampleResourceChild(t), "ResourceWorkflow") + host := inproc.BindSubworkflowAsExecutor(sampleResourceChild(t), "ResourceWorkflow", inproc.SubworkflowConfig{}) cache, policy := sampleResourceGate("ResourceCache", false), sampleResourceGate("QuotaPolicyEngine", true) resourcePort := workflow.RequestPort{ID: "ResourceRequest", Request: reflect.TypeFor[sampleResourceRequest](), Response: reflect.TypeFor[sampleResourceResponse]()} policyPort := workflow.RequestPort{ID: "PolicyCheckRequest", Request: reflect.TypeFor[samplePolicyRequest](), Response: reflect.TypeFor[samplePolicyResponse]()} diff --git a/workflow/inproc/sample_test.go b/workflow/inproc/sample_test.go index d8611804..2d3d1990 100644 --- a/workflow/inproc/sample_test.go +++ b/workflow/inproc/sample_test.go @@ -556,7 +556,7 @@ func sampleTextProcessingWorkflow(t *testing.T) *workflow.Workflow { if err != nil { t.Fatal(err) } - textProcessor := inproc.BindSubworkflowAsExecutor(child, "TextProcessor") + textProcessor := inproc.BindSubworkflowAsExecutor(child, "TextProcessor", inproc.SubworkflowConfig{}) orchestrator := workflow.BindNewExecutorFunc("TextProcessingOrchestrator", func(_ string, id string) (*workflow.Executor, error) { cache := &workflow.StatefulExecutorCache[*sampleTextState]{StateKey: "State", InitialStateFactory: func() *sampleTextState { return &sampleTextState{Pending: map[string]bool{}} }} return &workflow.Executor{ @@ -734,7 +734,7 @@ func sampleEchoSubworkflow(t *testing.T) *workflow.Workflow { if err != nil { t.Fatal(err) } - host := inproc.BindSubworkflowAsExecutor(child, "EchoSubworkflow") + host := inproc.BindSubworkflowAsExecutor(child, "EchoSubworkflow", inproc.SubworkflowConfig{}) parent, err := workflow.NewBuilder(host).WithOutputFrom(host).Build() if err != nil { t.Fatal(err) diff --git a/workflow/inproc/subworkflow.go b/workflow/inproc/subworkflow.go index 5c706173..b808a655 100644 --- a/workflow/inproc/subworkflow.go +++ b/workflow/inproc/subworkflow.go @@ -38,16 +38,10 @@ type SubworkflowConfig struct { } // BindSubworkflowAsExecutor returns an [workflow.ExecutorBinding] that hosts wf -// as a subworkflow with the supplied workflow-unique executor ID. Subworkflow -// outputs are both sent to downstream executors and yielded as parent outputs. -func BindSubworkflowAsExecutor(wf *workflow.Workflow, id string) workflow.ExecutorBinding { - return BindSubworkflowAsExecutorWithConfig(wf, id, SubworkflowConfig{}) -} - -// BindSubworkflowAsExecutorWithConfig is like [BindSubworkflowAsExecutor] but -// lets the caller choose whether subworkflow outputs are sent as messages -// and/or yielded as outputs of the parent workflow. -func BindSubworkflowAsExecutorWithConfig(wf *workflow.Workflow, id string, cfg SubworkflowConfig) workflow.ExecutorBinding { +// as a subworkflow with the supplied workflow-unique executor ID. cfg controls +// whether outputs are sent to downstream executors and/or yielded as parent +// outputs; nil config fields default to true. +func BindSubworkflowAsExecutor(wf *workflow.Workflow, id string, cfg SubworkflowConfig) workflow.ExecutorBinding { if wf == nil { panic("subworkflow: cannot bind nil Workflow as executor") } diff --git a/workflow/inproc/subworkflow_test.go b/workflow/inproc/subworkflow_test.go index f3e3ef63..e798820d 100644 --- a/workflow/inproc/subworkflow_test.go +++ b/workflow/inproc/subworkflow_test.go @@ -27,7 +27,7 @@ func TestSubworkflowBinding_ForwardsOutputsAsParentOutputsAndMessages(t *testing t.Fatalf("Build child: %v", err) } - host := inproc.BindSubworkflowAsExecutor(child, "child") + host := inproc.BindSubworkflowAsExecutor(child, "child", inproc.SubworkflowConfig{}) var gotAtSink []dataMessage sink := workflow.NewExecutor("sink", func(msg dataMessage) { gotAtSink = append(gotAtSink, msg) @@ -82,7 +82,7 @@ func TestSubworkflowBindingWithConfig_OutputForwardingGates(t *testing.T) { t.Fatalf("Build child: %v", err) } - host := inproc.BindSubworkflowAsExecutorWithConfig(child, "child", tt.cfg) + host := inproc.BindSubworkflowAsExecutor(child, "child", tt.cfg) sinkCount := 0 sink := workflow.NewExecutor("sink", func(dataMessage) { sinkCount++ }).Bind() parent, err := workflow.NewBuilder(host).AddEdge(host, sink).WithOutputFrom(host).Build() @@ -113,7 +113,7 @@ func TestSubworkflowBinding_ForwardsWorkflowWarningAsWorkflowWarning(t *testing. t.Fatalf("Build child: %v", err) } - host := inproc.BindSubworkflowAsExecutor(child, "child") + host := inproc.BindSubworkflowAsExecutor(child, "child", inproc.SubworkflowConfig{}) parent, err := workflow.NewBuilder(host).Build() if err != nil { t.Fatalf("Build parent: %v", err) @@ -164,7 +164,7 @@ func TestSubworkflowBinding_UsesActiveRunnerInputTypes(t *testing.T) { t.Fatalf("Build child: %v", err) } - host := inproc.BindSubworkflowAsExecutor(child, "child") + host := inproc.BindSubworkflowAsExecutor(child, "child", inproc.SubworkflowConfig{}) parent, err := workflow.NewBuilder(host). WithOutputFrom(host). Build() @@ -220,7 +220,7 @@ func TestSubworkflowBinding_QualifiedRequestPortRoundTrip(t *testing.T) { t.Fatalf("Build child: %v", err) } - host := inproc.BindSubworkflowAsExecutor(child, "child") + host := inproc.BindSubworkflowAsExecutor(child, "child", inproc.SubworkflowConfig{}) qualifiedPort := workflow.RequestPort{ ID: "child.ask", Request: port.Request, @@ -268,7 +268,7 @@ func TestSubworkflowBinding_RejectsUnqualifiedResponseBeforeChildRun(t *testing. if err != nil { t.Fatal(err) } - host := inproc.BindSubworkflowAsExecutor(child, "child") + host := inproc.BindSubworkflowAsExecutor(child, "child", inproc.SubworkflowConfig{}) response := &workflow.ExternalResponse{ PortInfo: workflow.RequestPortInfo{PortID: "other.port"}, RequestID: "request", @@ -354,7 +354,7 @@ func TestSubworkflowBinding_ParentInterceptsChildRequestLocally(t *testing.T) { } const hostID = "approver" - host := inproc.BindSubworkflowAsExecutor(child, hostID) + host := inproc.BindSubworkflowAsExecutor(child, hostID, inproc.SubworkflowConfig{}) escalationPort := workflow.RequestPort{ ID: hostID + "." + approvePort.ID, Request: approvePort.Request, @@ -511,7 +511,7 @@ func TestSubworkflowBinding_CheckpointedEchoSampleCanResumeTwice(t *testing.T) { t.Fatalf("Build subworkflow: %v", err) } - host := inproc.BindSubworkflowAsExecutor(sub, "EchoSubworkflow") + host := inproc.BindSubworkflowAsExecutor(sub, "EchoSubworkflow", inproc.SubworkflowConfig{}) parent, err := workflow.NewBuilder(host). WithOutputFrom(host). Build() @@ -555,7 +555,7 @@ func TestSubworkflowBinding_TextProcessingSample(t *testing.T) { t.Fatalf("Build subworkflow: %v", err) } - host := inproc.BindSubworkflowAsExecutor(sub, "TextProcessingSubWorkflow") + host := inproc.BindSubworkflowAsExecutor(sub, "TextProcessingSubWorkflow", inproc.SubworkflowConfig{}) prefix := stringTransformBinding("prefix", func(s string) string { return "INPUT: " + s }) post := stringTransformBinding("post", func(s string) string { return "[FINAL] " + s + " [END]" }) parent, err := workflow.NewBuilder(prefix). @@ -594,7 +594,7 @@ func TestSubworkflowBinding_NestedOrderProcessingSample(t *testing.T) { } validatePayment := orderTransformBinding("ValidatePayment", func(order sampleOrderInfo) sampleOrderInfo { return order }) - fraudCheckHost := inproc.BindSubworkflowAsExecutor(fraudCheck, "FraudCheck") + fraudCheckHost := inproc.BindSubworkflowAsExecutor(fraudCheck, "FraudCheck", inproc.SubworkflowConfig{}) chargePayment := orderTransformBinding("ChargePayment", func(order sampleOrderInfo) sampleOrderInfo { order.PaymentTransactionID = "TXN-ORD-001" return order @@ -622,8 +622,8 @@ func TestSubworkflowBinding_NestedOrderProcessingSample(t *testing.T) { } orderReceived := orderReceivedBinding("OrderReceived") - paymentHost := inproc.BindSubworkflowAsExecutor(payment, "Payment") - shippingHost := inproc.BindSubworkflowAsExecutor(shipping, "Shipping") + paymentHost := inproc.BindSubworkflowAsExecutor(payment, "Payment", inproc.SubworkflowConfig{}) + shippingHost := inproc.BindSubworkflowAsExecutor(shipping, "Shipping", inproc.SubworkflowConfig{}) orderCompleted := orderCompletedBinding("OrderCompleted") parent, err := workflow.NewBuilder(orderReceived). AddEdge(orderReceived, paymentHost). @@ -689,7 +689,7 @@ func TestSubworkflowBinding_SharedStateWorksWithinSubworkflow(t *testing.T) { t.Fatalf("Build subworkflow: %v", err) } - host := inproc.BindSubworkflowAsExecutor(sub, "internalStateSubworkflow") + host := inproc.BindSubworkflowAsExecutor(sub, "internalStateSubworkflow", inproc.SubworkflowConfig{}) parent, err := workflow.NewBuilder(host). WithOutputFrom(host). Build() @@ -743,7 +743,7 @@ func TestSubworkflowBinding_SharedStateIsIsolatedAcrossSubworkflowBoundary(t *te } textRead := textReadBinding("text-read") - host := inproc.BindSubworkflowAsExecutor(sub, "textTrimSubworkflow") + host := inproc.BindSubworkflowAsExecutor(sub, "textTrimSubworkflow", inproc.SubworkflowConfig{}) charCount := charCountBinding("char-count") parent, err := workflow.NewBuilder(textRead). AddEdge(textRead, host). @@ -836,7 +836,7 @@ func createCheckpointedSubworkflowRequestWorkflow(t *testing.T) *workflow.Workfl t.Fatalf("Build child request workflow: %v", err) } - host := inproc.BindSubworkflowAsExecutor(child, "Subworkflow") + host := inproc.BindSubworkflowAsExecutor(child, "Subworkflow", inproc.SubworkflowConfig{}) qualifiedPort := workflow.RequestPort{ ID: "ForwardedSubworkflowRequest", Request: innerPort.Request, From 92be21d653a1c85ef445d1d6ee4a0e7f553c4c76 Mon Sep 17 00:00:00 2001 From: Quim Muntal Date: Mon, 2 Nov 2026 21:43:28 +0100 Subject: [PATCH 3/3] Enhance subworkflow tests with DescribeProtocol Add protocol description check for parent in tests Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- workflow/inproc/subworkflow_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/workflow/inproc/subworkflow_test.go b/workflow/inproc/subworkflow_test.go index e798820d..b8571fe4 100644 --- a/workflow/inproc/subworkflow_test.go +++ b/workflow/inproc/subworkflow_test.go @@ -89,6 +89,13 @@ func TestSubworkflowBindingWithConfig_OutputForwardingGates(t *testing.T) { if err != nil { t.Fatalf("Build parent: %v", err) } + descriptor, err := parent.DescribeProtocol() + if err != nil { + t.Fatalf("DescribeProtocol: %v", err) + } + if got := containsType(descriptor.Yields, reflect.TypeFor[dataMessage]()); got != (tt.wantYield != 0) { + t.Fatalf("protocol declares dataMessage output = %v, want %v", got, tt.wantYield != 0) + } run, err := inproc.Lockstep.Run(t.Context(), parent, textMessage{Text: "abc"}) if err != nil {