Skip to content

fix: support async before/after_kickoff_callbacks in akickoff#6482

Open
magiccao wants to merge 8 commits into
crewAIInc:mainfrom
magiccao:fix/akickoff-async-callbacks
Open

fix: support async before/after_kickoff_callbacks in akickoff#6482
magiccao wants to merge 8 commits into
crewAIInc:mainfrom
magiccao:fix/akickoff-async-callbacks

Conversation

@magiccao

@magiccao magiccao commented Jul 8, 2026

Copy link
Copy Markdown

Summary

Fixes #6481

Crew.akickoff() is a native async execution path, but before_kickoff_callbacks and after_kickoff_callbacks were invoked synchronously, silently discarding async callables and blocking the event loop on IO-bound sync callbacks.

This PR brings akickoff into alignment with the existing async callback handling for task_callback and step_callback.

Changes

crews/utils.py

Refactors prepare_kickoff by extracting three private helpers:

  • _begin_prepare_kickoff — emission counter reset + resuming flag
  • _normalize_inputs — input type validation and dict conversion
  • _finish_prepare_kickoff — event emission, file handling, input interpolation, agent setup, planning

Adds aprepare_kickoff (async counterpart): identical to prepare_kickoff except the before_kickoff_callbacks loop uses inspect.isawaitable() + await to support coroutine callbacks.

crew.py

  • akickoff: replaces prepare_kickoff(...) with await aprepare_kickoff(...)
  • akickoff: adds inspect.isawaitable check in the after_kickoff_callbacks loop

Behavior

callback before after
sync callable unchanged unchanged
async callable in akickoff now awaited now awaited
async callable in kickoff_async still not awaited ⚠️ still not awaited ⚠️

Note on kickoff_async: this PR only fixes async callback support in akickoff. kickoff_async runs kickoff() in a worker thread via asyncio.to_thread, so sync callbacks won't block the event loop — but async before/after_kickoff_callbacks are still not awaited there. Fixing kickoff_async would require a separate effort.

Test plan

  • Existing test_async_crew.py tests pass (sync callbacks unaffected)
  • test_akickoff_calls_async_before_callbacks — async before callback is awaited; modified inputs reach task interpolation
  • test_akickoff_calls_async_after_callbacks — async after callback is awaited; result remains CrewOutput
  • test_akickoff_mixed_sync_async_callbacks — mixed pipeline (sync_before → async_before → async_after → sync_after) executes in correct order and preserves the transform chain

before_kickoff_callbacks and after_kickoff_callbacks were invoked
synchronously in akickoff, silently dropping async callables and
blocking the event loop on IO-bound sync callbacks.

- Extract _begin_prepare_kickoff, _normalize_inputs, and
  _finish_prepare_kickoff helpers from prepare_kickoff to eliminate
  code duplication between the sync and async paths.
- Add aprepare_kickoff (async counterpart of prepare_kickoff) that
  awaits coroutine before-callbacks via inspect.isawaitable, consistent
  with the existing task_callback pattern in task.py.
- Use aprepare_kickoff in akickoff instead of prepare_kickoff.
- Add inspect.isawaitable check for after_kickoff_callbacks in akickoff.
- Add tests covering async before, async after, and mixed sync+async
  callback pipelines.

kickoff_async is unaffected: it wraps the entire sync kickoff in
asyncio.to_thread, so before/after callbacks already run off the
event loop.
@coderabbitai

coderabbitai Bot commented Jul 8, 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: 4d6bcf49-c1b2-4880-a3e8-d94eeacb4337

📥 Commits

Reviewing files that changed from the base of the PR and between d5d5460 and 6cb8486.

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

📝 Walkthrough

Walkthrough

Adds async-aware handling for before_kickoff_callbacks and after_kickoff_callbacks in Crew.akickoff(). Introduces aprepare_kickoff alongside shared preparation helpers, plus tests covering async and mixed synchronous/asynchronous callback scenarios.

Changes

Async kickoff callback support

Layer / File(s) Summary
Shared kickoff preparation helpers and async variant
lib/crewai/src/crewai/crews/utils.py
Refactors kickoff preparation into shared helpers and adds aprepare_kickoff, which awaits awaitable before_kickoff_callbacks results before completing setup.
akickoff() awaits async preparation and callbacks
lib/crewai/src/crewai/crew.py
Uses awaited aprepare_kickoff and awaits awaitable after_kickoff_callbacks results before post-kickoff processing.
Async callback tests
lib/crewai/tests/crew/test_async_crew.py
Tests async before and after callbacks, input injection, and ordering across mixed callback types.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Crew
  participant aprepare_kickoff
  participant BeforeCallback
  participant AfterCallback

  Caller->>Crew: akickoff(inputs)
  Crew->>apprepare_kickoff: await preparation
  aprepare_kickoff->>BeforeCallback: invoke callback
  BeforeCallback-->>apprepare_kickoff: value or awaitable
  aprepare_kickoff->>apprepare_kickoff: await awaitable result
  aprepare_kickoff-->>Crew: prepared inputs
  Crew->>Crew: execute tasks and build CrewOutput
  Crew->>AfterCallback: invoke callback(CrewOutput)
  AfterCallback-->>Crew: value or awaitable
  Crew->>Crew: await awaitable result
  Crew-->>Caller: return CrewOutput
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: async before/after kickoff callbacks in akickoff.
Description check ✅ Passed The description directly describes the async callback fix and related kickoff refactor.
Linked Issues check ✅ Passed The PR implements the linked issue requirements by awaiting async before/after kickoff callbacks in akickoff and preserving sync behavior.
Out of Scope Changes check ✅ Passed The helper refactor and tests are supporting changes that stay within the async callback support scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

@ErenAta16 ErenAta16 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.

Same fix as #6500 and #6494 (also open), and like #6500 this one refactors prepare_kickoff into shared helpers (_begin_prepare_kickoff/_normalize_inputs/_finish_prepare_kickoff) rather than duplicating the whole function body for the new async path, so prepare_kickoff and aprepare_kickoff stay in sync automatically as the shared logic evolves — same quality bar as #6500, just a different helper decomposition. Left the fuller three-way comparison on #6500's thread.

Also flagging the same thing I noted there: the description mentions IO-bound sync callbacks blocking the event loop as motivation, but the actual fix (here and in the other two) only adds the inspect.isawaitable check for async callables — a slow sync callback still runs inline. Worth confirming that is intentionally out of scope for this PR rather than an oversight, since the title/description read as covering both.

@magiccao

magiccao commented Jul 13, 2026

Copy link
Copy Markdown
Author

@ErenAta16 Thanks for the close read. To clarify the scope: the "blocking the event loop on IO-bound sync callbacks" motivation and the async-callable fix are actually the same root cause, not two separate concerns.

In the old akickoff, async callables were silently discarded (the returned coroutine was never awaited), which left users with no non-blocking path — they were forced to write sync IO-bound callbacks that blocked the loop. By adding inspect.isawaitable() + await, users can now write async def callbacks with await inside for IO, yielding control back to the event loop:

# Before — blocks event loop (only path available)
def before(inputs):
    data = requests.get(...)  # blocks
    return {**inputs, **data}

# After — non-blocking (now actually supported)
async def before(inputs):
    data = await httpx_client.get(...)  # yields
    return {**inputs, **data}

So the blocking issue is resolved via the async path, not by offloading sync callbacks. The sync callable | unchanged row in the behavior table isn't a scope gap — sync callbacks should remain synchronous by definition; when non-blocking IO is needed, the callback should be written as async def. This is also consistent with how task_callback / step_callback already handle async callables in akickoff.

Re: the duplicate PRs (#6494, #6500) — happy to consolidate once maintainers indicate which approach they prefer; this one's helper decomposition (_begin_prepare_kickoff / _normalize_inputs / _finish_prepare_kickoff) keeps the sync and async variants sharing the same tail logic so they stay in sync as the shared logic evolves.

@ErenAta16

Copy link
Copy Markdown

That reframing holds up. The before/after example makes it clear: async callables were previously discarded outright (coroutine created, never awaited), so there was no non-blocking option at all, not even for callers willing to write async def. Once awaiting the callable is supported, the IO-bound-blocking motivation and the async-support fix collapse into the same change, and sync callbacks staying synchronous is just the callback keeping the contract it always had.

No further concern on scope from me. Agreed consolidating with #6494/#6500 is a maintainer call once they weigh in on which decomposition they'd rather carry forward.

@magiccao

Copy link
Copy Markdown
Author

Gentle nudge — #6482, #6494, and #6500 all fix the same bug (async before/after_kickoff_callbacks silently dropped in akickoff because the returned coroutine is never awaited). It'd help to converge on one.

This PR:

  • Adds aprepare_kickoff that awaits coroutine callbacks via inspect.isawaitable, mirroring the existing task_callback pattern.
  • Extracts _begin_prepare_kickoff / _normalize_inputs / _finish_prepare_kickoff so sync and async share the tail logic.
  • Adds the same awaitable check for after_kickoff_callbacks in akickoff.

Rebased on latest main to pick up the new interception hooks (#6516#6518). Wrapped the new EXECUTION_START / INPUT dispatch in two helpers (_dispatch_execution_start / _dispatch_input) called around the before-callback loop in both variants, so the shared-tail structure is preserved. Crew + hook + async crew tests pass locally.

Happy to close in favor of whichever decomposition the team prefers — just let us know. An approve from a maintainer would unblock the merge.

cc @joaomdmoura @greysonlalonde

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

2 participants