Skip to content

fix(task): parse pydantic output before guardrail invocation#4650

Open
giulio-leone wants to merge 1 commit intocrewAIInc:mainfrom
giulio-leone:fix/guardrail-pydantic-output-none
Open

fix(task): parse pydantic output before guardrail invocation#4650
giulio-leone wants to merge 1 commit intocrewAIInc:mainfrom
giulio-leone:fix/guardrail-pydantic-output-none

Conversation

@giulio-leone
Copy link
Contributor

@giulio-leone giulio-leone commented Feb 28, 2026

Problem

When a task has output_pydantic and a guardrail configured, TaskOutput.pydantic is None on the first guardrail invocation but correctly parsed on retry attempts. This makes it impossible to write guardrail functions that inspect structured Pydantic output.

Root Cause

In both execute_task() (async) and execute_sync(), when the result is a string and guardrails are present, _export_output() was intentionally skipped:

elif not self._guardrails and not self._guardrail:
    raw = result
    pydantic_output, json_output = self._export_output(result)
else:
    raw = result
    pydantic_output, json_output = None, None  # <-- BUG: always None with guardrails

On retries (line ~1155), _export_output() was always called, creating inconsistent behavior.

Fix

Remove the conditional skip so _export_output() is always called regardless of guardrail presence:

else:
    raw = result
    pydantic_output, json_output = self._export_output(result)

Applied to both sync and async execution paths.

Test

Added test_guardrail_pydantic_output_available_on_first_attempt that verifies:

  • Guardrail receives non-None pydantic on the first (and only) invocation
  • The parsed Pydantic model matches the expected output

All 20 existing guardrail tests pass (2 pre-existing failures unrelated to this change).

Fixes #4369


Note

Medium Risk
Touches core task execution output handling (sync and async), which could subtly change when/what structured parsing occurs, but failures are swallowed with a debug log to preserve raw output behavior.

Overview
Guardrails now receive structured outputs on the first pass. In Task sync + async execution paths, string results are now always run through _export_output() before any guardrail invocation, so TaskOutput.pydantic/json_dict are populated consistently (instead of only after a retry).

Parsing failures are handled defensively (debug log + continue with raw output), and tests were updated/expanded with a regression case (test_guardrail_pydantic_output_available_on_first_attempt) to lock in the new behavior. A tiny formatting-only change was made in crew_agent_executor.py.

Written by Cursor Bugbot for commit 47680d2. This will update automatically on new commits. Configure here.

@giulio-leone
Copy link
Contributor Author

Friendly ping — CI is green and this is ready for review. Happy to address any feedback. Thanks!

Copilot AI review requested due to automatic review settings March 1, 2026 02:14
@giulio-leone giulio-leone force-pushed the fix/guardrail-pydantic-output-none branch from ffac07f to 0a7e9c6 Compare March 1, 2026 02:14
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an inconsistency in Task execution where structured outputs (output_pydantic / output_json) were not exported before guardrails ran on the first attempt, making TaskOutput.pydantic/json_dict unavailable to guardrails until retries.

Changes:

  • Update both async (_aexecute_core) and sync (_execute_core) paths to call _export_output() even when guardrails are configured.
  • Add a regression test ensuring TaskOutput.pydantic is populated for guardrails on the first invocation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
lib/crewai/src/crewai/task.py Always exports structured output before invoking guardrails (sync + async).
lib/crewai/tests/test_task_guardrails.py Adds regression test asserting guardrails receive non-None Pydantic output on first attempt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@giulio-leone giulio-leone force-pushed the fix/guardrail-pydantic-output-none branch 2 times, most recently from e2dd079 to e8ab7fa Compare March 1, 2026 23:12
giulio-leone added a commit to giulio-leone/crewAI that referenced this pull request Mar 2, 2026
@giulio-leone giulio-leone force-pushed the fix/guardrail-pydantic-output-none branch from 769f502 to 9a49654 Compare March 3, 2026 22:36
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

try:
pydantic_output, json_output = self._export_output(result)
except Exception:
self.logger.debug("Pre-guardrail output export failed, continuing with raw output")
Copy link

Choose a reason for hiding this comment

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

Broad try/except silences errors for non-guardrail tasks

Medium Severity

The old code called _export_output() without try/except for tasks without guardrails, allowing errors like ValidationError to propagate to the caller. The new code merges all non-BaseModel results into one else branch with a blanket except Exception, so tasks without guardrails now silently swallow _export_output() failures and produce pydantic=None / json_dict=None instead of raising. This masks legitimate conversion errors (e.g., ValidationError, TypeError from missing agent) that previously surfaced to users. The try/except guard is only needed when guardrails are present.

Additional Locations (1)

Fix in Cursor Fix in Web

@giulio-leone giulio-leone force-pushed the fix/guardrail-pydantic-output-none branch from 9a49654 to 875bb63 Compare March 4, 2026 00:42
@giulio-leone giulio-leone force-pushed the fix/guardrail-pydantic-output-none branch from 97e4e67 to 47680d2 Compare March 4, 2026 04:30
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] TaskOutput.pydantic is None on first guardrail attempt but parsed on retries

2 participants