crewai - ✅(Solved) Fix [BUG] Do not invoke synchronous `call()` on LLM from asynchronous workflow in `_export_output` / converter [1 pull requests, 2 comments, 3 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
crewAIInc/crewAI#5230Fetched 2026-04-08 02:43:17
View on GitHub
Comments
2
Participants
3
Timeline
6
Reactions
1
Timeline (top)
commented ×2referenced ×2cross-referenced ×1labeled ×1

In an asynchronous workflow started via akickoff(), there should not be synchronous calls being made as that blocks the event loop. If the output of a task requires converting to fit the desired output format, there are currently synchronous calls made to the LLM via several invocations of self.llm.call() here in the converter module (https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/utilities/converter.py), which blocks the event loop.

Calls from async methods here: https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L605 https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L1258

_export_output then delegates to the converter module here: https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L1023

Root Cause

In an asynchronous workflow started via akickoff(), there should not be synchronous calls being made as that blocks the event loop. If the output of a task requires converting to fit the desired output format, there are currently synchronous calls made to the LLM via several invocations of self.llm.call() here in the converter module (https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/utilities/converter.py), which blocks the event loop.

Calls from async methods here: https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L605 https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L1258

_export_output then delegates to the converter module here: https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L1023

Fix Action

Fixed

PR fix notes

PR #5252: fix: use async LLM calls in async task output conversion

Description (problem / solution / changelog)

Summary

Fixes #5230

When tasks execute via akickoff(), _export_output() calls synchronous llm.call() inside async methods (_aexecute_core, _ainvoke_guardrail_function), blocking the event loop and severely degrading async throughput.

This PR adds async variants throughout the output conversion chain so that async task execution never blocks the event loop with synchronous LLM calls. Sync paths are completely untouched.

Changes

FileChange
base_output_converter.pyAdded ato_pydantic() / ato_json() with asyncio.to_thread defaults for backward compat
internal_instructor.pyAdded async instructor client (_get_async_client) + ato_pydantic() / ato_json(). Extracted _build_provider_model_string() to deduplicate sync/async client factories
converter.pyAdded Converter.ato_pydantic() / ato_json() using llm.acall(). Added module-level aconvert_to_model(), ahandle_partial_json(), aconvert_with_instructions()
task.pyAdded _aexport_output() using aconvert_to_model(). Updated all 3 async call sites
test_converter.py9 new async tests verifying llm.call() is never invoked from async paths

Design decisions

  • OutputConverter base class: New async methods have asyncio.to_thread defaults so existing custom subclasses work without modification. Converter overrides with native async.
  • ato_pydantic fallback: Uses inline regex extraction instead of calling sync handle_partial_json (which would fall through to llm.call()).
  • InternalInstructor: Lazy-creates async instructor client via _get_async_client(), cached after first use. Uses instructor.from_litellm(acompletion) for litellm or instructor.from_provider(..., async_mode="async") for other providers.

Test plan

  • 9 new async tests all pass (pytest -k "async or ato_")
  • All 51 existing converter tests still pass (no regressions)
  • Each async test asserts llm.call.assert_not_called() — sync path never invoked
  • Key regression test: test_ato_pydantic_validation_failure_never_calls_sync covers the ValidationError fallback path
  • Manual testing with akickoff() + output_pydantic task in a real crew

Changed files

  • lib/crewai/src/crewai/agents/agent_builder/utilities/base_output_converter.py (modified, +31/-0)
  • lib/crewai/src/crewai/task.py (modified, +35/-4)
  • lib/crewai/src/crewai/utilities/converter.py (modified, +258/-0)
  • lib/crewai/src/crewai/utilities/internal_instructor.py (modified, +91/-8)
  • lib/crewai/tests/utilities/test_converter.py (modified, +192/-1)
RAW_BUFFERClick to expand / collapse

Description

In an asynchronous workflow started via akickoff(), there should not be synchronous calls being made as that blocks the event loop. If the output of a task requires converting to fit the desired output format, there are currently synchronous calls made to the LLM via several invocations of self.llm.call() here in the converter module (https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/utilities/converter.py), which blocks the event loop.

Calls from async methods here: https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L605 https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L1258

_export_output then delegates to the converter module here: https://github.com/crewAIInc/crewAI/blob/c14abf1758dd3aafc57ad7f17569174c7cc1ea68/lib/crewai/src/crewai/task.py#L1023

Steps to Reproduce

Use some example workflow that uses a Task(output_json=...)orTask(output_pydantic=...) to require a specific output format, e.g.: https://github.com/crewAIInc/crewAI-examples/blob/b4c0ef6522c37d4148f8372a731fd2851fdcb864/flows/self_evaluation_loop_flow/src/self_evaluation_loop_flow/crews/x_post_review_crew/x_post_review_crew.py#L32

Invoke the workflow asynchronously using akickoff().

Expected behavior

Only calls to await llm.acall() should be made from an asynchronous workflow.

Screenshots/Code snippets

Operating System

Debian 12 Bookworm

Python Version

3.12

crewAI Version

1.12.2

crewAI Tools Version

Virtual Environment

Venv

Evidence

Possible Solution

Additional context

extent analysis

TL;DR

Replace synchronous self.llm.call() with asynchronous await self.llm.acall() in the converter module to prevent blocking the event loop.

Guidance

  • Identify all instances of self.llm.call() in the converter module and replace them with await self.llm.acall() to ensure asynchronous execution.
  • Verify that the converter module is properly handling asynchronous calls by checking for any remaining synchronous self.llm.call() invocations.
  • Review the task module to ensure that all calls to the converter module are properly awaiting the asynchronous results, using await keywords where necessary.
  • Test the modified workflow using akickoff() to confirm that the event loop is no longer blocked by synchronous calls.

Example

# Before
output = self.llm.call(input_data)

# After
output = await self.llm.acall(input_data)

Notes

This solution assumes that self.llm.acall() is a properly implemented asynchronous method. If this method does not exist or is not correctly implemented, additional modifications may be necessary.

Recommendation

Apply workaround: Replace synchronous self.llm.call() with asynchronous await self.llm.acall() to prevent blocking the event loop, as this is a more targeted solution than upgrading to a potentially non-existent fixed version.

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

Only calls to await llm.acall() should be made from an asynchronous workflow.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING