Skip to content

fix: support async callables in akickoff before/after callbacks#6494

Open
ashusnapx wants to merge 1 commit into
crewAIInc:mainfrom
ashusnapx:feat/async-callback-fix
Open

fix: support async callables in akickoff before/after callbacks#6494
ashusnapx wants to merge 1 commit into
crewAIInc:mainfrom
ashusnapx:feat/async-callback-fix

Conversation

@ashusnapx

Copy link
Copy Markdown

Summary

Fixes silent discarding of async callables in Crew.akickoff().

Closes #6481

Problem

Crew.akickoff() is a native async execution path, but before_kickoff_callbacks and after_kickoff_callbacks were invoked synchronously with no inspect.isawaitable check. Async callables were silently discarded — their coroutine objects were never awaited.

async def my_callback(result):
    await some_async_operation()  # never awaited
    return result

crew = Crew(..., after_kickoff_callbacks=[my_callback])
await crew.akickoff()  # callback silently discarded

Fix

Component Change
after_kickoff_callbacks in akickoff() Added inspect.isawaitable check + await
before_kickoff_callbacks in akickoff() Created aprepare_kickoff() async variant that awaits async callbacks

Follows the same pattern used by task_callback and step_callback throughout the codebase:

cb_result = callback(result)
if inspect.isawaitable(cb_result):
    await cb_result

Files Changed

File Change
crew.py Added import inspect, fixed after_callback loop, imports aprepare_kickoff
crews/utils.py Added aprepare_kickoff() async variant with inspect.isawaitable for before_callbacks
tests/crew/test_async_crew.py 3 new tests: async before, async after, mixed sync+async

Tests

# Run async crew tests
pytest lib/crewai/tests/crew/test_async_crew.py -v

# Run all crew tests
pytest lib/crewai/tests/test_crew.py lib/crewai/tests/crew/ -v
  • 14 async crew tests pass (3 new)
  • ruff: all checks passed
  • mypy: no issues found

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b770c083-e7d3-4267-80fb-ebfbb5c2b361

📥 Commits

Reviewing files that changed from the base of the PR and between eb92e41 and 04c4ced.

📒 Files selected for processing (3)
  • lib/crewai/src/crewai/crew.py
  • lib/crewai/src/crewai/crews/utils.py
  • lib/crewai/tests/crew/test_async_crew.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/crewai/src/crewai/crew.py
  • lib/crewai/src/crewai/crews/utils.py
  • lib/crewai/tests/crew/test_async_crew.py

📝 Walkthrough

Walkthrough

Adds async kickoff preparation in crews/utils.py, updates Crew.akickoff to use it and await awaitable callback results, and extends async kickoff tests for before/after callback handling.

Changes

Async before/after kickoff callback support

Layer / File(s) Summary
Async kickoff preparation helper
lib/crewai/src/crewai/crews/utils.py
Adds inspect import and new aprepare_kickoff async function mirroring prepare_kickoff, awaiting awaitable before_kickoff_callbacks results and completing kickoff setup.
akickoff integration with async preparation and callback awaiting
lib/crewai/src/crewai/crew.py
Imports inspect and aprepare_kickoff, replaces the synchronous prepare_kickoff call with await aprepare_kickoff, and awaits after_kickoff_callbacks results when they are awaitable.
Async callback test coverage
lib/crewai/tests/crew/test_async_crew.py
Adds asyncio import and three new async test methods validating that akickoff() awaits async before/after callbacks and executes mixed sync/async callbacks in the expected order.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Crew as Crew.akickoff
  participant Utils as aprepare_kickoff
  participant BeforeCB as before_kickoff_callbacks
  participant AfterCB as after_kickoff_callbacks

  Caller->>Crew: await akickoff(inputs)
  Crew->>Utils: await aprepare_kickoff(crew, inputs)
  Utils->>BeforeCB: call before_callback(normalized)
  BeforeCB-->>Utils: result or awaitable
  Utils->>Utils: await result if isawaitable
  Utils-->>Crew: normalized inputs
  Crew->>AfterCB: call after_callback(result)
  AfterCB-->>Crew: result or awaitable
  Crew->>Crew: await result if isawaitable
  Crew-->>Caller: CrewOutput
Loading

Suggested reviewers: vinibrsl

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding async callback support to akickoff before/after hooks.
Description check ✅ Passed The description directly matches the code changes and explains the async callback fix in detail.
Linked Issues check ✅ Passed The changes satisfy #6481 by awaiting async after callbacks and adding an async prepare_kickoff path for before callbacks.
Out of Scope Changes check ✅ Passed The diff appears focused on the async callback fix and related tests, with no obvious unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Closes crewAIInc#6481

Crew.akickoff() is a native async execution path, but
before_kickoff_callbacks and after_kickoff_callbacks were invoked
synchronously with no awaitable check. Async callables were silently
discarded — their coroutine objects were never awaited.

- crew.py: Add inspect.isawaitable check in akickoff after_callback loop
- crews/utils.py: Add aprepare_kickoff() async variant that awaits async
  before_kickoff_callbacks using inspect.isawaitable
- crew.py: akickoff() now calls aprepare_kickoff() instead of
  prepare_kickoff()

Follows the same pattern used by task_callback and step_callback.

Tests: test_akickoff_awaits_async_after_callback,
test_akickoff_awaits_async_before_callback,
test_akickoff_mixed_sync_async_callbacks.
All 14 async crew tests pass. Lint and mypy clean.
@ashusnapx ashusnapx force-pushed the feat/async-callback-fix branch from eb92e41 to 04c4ced Compare July 9, 2026 10:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/crews/utils.py`:
- Around line 362-474: The kickoff preparation logic is duplicated between
prepare_kickoff and aprepare_kickoff, creating drift risk as shared behavior
changes over time. Extract the common control flow into a shared internal helper
used by both functions, and keep only the before_kickoff_callbacks execution
different by injecting a callback runner that either returns directly or awaits
awaitable results. Preserve the existing behavior around crewai_event_bus
emission, store_files, _set_tasks_callbacks, setup_agents, and planning in the
shared helper.
- Around line 423-431: The kickoff planning path is still blocking the event
loop because `aprepare_kickoff` ultimately calls `crew._handle_crew_planning()`
synchronously via `CrewPlanner._handle_crew_planning()` and `execute_sync()`.
Update the async kickoff flow in `utils.py` so planning is offloaded to a thread
or handled through a non-blocking async path before returning from `akickoff()`,
using the existing `aprepare_kickoff`, `crew._handle_crew_planning`, and
`CrewPlanner` hooks to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 460ccfbf-0675-415f-a37f-8ca756db70d3

📥 Commits

Reviewing files that changed from the base of the PR and between 289686a and eb92e41.

📒 Files selected for processing (3)
  • lib/crewai/src/crewai/crew.py
  • lib/crewai/src/crewai/crews/utils.py
  • lib/crewai/tests/crew/test_async_crew.py

Comment on lines +362 to +474
async def aprepare_kickoff(
crew: Crew,
inputs: dict[str, Any] | None,
input_files: dict[str, FileInput] | None = None,
) -> dict[str, Any] | None:
"""Async version of prepare_kickoff for use with akickoff().

Mirrors :func:`prepare_kickoff` but awaits async before-kickoff
callbacks so that coroutines are not silently discarded.

Args:
crew: The crew instance to prepare.
inputs: Optional input dictionary to pass to the crew.
input_files: Optional dict of named file inputs for the crew.

Returns:
The potentially modified inputs dictionary after before callbacks.
"""
from crewai.events.base_events import reset_emission_counter
from crewai.events.event_bus import crewai_event_bus
from crewai.events.event_context import (
get_current_parent_id,
reset_last_event_id,
)
from crewai.events.types.crew_events import CrewKickoffStartedEvent

resuming = crew.checkpoint_kickoff_event_id is not None

if not resuming and get_current_parent_id() is None:
reset_emission_counter()
reset_last_event_id()

normalized: dict[str, Any] | None = None
if inputs is not None:
if not isinstance(inputs, Mapping):
raise TypeError(
f"inputs must be a dict or Mapping, got {type(inputs).__name__}"
)
normalized = dict(inputs)

for before_callback in crew.before_kickoff_callbacks:
if normalized is None:
normalized = {}
normalized = before_callback(normalized)
if inspect.isawaitable(normalized):
normalized = await normalized

if resuming and crew._kickoff_event_id:
if crew.verbose:
from crewai.events.utils.console_formatter import ConsoleFormatter

fmt = ConsoleFormatter(verbose=True)
content = fmt.create_status_content(
"Resuming from Checkpoint",
crew.name or "Crew",
"bright_magenta",
ID=str(crew.id),
)
fmt.print_panel(
content, "\U0001f504 Resuming from Checkpoint", "bright_magenta"
)
else:
started_event = CrewKickoffStartedEvent(crew_name=crew.name, inputs=normalized)
crew._kickoff_event_id = started_event.event_id
future = crewai_event_bus.emit(crew, started_event)
if future is not None:
try:
future.result()
except Exception: # noqa: S110
pass

crew._task_output_handler.reset()
crew._logging_color = "bold_purple"

_flow_files = baggage.get_baggage("flow_input_files")
flow_files: dict[str, Any] = _flow_files if isinstance(_flow_files, dict) else {}

if normalized is not None:
unpacked_files = _extract_files_from_inputs(normalized)

all_files = {**flow_files, **(input_files or {}), **unpacked_files}
if all_files:
store_files(crew.id, all_files)

crew._inputs = normalized
crew._interpolate_inputs(normalized)
else:
all_files = {**flow_files, **(input_files or {})}
if all_files:
store_files(crew.id, all_files)
crew._set_tasks_callbacks()
crew._set_allow_crewai_trigger_context_for_first_task()

agents_to_setup: list[BaseAgent] = list(crew.agents)
seen_agent_ids: set[int] = {id(agent) for agent in agents_to_setup}
for task in crew.tasks:
if task.agent is not None and id(task.agent) not in seen_agent_ids:
agents_to_setup.append(task.agent)
seen_agent_ids.add(id(task.agent))

setup_agents(
crew,
agents_to_setup,
crew.embedder,
crew.function_calling_llm,
crew.step_callback,
)

if crew.planning:
crew._handle_crew_planning()

return normalized

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Large duplicated function body between prepare_kickoff and aprepare_kickoff.

aprepare_kickoff Introduces the new async function aprepare_kickoff, duplicating the core kickoff preparation/control flow from prepare_kickoff while extending before_kickoff_callbacks handling to await callback results when they are awaitable. Aside from the two lines added at 406-407, the ~110-line body is a verbatim copy of prepare_kickoff (lines 250-359). Any future fix/change to the shared logic (event emission, file storage, task/agent setup, planning) now has to be manually mirrored in both functions, which is a real drift risk.

Consider extracting a shared internal helper (parameterized by an "await-if-awaitable" strategy or an injected callback runner) that both prepare_kickoff and aprepare_kickoff call, keeping only the callback-invocation loop distinct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 362 - 474, The kickoff
preparation logic is duplicated between prepare_kickoff and aprepare_kickoff,
creating drift risk as shared behavior changes over time. Extract the common
control flow into a shared internal helper used by both functions, and keep only
the before_kickoff_callbacks execution different by injecting a callback runner
that either returns directly or awaits awaitable results. Preserve the existing
behavior around crewai_event_bus emission, store_files, _set_tasks_callbacks,
setup_agents, and planning in the shared helper.

Comment thread lib/crewai/src/crewai/crews/utils.py
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.

[BUG] before/after_kickoff_callbacks do not support async callables in akickoff

1 participant