Skip to content

ADK 2.0 ignores before_run callback for Workflow #6013

Description

@mfojtak

ADK 2.0 ignores before_run callback returning a value for workflows. In this case the execution should stop and no further llm calls should be executed.

Problem is in runner.py / _run_node_async where

await ic.plugin_manager.run_before_run_callback(invocation_context=ic)

swallows returned value and never checks if execution should continue

Steps to Reproduce:
Use Workflow as root_agent
Implement plugin and override before_run_callback

Expected Behavior:
Execution should stop if before_run_callback returns a value as described here:
"Returning a value to halt execution of the runner and ends the runner with that event"

Environment Details:

  • ADK Library Version (pip show google-adk): 2.2
  • Desktop OS: Linux
  • Python Version (python -V): 3.12

Model Information:

  • Are you using LiteLLM: No

How often has this issue occurred?:

  • Always (100%)

Activity

  1. added
    workflow[Component] This issue is related to ADKworkflow
    on Jun 8, 2026
  2. added theissue type on Jun 8, 2026
  3. adk-bot commented on Jun 8, 2026

    @adk-bot
    Collaborator

    I will begin by reading the instructions for the adk-issue-analyze skill to understand the strict read-only analysis process.I will fetch the details of GitHub issue #6013.I will search for the path of runner.py using find_by_name.I will search for the string run_before_run_callback to locate where this callback is invoked.I will view src/google/adk/runners.py starting around line 500 to check how run_before_run_callback is called.I will view src/google/adk/runners.py around line 1350 to see the second usage of run_before_run_callback.I will view src/google/adk/runners.py from line 440 to 500 to identify the surrounding class and method.I will search for the class definitions in runners.py to see which class contains _run_node_async.I will search for _run_node_async calls in runners.py.I will view lines 580 to 650 of src/google/adk/runners.py.I will view src/google/adk/runners.py from line 640 to 700 to check for any usage of run_before_run_callback in _run_node_live.I will run a gh command to search for any pull requests associated with issue #6013.I will search for test files related to plugins or runners to see how they are tested.I will search for callback or plugin test instances in tests/unittests/test_runners.py.I will view tests/unittests/test_runners.py from line 145 to 180 to inspect the test plugin setup.I will view lines 940 to 960 in tests/unittests/test_runners.py.I will view the tests/unittests/runners/test_runner_node.py file to see how node or workflow runners are tested.I will view src/google/adk/plugins/plugin_manager.py around line 145 to see how run_before_run_callback handles returning values.I will run the runner node unit tests to confirm the test suite is functioning correctly.# GitHub Issue #6013 Analysis: ADK 2.0 ignores before_run callback for Workflow

    Executive Summary

    1. What is broken? When executing a workflow (or any other BaseNode / agent node) via Runner(node=...), ADK 2.0 executes _run_node_async where it discards the return value of before_run_callback. Unlike the standard LLM sequential execution path in _exec_with_plugin, early exit returned values of type types.Content are swallowed and never checked, letting node/workflow execution progress unchecked.
    2. Is there a linked PR that fixes this issue? None
    3. Recommendation: Should Fix (High Priority) - Update _run_node_async to capture the callback return value and gracefully early-exit if a types.Content instance is returned, mirroring the behavior of _exec_with_plugin.
    Detailed Analysis

    1. Root Cause Analysis ("What is broken?")

    • The Bug:
      In runners.py, agent workflows are executed through _run_node_async because Workflow is a subclass of BaseNode. At line 532, the before-run callbacks are executed:

      # Run before_run callbacks
      await ic.plugin_manager.run_before_run_callback(invocation_context=ic)

      The returned value of type Optional[types.Content] is ignored/discarded. Consequently, if a plugin returns a non-None value to stop execution and end the runner, the runner ignores it and schedules the root node/workflow anyway.

      In comparison, the sequential execution wrapper _exec_with_plugin in runners.py:L1373-1393 correctly evaluates the callback return value:

      early_exit_result = await plugin_manager.run_before_run_callback(
          invocation_context=invocation_context
      )
      if isinstance(early_exit_result, types.Content):
        early_exit_event = Event(...)
        ...
        yield early_exit_event
      else:
        # Continue execution...
    • Code References:

      • Malfunctioning callback invocation: runners.py:L531-L532
      • Reference working code structure: runners.py:L1373-L1393

    2. Existing Pull Requests ("Is there a linked PR that fixes this issue?")

    • Linked PR: None
    • Analysis: Command gh pr list --search "6013" returned no related PRs.

    3. Recommendation

    • Recommendation: Should Fix (High Priority)
    • Rationale:
      • Impact: Swallowing before-run callback outputs during workflow runs breaks the documented contract of interrupting runners cleanly. Under normal conditions, interrupting workflows early is highly critical to preventing unnecessary LLM charges and downstream orchestration side-effects.
      • Implementation Complexity: Low. The fix is localized to _run_node_async and matches the pattern employed by _exec_with_plugin.
      • Proposed Fix:
        Replace runners.py:L531-L532 with:
        # Run before_run callbacks
        early_exit_result = await ic.plugin_manager.run_before_run_callback(
            invocation_context=ic
        )
        if isinstance(early_exit_result, types.Content):
          early_exit_event = Event(
              invocation_id=ic.invocation_id,
              author='model',
              content=early_exit_result,
          )
          _apply_run_config_custom_metadata(
              early_exit_event, ic.run_config
          )
          await self.session_service.append_event(
              session=ic.session,
              event=early_exit_event,
          )
          yield early_exit_event
          await ic.plugin_manager.run_after_run_callback(invocation_context=ic)
          if self.app and self.app.events_compaction_config:
            logger.debug('Running event compactor.')
            from google.adk.apps.compaction import _run_compaction_for_sliding_window
        
            await _run_compaction_for_sliding_window(
                self.app,
                session,
                self.session_service,
                skip_token_compaction=ic.token_compaction_checked,
            )
          return

    Summary of Completed Steps

    1. Retrieved & Parsed Issue: Fetched issue details for ADK 2.0 ignores before_run callback for Workflow #6013 via custom API tooling.
    2. Located & Analyzed MALFUNCTIONING Code: Traced workflow execution to the _run_node_async routing in runners.py and inspected the missing check on run_before_run_callback.
    3. Confirmed Non-existence of existing PRs: Polled repository PRs for references to issue ADK 2.0 ignores before_run callback for Workflow #6013.
    4. Delivered Premium Report: Drafted an exhaustive, structured triage report outlining the root-cause, alignment, and a reliable implementation fix.
  4. llalitkumarrr commented on Jun 10, 2026

    @llalitkumarrr
    Collaborator

    Hello @mfojtak ,

    To help us investigate this issue, could you please share your reproduction code, the steps to reproduce the behavior and any additional observations you have?
    Additionally, you can refer to this page in the ADK documentation to verify your implementation.

  5. added
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jun 10, 2026
  6. mfojtak commented on Jun 10, 2026

    @mfojtak
    Author

    Hello @llalitkumarrr
    I cannot share the reproduction code as it is our internal protected code.
    But the root of the problem is simple. In the new 2.0 implementation, the plugins are not working properly because of line

    await ic.plugin_manager.run_before_run_callback(invocation_context=ic)

    which ignores return value of the run_before_run_callback function and violates the predefined behavior of any plugin.

    The correct behavior is implemented in the old pre-2.0 code:

    early_exit_result = await plugin_manager.run_before_run_callback(
            invocation_context=invocation_context
        )
        if isinstance(early_exit_result, types.Content):
          early_exit_event = Event(
              invocation_id=invocation_context.invocation_id,
              author='model',
              content=early_exit_result,
          )

    where the return value is checked for early exit.

  7. llalitkumarrr commented on Jun 17, 2026

    @llalitkumarrr
    Collaborator

    Hello @mfojtak ,

    Could please have a look at #6032 check if this fix your issue?

  8. adk-bot commented on Jun 26, 2026

    @adk-bot
    Collaborator

    This issue has been automatically marked as stale because it has not had recent activity for 7 days after a maintainer requested clarification. It will be closed if no further activity occurs within 7 days.

  9. added
    stale[Status] Issues which have been marked inactive since there is no user response
    on Jun 26, 2026
  10. adk-bot commented on Jul 4, 2026

    @adk-bot
    Collaborator

    This has been automatically closed because it has been marked as stale for over 7 days.

  11. added a commit that references this issue on Aug 21, 2026
    dac1869
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

request clarification[Status] The maintainer need clarification or more information from the authorstale[Status] Issues which have been marked inactive since there is no user responsev2Affects only 2.0 versionworkflow[Component] This issue is related to ADKworkflow

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions