From 915b4ce840b3ae36669b3fab6d87863ad3d09a6b Mon Sep 17 00:00:00 2001 From: GareArc Date: Thu, 29 Jan 2026 17:06:29 -0800 Subject: [PATCH] feat: Add parent trace context propagation for workflow-as-tool hierarchy Enables distributed tracing for nested workflows across all trace providers (Langfuse, LangSmith, community providers). When a workflow invokes another workflow via workflow-as-tool, the child workflow now includes parent context attributes that allow trace systems to reconstruct the full execution tree. Changes: - Add parent_trace_context field to WorkflowTool - Set parent context in tool node when invoking workflow-as-tool - Extract and pass parent context through app generator This is a community enhancement (ungated) that improves distributed tracing for all users. Parent context includes: trace_id, node_execution_id, workflow_run_id, and app_id. --- api/core/app/apps/workflow/app_generator.py | 5 ++++- api/core/tools/workflow_as_tool/tool.py | 7 ++++++- api/core/workflow/nodes/tool/tool_node.py | 11 +++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index 0165c74295..f21f40bbc5 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -141,9 +141,12 @@ class WorkflowAppGenerator(BaseAppGenerator): inputs: Mapping[str, Any] = args["inputs"] - extras = { + extras: dict[str, Any] = { **extract_external_trace_id_from_args(args), } + parent_trace_context = args.get("_parent_trace_context") + if parent_trace_context: + extras["parent_trace_context"] = parent_trace_context workflow_run_id = str(uuid.uuid4()) # FIXME (Yeuoly): we need to remove the SKIP_PREPARE_USER_INPUTS_KEY from the args # trigger shouldn't prepare user inputs diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 33b15b3438..2a8cf0c280 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -51,6 +51,7 @@ class WorkflowTool(Tool): self.workflow_call_depth = workflow_call_depth self.label = label self._latest_usage = LLMUsage.empty_usage() + self.parent_trace_context: dict[str, str] | None = None super().__init__(entity=entity, runtime=runtime) @@ -91,11 +92,15 @@ class WorkflowTool(Tool): self._latest_usage = LLMUsage.empty_usage() + args: dict[str, Any] = {"inputs": tool_parameters, "files": files} + if self.parent_trace_context: + args["_parent_trace_context"] = self.parent_trace_context + result = generator.generate( app_model=app, workflow=workflow, user=user, - args={"inputs": tool_parameters, "files": files}, + args=args, invoke_from=self.runtime.invoke_from, streaming=False, call_depth=self.workflow_call_depth + 1, diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 2e7ec757b4..2a6b59437c 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -105,6 +105,17 @@ class ToolNode(Node[ToolNodeData]): # get conversation id conversation_id = self.graph_runtime_state.variable_pool.get(["sys", SystemVariableKey.CONVERSATION_ID]) + from core.tools.workflow_as_tool.tool import WorkflowTool + + if isinstance(tool_runtime, WorkflowTool): + workflow_run_id_var = self.graph_runtime_state.variable_pool.get(["sys", SystemVariableKey.WORKFLOW_RUN_ID]) + tool_runtime.parent_trace_context = { + "trace_id": str(workflow_run_id_var.text) if workflow_run_id_var else "", + "parent_node_execution_id": self.execution_id, + "parent_workflow_run_id": str(workflow_run_id_var.text) if workflow_run_id_var else "", + "parent_app_id": self.app_id, + } + try: message_stream = ToolEngine.generic_invoke( tool=tool_runtime,