n8n - ✅(Solved) Fix OpenTelemetry sub-workflow executions not traced (missing ModulesHooksRegistry.addHooks in getLifecycleHooksForSubExecutions) [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
n8n-io/n8n#28592Fetched 2026-04-17 08:54:44
View on GitHub
Comments
1
Participants
2
Timeline
9
Reactions
0
Author
Timeline (top)
mentioned ×3subscribed ×3commented ×1cross-referenced ×1

Root Cause

Root cause: In packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts, the factory getLifecycleHooksForSubExecutions is the only lifecycle-hooks factory that does not call Container.get(ModulesHooksRegistry).addHooks(hooks). The other three (getLifecycleHooksForRegularMain, getLifecycleHooksForScalingMain, getLifecycleHooksForScalingWorker) all register module hooks. Because sub-executions bypass the registry, the OTEL module's @OnLifecycleEvent handlers never fire for them. This affects any backend module that participates in the lifecycle, not just OTEL.

Fix Action

Fixed

PR fix notes

PR #28593: fix(core): Trace sub-workflow executions within their parent workflow trace

Description (problem / solution / changelog)

Summary

Sub-workflow executions (workflows invoked via the Execute Workflow node) were not being traced by the OpenTelemetry module. The parent workflow's trace showed only the parent's spans, and the sub-workflow's workflow.execute / node.execute spans never appeared. This affected any backend module that relies on @OnLifecycleEvent to observe the lifecycle of a sub-execution; OpenTelemetry was the most visible symptom.

Root cause. getLifecycleHooksForSubExecutions was the only lifecycle-hooks factory that did not call Container.get(ModulesHooksRegistry).addHooks(hooks). The other three factories (Main, ScalingMain, ScalingWorker) all register module hooks, so sub-executions silently bypassed every module-registered @OnLifecycleEvent handler.

Secondary gap. WorkflowExecuteBeforeContext did not carry parentExecution, so even with module hooks registered, the OTEL handler had no way to link the sub-workflow's workflow.execute span to its parent's trace.

Changes

Commit 1 — fix(core): Register module lifecycle hooks for sub-workflow executions

  • Add Container.get(ModulesHooksRegistry).addHooks(hooks) to getLifecycleHooksForSubExecutions, matching the other three factories.
  • Plumb parentExecution through ExecutionLifecycleHooks (new optional constructor parameter) and through WorkflowExecuteBeforeContext so modules can observe the parent of a sub-execution.
  • Unit test: a module-registered workflowExecuteBefore handler now fires for sub-executions and sees parentExecution in the context.

Commit 2 — fix(core): Link sub-workflow traces to parent workflow execution span

  • WorkflowStartHandler uses ctx.parentExecution (when present) to look up the parent's workflow.execute span in SpanRegistry and start the child workflow.execute span with the parent as its OTEL context. Falls back to a root span when no parent is supplied or the parent span is not in the registry.
  • Unit tests for WorkflowStartHandler covering attributes, root-span fallback, child-of-parent linking, and missing-parent fallback.

Resulting trace

Before: two disconnected root traces — one for the parent, one for the sub-workflow.

After: one trace with the sub-workflow's workflow.execute span nested under the parent's workflow.execute span, sharing a single traceId:

workflow.execute  (Parent)
├── node.execute  Trigger
├── node.execute  Set
├── node.execute  Execute Workflow
└── workflow.execute  (Child)
    ├── node.execute  Set
    └── node.execute  Code

Known limitation

In scaling mode where parent and child executions run on different worker processes, the parent's workflow.execute span isn't present in the child process's SpanRegistry. In that case the child is still emitted as a root span — no worse than today, but a follow-up could propagate OTEL trace context through the execution queue if cross-process linking is desired. Happy to file a separate issue for that if reviewers agree.

How to test

  1. Set N8N_OTEL_ENABLED=true and N8N_OTEL_EXPORTER_OTLP_ENDPOINT=http://<collector>:4318 on an n8n instance.
  2. Create two workflows: a "Child" with any pair of nodes, and a "Parent" that ends with an Execute Workflow node calling Child.
  3. Execute Parent and inspect the trace in your OTLP backend. The Child's workflow.execute span should share the Parent's traceId and its parentSpanId should match the Parent's workflow.execute span id.

Related Linear tickets, Github issues, and Community forum posts

Fixes #28592

https://linear.app/n8n/issue/GHC-7752

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)

Changed files

  • packages/@n8n/decorators/src/execution-lifecycle/lifecycle-metadata.ts (modified, +7/-0)
  • packages/cli/src/execution-lifecycle/__tests__/execution-lifecycle-hooks.test.ts (modified, +53/-0)
  • packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts (modified, +9/-1)
  • packages/cli/src/modules/otel/__tests__/workflow-start.handler.test.ts (added, +148/-0)
  • packages/cli/src/modules/otel/handlers/workflow-start.handler.ts (modified, +21/-8)
  • packages/core/src/execution-engine/execution-lifecycle-hooks.ts (modified, +2/-0)

Code Example

workflow.execute  (Parent)
├── node.execute  Trigger
├── node.execute  Set
└── node.execute  Execute Workflow
    └── workflow.execute  (Child)
        ├── node.execute  Set
        └── node.execute  Code
RAW_BUFFERClick to expand / collapse

Bug Description

OpenTelemetry tracing does not trace sub-workflows invoked via the Execute Workflow node. When a parent workflow calls a sub-workflow, the parent trace shows a single node.execute span for the Execute Workflow node, but none of the sub-workflow's workflow.execute or node.execute spans are emitted. This makes it impossible to see the full execution graph of any workflow that uses sub-workflows.

Root cause: In packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts, the factory getLifecycleHooksForSubExecutions is the only lifecycle-hooks factory that does not call Container.get(ModulesHooksRegistry).addHooks(hooks). The other three (getLifecycleHooksForRegularMain, getLifecycleHooksForScalingMain, getLifecycleHooksForScalingWorker) all register module hooks. Because sub-executions bypass the registry, the OTEL module's @OnLifecycleEvent handlers never fire for them. This affects any backend module that participates in the lifecycle, not just OTEL.

Secondary gap: WorkflowExecuteBeforeContext does not carry parentExecution, so even if module hooks were registered, there is no way to link the sub-workflow's workflow.execute span to the parent's Execute Workflow node.execute span to produce a single nested trace.

Verified on the [email protected] tag and still present on master. No existing issue or PR addresses this.

cc @geemanjs @alielkhateeb, who authored the OTEL module in #27528 and #27789.

To Reproduce

  1. Enable native OTEL on an n8n instance:
    • N8N_OTEL_ENABLED=true
    • N8N_OTEL_EXPORTER_OTLP_ENDPOINT=http://<collector>:4318
    • Point at any OTLP-compatible backend (Grafana Tempo, Jaeger, Alloy, etc.).
  2. Create a workflow called "Child" with one or more nodes (e.g. Set → Code).
  3. Create a workflow called "Parent" with a trigger, a Set node, and an Execute Workflow node that calls "Child".
  4. Execute Parent.
  5. Inspect traces in the OTLP backend.

Expected behavior

One trace with a clean parent/child hierarchy, for example:

workflow.execute  (Parent)
├── node.execute  Trigger
├── node.execute  Set
└── node.execute  Execute Workflow
    └── workflow.execute  (Child)
        ├── node.execute  Set
        └── node.execute  Code

All spans share a single traceId. The Child's workflow.execute span's parentSpanId equals the Parent's Execute Workflow node.execute span id.

Actual: Only the Parent's workflow.execute and its node.execute spans are emitted. The Child workflow's spans never appear. Nothing links the parent execution to the child execution in the tracing backend.

Debug Info

<!-- TODO: Replace this with the output from Help > About n8n > Copy debug information on your instance before submitting. -->

Operating System

Windows Server 2022

n8n Version

2.17.2

Node.js Version

22.22.2

Database

PostgreSQL

Execution mode

queue

Hosting

self hosted

extent analysis

TL;DR

To fix the OpenTelemetry tracing issue with sub-workflows, modify the getLifecycleHooksForSubExecutions factory to register module hooks, and update WorkflowExecuteBeforeContext to carry the parentExecution information.

Guidance

  • Review the getLifecycleHooksForSubExecutions factory in execution-lifecycle-hooks.ts to ensure it calls Container.get(ModulesHooksRegistry).addHooks(hooks) to register module hooks for sub-executions.
  • Update WorkflowExecuteBeforeContext to include the parentExecution information, allowing the OTEL module to link the sub-workflow's workflow.execute span to the parent's Execute Workflow node.execute span.
  • Verify the fix by executing the "Parent" workflow and inspecting the traces in the OTLP backend to ensure a single nested trace with the expected hierarchy is produced.
  • Consider creating a test case to validate the tracing behavior for sub-workflows.

Example

// Modified getLifecycleHooksForSubExecutions factory
export function getLifecycleHooksForSubExecutions(): ILifecycleHooks {
  const hooks: ILifecycleHooks = {
    // ...
  };
  Container.get(ModulesHooksRegistry).addHooks(hooks); // Register module hooks
  return hooks;
}

Notes

The provided solution assumes that the getLifecycleHooksForSubExecutions factory is the primary cause of the issue. However, additional modifications might be necessary to ensure proper tracing behavior for sub-workflows.

Recommendation

Apply the workaround by modifying the getLifecycleHooksForSubExecutions factory and updating WorkflowExecuteBeforeContext to carry the parentExecution information, as this addresses the identified root cause and secondary gap.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

One trace with a clean parent/child hierarchy, for example:

workflow.execute  (Parent)
├── node.execute  Trigger
├── node.execute  Set
└── node.execute  Execute Workflow
    └── workflow.execute  (Child)
        ├── node.execute  Set
        └── node.execute  Code

All spans share a single traceId. The Child's workflow.execute span's parentSpanId equals the Parent's Execute Workflow node.execute span id.

Actual: Only the Parent's workflow.execute and its node.execute spans are emitted. The Child workflow's spans never appear. Nothing links the parent execution to the child execution in the tracing backend.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING