Skip to content

Add typed output schemas for CrewAI tools - #6236

Merged
vinibrsl merged 9 commits into
mainfrom
tools-schema
Jun 19, 2026
Merged

Add typed output schemas for CrewAI tools#6236
vinibrsl merged 9 commits into
mainfrom
tools-schema

Conversation

@vinibrsl

@vinibrsl vinibrsl commented Jun 19, 2026

Copy link
Copy Markdown
Member

Currently, tools have a strong input contract through args_schema, but no output contract. This means that anything a tool outputs is converted to string.

Not only the contract is weak, but the "invisible" conversion to string can have unexpected effects when the tool returns complex objects like dicts and arrays.

With this PR, a tool can optionally define an output contract with output_schema. CrewAI validates the raw result and sends the agent JSON.

class ProductResult(BaseModel):
    sku: str
    name: str
    in_stock: bool

class ProductLookupTool(BaseTool):
    name: str = "Product Lookup"
    description: str = "Look up product availability by SKU."

    def _run(self, sku: str) -> ProductResult:
        return ProductResult(sku=sku, name="USB-C dock", in_stock=True)

If the result does not match the schema, CrewAI warns and falls back to str(raw_result) instead of failing the run:

@tool("Product Lookup", output_schema=ProductResult)
def product_lookup(sku: str) -> dict[str, object]:
    return {"sku": sku, "name": "USB-C dock", "in_stock": True}

#=> RuntimeWarning: Failed to validate or serialize output from tool 'Bad Product Lookup' using output_schema 'ProductResult'... Falling back to str(raw_result).

This is additive and non-breaking. Existing tools do not need to change. Tools without output_schema keep the old string behavior. Invalid typed outputs warn and fall back to the old formatting path.


Note

Medium Risk
Changes span shared tool execution, caching, and hook context across multiple executors; behavior is intended to be backward compatible but alters how non-string tool results reach agents.

Overview
Optional typed tool outputs mirror input args_schema: tools can declare result_schema (or infer it from a Pydantic _run return type), validate results, and send JSON strings to agents while tool.run() still returns the raw value.

Execution paths (ReAct ToolUsage, native tool calling in crew_agent_executor / agent_utils, experimental executor) now use format_output_for_agent instead of blind str() conversion. @tool accepts result_schema; BaseTool exposes overridable formatting for Markdown/summaries. Invalid or unserializable typed output warns and falls back to str(raw_result).

Tool hooks gain raw_tool_result alongside agent-facing tool_result; caching and ToolsHandler.on_tool_use store raw outputs. Docs cover typed outputs, publishing, and hook semantics.

Reviewed by Cursor Bugbot for commit 1c327a2. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Tools now support optional typed outputs using Pydantic models to provide structured JSON results to agents
    • Typed outputs are automatically formatted as JSON for agent consumption while preserving raw Python values for direct tool calls
    • Tool hook context now includes raw tool results alongside formatted output
  • Documentation

    • Added comprehensive guides on defining and using typed tool outputs with output schemas
    • Included best practices for implementing structured tool outputs

@mintlify

mintlify Bot commented Jun 19, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
crewai 🟢 Ready View Preview Jun 19, 2026, 5:13 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds first-class Pydantic output_schema support to BaseTool and CrewStructuredTool, with schema inference from return type annotations and a format_output_for_agent method that serializes structured results to JSON for agents while preserving raw Python values for direct callers. A new last_raw_result attribute on ToolUsage and a raw_tool_result field on ToolCallHookContext propagate the unformatted return through all executor paths and after-hooks. TavilyResearchTool adds a tavily_output_schema kwarg alias to avoid collision with the new base field.

Changes

Typed Tool Outputs

Layer / File(s) Summary
output_schema field and format_output_for_agent on BaseTool and CrewStructuredTool
lib/crewai/src/crewai/tools/structured_tool.py, lib/crewai/src/crewai/tools/base_tool.py
CrewStructuredTool adds _infer_output_schema_from_callable (from return annotation via get_type_hints), _format_tool_output_for_agent (validates against schema, returns JSON, emits RuntimeWarning and falls back to str() on failure), output_schema Pydantic field, from_function wiring, and format_output_for_agent. BaseTool adds output_schema field with before-validator inference, format_output_for_agent delegation, to_structured_tool propagation, from_langchain inference, and tool(...) decorator output_schema parameter.
ToolUsage.last_raw_result tracking and ToolsHandler type widening
lib/crewai/src/crewai/tools/tool_usage.py, lib/crewai/src/crewai/agents/tools_handler.py
ToolUsage.__init__ initializes last_raw_result = None; all sync/async success, error, usage-limit, and exception branches store raw returns into last_raw_result and call tool.format_output_for_agent before _format_result. ToolsHandler.on_tool_use output parameter widened from str to Any.
ToolCallHookContext.raw_tool_result and executor propagation
lib/crewai/src/crewai/hooks/tool_hooks.py, lib/crewai/src/crewai/utilities/agent_utils.py, lib/crewai/src/crewai/utilities/tool_utils.py, lib/crewai/src/crewai/agents/crew_agent_executor.py, lib/crewai/src/crewai/experimental/agent_executor.py
ToolCallHookContext gains raw_tool_result: Any | None parameter and attribute. All three executor paths (crew_agent_executor, experimental/agent_executor, agent_utils) resolve output_tool early, store raw_tool_result from callable returns and cache reads, format via format_native_tool_output_for_agent, remove redundant structured_tool lookups, and pass raw_tool_result into after-hook contexts. tool_utils reads tool_usage.last_raw_result and passes it to after-hook contexts.
Tests for output_schema, format_output_for_agent, and raw_tool_result
lib/crewai/tests/tools/test_base_tool.py, lib/crewai/tests/tools/test_structured_tool.py, lib/crewai/tests/tools/test_tool_usage.py, lib/crewai/tests/hooks/test_tool_hooks.py, lib/crewai/tests/agents/test_native_tool_calling.py, lib/crewai/tests/utilities/test_agent_utils.py
TestToolOutputSchema asserts raw returns, JSON formatting, warning/fallback behavior, and structured-tool carry-over. CrewStructuredTool tests cover schema inference and JSON output. ToolUsage tests verify JSON returns, cache callback typed values, and after-hook raw_tool_result. ToolCallHookContext tests verify raw_tool_result default and assignment. Executor and agent_utils tests assert JSON output, custom formatter usage, and hook raw_tool_result values.
Typed output schema documentation
docs/edge/en/concepts/tools.mdx, docs/edge/en/guides/tools/publish-custom-tools.mdx, docs/edge/en/learn/create-custom-tools.mdx, docs/edge/en/learn/tool-hooks.mdx, docs/edge/en/learn/execution-hooks.mdx
Adds "Typed Tool Outputs" and "Best Practice: Define Typed Outputs" sections with output_schema usage, format_output_for_agent override examples, and JSON payload illustrations. Updates hook docs to document raw_tool_result alongside tool_result.

TavilyResearchTool Schema Field Alignment

Layer / File(s) Summary
TavilyResearchTool output_schema field override and tavily_output_schema kwarg alias
lib/crewai-tools/src/crewai_tools/tools/tavily_research_tool/tavily_research_tool.py, lib/crewai-tools/tool.specs.json
output_schema field annotated as dict[str, Any] | None with type-ignore and custom pre-validator/serializer that preserves the dict unchanged. __init__ remaps tavily_output_schema kwarg into output_schema when not already set. tool.specs.json replaces the output_schema entry with tavily_output_schema and adds a stream field.
TavilyResearchTool schema field tests
lib/crewai-tools/tests/tools/tavily_research_tool_test.py
Three tests using _FakeTavilyClient verify output_schema is preserved alone, output_schema wins over tavily_output_schema when both are provided, and tavily_output_schema maps into output_schema when provided alone.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add typed output schemas for CrewAI tools' directly and clearly summarizes the main feature being added across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tools-schema

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 and usage tips.

Comment thread lib/crewai/src/crewai/utilities/agent_utils.py
Comment thread lib/crewai/src/crewai/agents/crew_agent_executor.py Outdated

@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: 1

🧹 Nitpick comments (1)
lib/crewai/tests/tools/test_tool_usage.py (1)

241-244: ⚡ Quick win

Avoid asserting exact JSON string formatting in hook-result expectations.

This currently couples the test to compact serializer formatting ({"query":"crew","score":0.7}) rather than the JSON payload itself. Parse context.tool_result before comparison to keep the test stable across harmless formatter changes.

Suggested test assertion adjustment
-    assert seen_results == [
-        ('{"query":"crew","score":0.7}', SearchOutput(query="crew", score=0.7)),
-        ('{"query":"crew","score":0.7}', SearchOutput(query="crew", score=0.7)),
-    ]
+    assert len(seen_results) == 2
+    for tool_result, raw_tool_result in seen_results:
+        assert json.loads(tool_result or "") == {"query": "crew", "score": 0.7}
+        assert raw_tool_result == SearchOutput(query="crew", score=0.7)
🤖 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/tests/tools/test_tool_usage.py` around lines 241 - 244, The test
assertion at line 241-244 is coupling the test to exact JSON string formatting
by comparing the compact JSON string directly. Instead of asserting on the raw
JSON string format, parse the JSON strings in the seen_results list before
comparison. For each tuple in seen_results, convert the first element (the JSON
string) to a parsed dictionary using json.loads() and compare the parsed objects
rather than the exact string formatting. This keeps the test stable across
formatter changes while still validating the actual JSON payload content.
🤖 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/tools/structured_tool.py`:
- Around line 72-80: In the exception handler where the validation warning is
raised, the warning message currently includes the full exception object using
{exc} which can expose sensitive data. Replace {exc} with
{exc.__class__.__name__} in the f-string on the line that constructs the warning
message (within the warnings.warn call) so that only the exception type name is
logged instead of the full exception details that may contain raw tool output,
secrets, or PII.

---

Nitpick comments:
In `@lib/crewai/tests/tools/test_tool_usage.py`:
- Around line 241-244: The test assertion at line 241-244 is coupling the test
to exact JSON string formatting by comparing the compact JSON string directly.
Instead of asserting on the raw JSON string format, parse the JSON strings in
the seen_results list before comparison. For each tuple in seen_results, convert
the first element (the JSON string) to a parsed dictionary using json.loads()
and compare the parsed objects rather than the exact string formatting. This
keeps the test stable across formatter changes while still validating the actual
JSON payload content.
🪄 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: 2fa4a316-135f-4588-aaa2-a75977ee5565

📥 Commits

Reviewing files that changed from the base of the PR and between 854c67d and 5fbab5a.

📒 Files selected for processing (20)
  • docs/edge/en/concepts/tools.mdx
  • docs/edge/en/guides/tools/publish-custom-tools.mdx
  • docs/edge/en/learn/create-custom-tools.mdx
  • docs/edge/en/learn/execution-hooks.mdx
  • docs/edge/en/learn/tool-hooks.mdx
  • lib/crewai/src/crewai/agents/crew_agent_executor.py
  • lib/crewai/src/crewai/agents/tools_handler.py
  • lib/crewai/src/crewai/experimental/agent_executor.py
  • lib/crewai/src/crewai/hooks/tool_hooks.py
  • lib/crewai/src/crewai/tools/base_tool.py
  • lib/crewai/src/crewai/tools/structured_tool.py
  • lib/crewai/src/crewai/tools/tool_usage.py
  • lib/crewai/src/crewai/utilities/agent_utils.py
  • lib/crewai/src/crewai/utilities/tool_utils.py
  • lib/crewai/tests/agents/test_native_tool_calling.py
  • lib/crewai/tests/hooks/test_tool_hooks.py
  • lib/crewai/tests/tools/test_base_tool.py
  • lib/crewai/tests/tools/test_structured_tool.py
  • lib/crewai/tests/tools/test_tool_usage.py
  • lib/crewai/tests/utilities/test_agent_utils.py

Comment thread lib/crewai/src/crewai/tools/structured_tool.py

@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: 1

🤖 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-tools/src/crewai_tools/tools/tavily_research_tool/tavily_research_tool.py`:
- Around line 90-92: The current logic in the output_schema handling block only
renames output_schema to tavily_output_schema when tavily_output_schema is not
already present, but fails to handle the case when both are provided. When both
output_schema and tavily_output_schema exist in kwargs, the legacy output_schema
dict remains in kwargs and causes BaseTool.__init__ to fail due to type
incompatibility. Modify the conditional logic to check if tavily_output_schema
exists in kwargs and remove the legacy output_schema in that case, then only
perform the remapping of output_schema to tavily_output_schema when
tavily_output_schema is absent from kwargs.
🪄 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: 2ac50565-192f-422e-9712-f4ddd7d2dd4b

📥 Commits

Reviewing files that changed from the base of the PR and between 5fbab5a and 7d83342.

📒 Files selected for processing (3)
  • docs/edge/en/tools/search-research/tavilyresearchtool.mdx
  • lib/crewai-tools/src/crewai_tools/tools/tavily_research_tool/tavily_research_tool.py
  • lib/crewai-tools/tool.specs.json

@vinibrsl
vinibrsl force-pushed the tools-schema branch 2 times, most recently from b0394d1 to 23b2df7 Compare June 19, 2026 21:04
vinibrsl added 8 commits June 19, 2026 14:10
Tools can now declare an `output_schema`, set explicitly or inferred
from a Pydantic return annotation. `format_output_for_agent` validates
the raw result against it and serializes to JSON for the agent, while
`run` keeps returning the raw value. Falls back to `str(raw_result)`
with a warning when validation or serialization fails.
Tools with an `output_schema` returned a Python repr to the agent
instead of clean JSON. Send every tool result through
`format_output_for_agent` so the agent reads valid JSON, across all
executors and `ToolUsage`.

The cache still stores the raw result, so cache callbacks keep getting
the original typed object.
Typed tools format their output to JSON before handing it to the agent,
so `tool_result` reaches after hooks as a string and the original Python
object is lost. Thread the unformatted result through as
`raw_tool_result` so hooks can inspect the typed value. This covers all
execution paths: native tool calls, the ReAct `ToolUsage` path, cached
results, and error/blocked branches.
When a `BaseTool` subclass overrides `format_output_for_agent`, route
the agent-facing text through it instead of the default JSON/`str()`
serialization. The structured tool wrapper now delegates to the original
tool via `_original_tool`, so a tool can present Markdown or any custom
representation to the agent while `tool.run(...)` still returns the raw
Python value.

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

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

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit f6c971d. Configure here.

Comment thread lib/crewai/src/crewai/utilities/tool_utils.py Outdated
@vinibrsl
vinibrsl merged commit 9db2d44 into main Jun 19, 2026
58 checks passed
@vinibrsl
vinibrsl deleted the tools-schema branch June 19, 2026 21:33
@Sourav-Nandy-ai

Sourav-Nandy-ai commented Jul 11, 2026

Copy link
Copy Markdown

Hi Vinicius,

I maintain agent-eval (agent_regress), an OSS stats-based regression harness for agent behavior. I tried to build a real before-and-after test of this PR using the tool-level runner in agent-eval (crewai_tool_runner() + schema_conformance_scorer()), and I found something worth flagging. As currently implemented, it does not actually see this change.

The crewai_tool_runner() calls tool.run() or tool._run() directly and scores the raw result. However, your result_schema validate-or-fallback logic lives one layer downstream in ToolUsage, which calls the new tool.format_output_for_agent(). The .run() and ._run() methods never touch this. I installed crewai==1.14.7 (pre-merge) and 1.15.0 (post-merge) and confirmed this empirically. Both versions produced identical raw tool output and identical schema-conformance scores, yielding zero detectable signal.

The PR itself works exactly as documented once I called format_output_for_agent() directly. A conforming dict round-trips to compact JSON, and a non-conforming one fires a real RuntimeWarning naming the tool and schema before falling back to str(). I confirmed this against both the happy and failure paths.

This is not a knock on the PR; it is a gap on my end. I am going to update crewai_tool_runner() to optionally route through format_output_for_agent() so that tool-level regression tests actually exercise this code path. I am flagging it here since #6236 is what surfaced this issue.

You can find the repository here: https://github.com/RudrenduPaul/agent-eval

Best,
Sourav

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants