Tool Boundaries and Structured Output

Learning objectives

You will define parameter, return, authority, side-effect, and error contracts for CrewAI tools; understand the current official BaseTool/@tool shape; and use Task Pydantic/JSON output and guardrails to establish contracts between Agents.

A tool is first an authority interface

A tool turns a model suggestion into a data read or external action. Each Agent receives only tools needed by its current Task: a researcher can read local sources, a writer consumes structured research results, and a reviewer should not hold publication authority.

Every tool contract needs at least:

FieldQuestion
Name/descriptionWhen may it be used, and when may it not?
Parameter schemaWhat are its types, ranges, normalization, and rejection conditions?
Return schemaHow are success, empty result, and error distinguished?
AuthorityWhich identity is used and which resources are reachable?
Side effectRead-only, reversible, or irreversible?
TimeoutDoes timeout mean failure or unknown commit state?
RetryWhich categories are retryable, and with what budget?
IdempotencyHow is one business action recognized?
AuditWhich digest is retained, and which secrets are never logged?

Parameters produced by a model must pass execution-layer validation. A Tool description helps select a Tool; it grants no authority.

Current official custom-Tool shape

The official Tools page presents two primary approaches: subclass BaseTool with Pydantic args_schema, or decorate a function with @tool. The following is a minimal shape for reading; it was not run in this course’s real installed environment:

from typing import Type  # Declare a type for the args_schema class object.
from crewai.tools import BaseTool  # Import the CrewAI base class for custom tools.
from pydantic import BaseModel, Field  # Define and constrain tool input with Pydantic.
 
class LocalSearchInput(BaseModel):  # Accept only type-checked, length-checked parameters.
    topic: str = Field(min_length=1, max_length=120)  # Reject empty or unusually long topics.
 
class LocalSearchTool(BaseTool):  # Implement a read-only local-search tool with a narrow boundary.
    name: str = "search_approved_local_sources"  # Use one action name rather than an ambiguous general-purpose tool.
    description: str = "Search only the approved local source catalog."  # State the permitted data boundary for the model.
    args_schema: Type[BaseModel] = LocalSearchInput  # Bind the schema so runtime validates input first.
 
    def _run(self, topic: str) -> str:  # Define the synchronous execution entry; production also needs authorization and audit.
        return search_catalog(topic)  # Call only an application-controlled catalog search.

search_catalog is your controlled implementation. Define a schema before returning a JSON string, and do not leak filesystem paths, credentials, or server stacks through exceptions. The official page also shows synchronous/asynchronous tools and cache_function, but cache only known-reusable reads—not payments, sends, or other side-effecting results.

Async tools do not change commit semantics

The current Tools page supports async def functions decorated by @tool and asynchronous _run. That prevents one coroutine from blocking while it waits for network or file I/O; it does not prove that a remote request did not complete after a timeout or cancellation. For a write action, cancellation, disconnection, or timeout means unknown_commit. Query the receipt by idempotency key before retrying rather than assuming a cancelled function is safe to resend.

Concurrency also amplifies shared quotas, contention for the same resource, and logging correlation problems. Every call should carry a stable operation ID, deadline, and audit correlation ID.

Separate read and write tools

Split a high-risk action into:

  1. preview_publish: read-only; produces the normalized action, target, and diff.
  2. The Flow stores the action fingerprint and asks for approval.
  3. publish_report: verifies trusted approval, target, and idempotency ID.
  4. get_publication_receipt: queried during recovery to learn whether commitment occurred.

Do not let a Tool search Task text for the words “approved.” An approval record must come from controlled state or an authorization service.

Structured Task output

The official Tasks documentation lists:

  • output_pydantic=Model: TaskOutput.pydantic contains a model instance;
  • output_json=Model: TaskOutput.json_dict contains JSON output;
  • the default guarantees only raw;
  • guardrail can validate a Task output before it enters a downstream step.

Conceptual example:

from pydantic import BaseModel  # Import the base model for structured output.
from crewai import Task  # Import Task, which declares an output contract.
 
class Claim(BaseModel):  # Represent a research statement that requires traceable evidence.
    text: str  # Store the statement text; factual correctness still needs external checking.
    source_ids: list[str]  # Store stable identifiers for sources supporting the statement.
 
class ResearchResult(BaseModel):  # Define the complete structured result of a research Task.
    claims: list[Claim]  # Collect all sourced statement objects.
    unknowns: list[str]  # Preserve evidence gaps explicitly instead of letting the model invent conclusions.
 
task = Task(  # Create a research Task accepted through a Pydantic model.
    description="Extract claims only from approved sources.",  # Bound the allowed evidence.
    expected_output="Claims with source IDs plus explicit unknowns.",  # Add natural-language requirements for the model.
    output_pydantic=ResearchResult,  # Ask the framework to parse the result into the model above.
)

Whether a real version permits omitted agent, the exact guardrail signature, and retry behavior must be verified against the pinned version. Structural validity does not prove factual correctness: still check that every source_id exists and that source text supports the claim.

What a guardrail should do

Good deterministic guardrails include:

  • JSON/Pydantic types;
  • required fields, lengths, and enumerations;
  • existence of cited IDs;
  • prohibited tools or targets;
  • whether a file path stays under an allowed root;
  • iteration or cost-budget exhaustion.

Style and argument quality may use a model grader, but fix its rubric and calibrate it against human samples. A review Agent that lacks the same source as the producing Agent is not inherently more trustworthy.

Tool error categories

Return stable categories such as invalid_input, not_found, permission_denied, transient, permanent, and unknown_commit. Retry only transient failure within a finite budget. A permission or policy rejection must not be bypassed by swapping Agents. For unknown commitment, query the receipt first.

{
  "ok": false,
  "error": {
    "category": "permission_denied",
    "retryable": false,
    "message": "caller cannot publish to this target"
  }
}
  • ok is the machine-readable success switch; downstream code must not treat a failed call as normal output.
  • error.category is the stable class that determines retry or human escalation.
  • error.retryable avoids guessing policy from a natural-language error message.
  • error.message explains the failure to a caller; production must omit credentials, paths, and internal stacks.

External content is untrusted data

Web pages, documents, MCP responses, and Knowledge fragments can contain prompt injection. The Tool layer must constrain paths, network targets, SQL/command parameters, and identity; a model cannot use instructions found in a document to expand its allowlist. When Shell or code execution is necessary, use an isolated service and disposable environment rather than directly executing model-generated strings on the host.

Common mistakes and diagnosis

  • All Agents share one Tool set: build a least-privilege allowlist per Task.
  • Only checking that JSON parses: also check field semantics, sources, and business constraints.
  • Giving natural-language errors to a retryer: adapt them into finite categories.
  • Automatically caching/retrying a write: use a business idempotency ID and receipt.
  • Letting a model choose any output path: normalize it and ensure it lies under the allowed root.
  • Copying credential-placeholder conventions from an official example: real values come only from the environment or a secret service.

Exercise

For a research Crew, design search_local_sources, read_source, preview_publish, and publish_report:

  1. write parameter and return schemas;
  2. identify each Agent’s Tool allowlist;
  3. design approval, action fingerprint, and idempotency receipt for publication;
  4. write eight failure samples, including path escape, unknown source, permission denial, and commit timeout;
  5. explain which checks belong to a Task guardrail and which must execute in the Tool service.

Mastery check

  • Explain the difference between a Tool description and execution authorization.
  • Recognize the current official BaseTool, args_schema, and @tool shapes.
  • Constrain Task artifacts with output_pydantic/output_json.
  • Validate schema, sources, and business semantics together.
  • Design preview, approval, idempotency, and receipts for a write Tool.

Next step

Continue to Memory, Knowledge, and Context to decide which information belongs to which lifecycle layer.

References