Tools

September 20, 2026 · View on GitHub

Languages: English (current) · Português

A Tool gives "hands" to an agent: it lets the agent perform actions — calculate, query an API, read a file — during reasoning.

The interface

type Tool interface {
	Name() string
	Description() string
	Call(ctx context.Context, input string) (string, error)
}

Input and output are string to match the agent's text-based reasoning. If your tool needs structured arguments, document the expected format (e.g. a JSON object) in its Description.

Creating a tool from a function

weather := crewai.NewTool(
	"weather",
	"Looks up the weather for a city. Input: the city name.",
	func(ctx context.Context, city string) (string, error) {
		// call your API here...
		return fmt.Sprintf("Sunny in %s, 27°C", city), nil
	},
)

Built-in tools (the tools package)

import "github.com/rhgs/crewai-go/tools"

tools.Calculator()      // evaluates "2 + 2 * (3 - 1)" — offline and safe
tools.CurrentTime("")   // current date/time (time package layout; "" = RFC3339)
tools.WordCount()       // counts words and characters in the text

tools.NewHTTPFetch(tools.WithHTTPAllowlist("api.example.com"))
tools.NewFileRead("/var/data")
tools.NewFileWrite("/var/data", tools.WithAllowWrite())

Memory tools (recall_memory / remember) live on the root package: crewai.NewRecallMemoryTool(crew), crewai.NewRememberTool(crew), or Crew.EnableMemoryTools. See Memory.

HTTPFetch (SSRF-safe)

Deny-by-default: an empty allowlist rejects every request. Hosts are matched with path.Match (case-insensitive). Default method is GET. Redirects are capped at 3 hops and re-validated (scheme, userinfo, private/loopback/ link-local/CGNAT, DNS rebind). Response bodies are capped at crewai.MaxToolOutputBytes (1 MiB).

ht := tools.NewHTTPFetch(
    tools.WithHTTPAllowlist("api.example.com", "docs.*.internal"),
    tools.WithHTTPTimeout(30*time.Second),
    tools.WithHTTPMaxResponse(1<<20),
    tools.WithHTTPHeader("User-Agent", "crewai-go"),
)
out, err := ht.Call(ctx, "https://api.example.com/v1/status")

Never pass a model-controlled URL without an allowlist. Link-local / metadata hosts (169.254.169.254, metadata.google.internal) cannot be allowlisted.

Offline demo: examples/tools_http.

FileRead / FileWrite (directory jail)

Both tools resolve paths with the same symlink-aware jail as Task.OutputDir (EvalSymlinks, fail closed). Write is off until WithAllowWrite. Binary (NUL) reads are rejected unless WithAllowBinary.

read := tools.NewFileRead("/var/data")
write := tools.NewFileWrite("/var/data", tools.WithAllowWrite())

text, _ := read.Call(ctx, "/var/data/note.txt")
_, _ = write.Call(ctx, `{"path":"/var/data/out.txt","content":"saved"}`)

FileWrite input is JSON {"path","content"}. Files are written with mode 0600. Offline demo: examples/tools_files.

RAG stays a pattern, not a built-in tool — see docs/rag.md and examples/rag_file.

Attaching tools

To an agent (available for all its tasks):

agent.WithTools(tools.Calculator(), weather)

To a specific task (overrides the agent's tools for that task only):

task.Tools = []crewai.Tool{weather}

The ReAct protocol

When an agent has tools, it follows a Reasoning + Action cycle. The model is instructed to respond in this format:

Thought: I need to calculate the total
Action: calculator
Action Input: 1500 * 1.12

The framework runs the tool and returns:

Observation: 1680

The cycle repeats until the model concludes:

Thought: now I know the answer
Final Answer: The final value is \$1,680.00.

Robustness

  • If the model does not follow the protocol, the output is treated as the final answer (instead of stalling).
  • If the model asks for a non-existent tool, the framework returns an error Observation listing the valid tools, and the agent tries again.
  • Errors returned by a tool become an error Observation — the agent can react to them.
  • The loop stops at MaxIterations (default 15), returning ErrMaxIterations.

Best practices

  • Short names without spaces (web_search, not Web Search).
  • Clear descriptions stating what it does and what input is expected.
  • Tools should be idempotent when possible — the agent may call them more than once.
  • Respect context.Context (timeouts/cancellation) in network calls.

Facts & provenance

A Fact is a piece of data produced by a deterministic connector tool, not by the LLM. It always carries provenance (source organization, source URL, collection time, payload hash) so a wrong value can never be presented as a "fact the model remembered".

The Fact type

type Fact struct {
    Claim       string    `json:"claim"`
    SourceOrg   string    `json:"source_org"`
    SourceURL   string    `json:"source_url"`
    CollectedAt time.Time `json:"collected_at"`
    PayloadHash string    `json:"payload_hash"`
}

Making a tool a FactSource

A tool declares itself as a fact source by using NewFactSourceTool:

tool := crewai.NewFactSourceTool(
    "cnpj_lookup",
    "Looks up CNPJ status. Input: the CNPJ number.",
    func(_ context.Context, cnpj string) (string, error) {
        // call your API...
        return "Company X is ATIVA", nil
    },
    func(_ context.Context, output string) []crewai.Fact {
        return []crewai.Fact{
            crewai.NewFact(output, "Receita Federal",
                "https://api.receita.gov.br/v1/cnpj/...", []byte(rawPayload)),
        }
    },
)

After each successful Call, the executor collects the tool's Facts() and attaches them to TaskOutput.Facts and CrewOutput.Facts.

Rules

  • The LLM NEVER produces a Fact. Facts come only from FactSource tools.
  • Facts are deduplicated by PayloadHash (first occurrence kept).
  • Tools not implementing FactSource contribute zero facts.

Provenance guardrails

Use AllFactsProvenanced in a guardrail to enforce that every fact has SourceURL and PayloadHash:

crew.Guardrails = []crewai.Guardrail{
    func(_ context.Context, out *crewai.CrewOutput) error {
        return crewai.AllFactsProvenanced(out.Facts)
    },
}

If any fact lacks provenance, Kickoff returns ErrBlockedByGuardrail.

Web search (WebSearchTool)

The WebSearchTool is a Tool that searches the web via a pluggable SearchProvider and also implements FactSource, so results are collected as Facts with provenance. Use this in the ReAct loop when you want the LLM to decide when to search (model-driven pattern).

import "github.com/rhgs/crewai-go/tools"

// Default: Wikipedia (free, no API key).
tool := tools.NewWebSearch(tools.NewWikipediaSearch())
agent.WithTools(tool)

SearchProvider implementations

ProviderConstructorAPI keyNotes
Wikipedia (default)NewWikipediaSearch()Free, noneSearches Wikipedia articles only. NewWikipediaSearchWithLanguage("pt") to set language.
LangSearchNewLangSearch(apiKey)Free tier100% free. General web search.
SerpstackNewSerpstack(apiKey)Free 1000/monthGoogle SERP results.
DuckDuckGoNewDuckDuckGoSearch()Free, noneOptional provider. May be rate-limited or blocked by DuckDuckGo.
GoogleNewGoogleSearch(apiKey, cxID)RequiredGoogle Custom Search. Needs API key + CX ID.
BraveNewBraveSearch(apiKey)RequiredBrave Search API.

Provider examples

Wikipedia (default, free):

tool := tools.NewWebSearch(tools.NewWikipediaSearch())
// or with a specific language:
tool := tools.NewWebSearch(tools.NewWikipediaSearchWithLanguage("en"))

LangSearch (100% free):

tool := tools.NewWebSearch(tools.NewLangSearch(os.Getenv("LANGSEARCH_API_KEY")))

Serpstack (1000 requests/month free):

tool := tools.NewWebSearch(tools.NewSerpstack(os.Getenv("SERPSTACK_API_KEY")))

DuckDuckGo (optional, no API key):

tool := tools.NewWebSearch(tools.NewDuckDuckGoSearch())

Google Custom Search (requires API key + CX ID):

tool := tools.NewWebSearch(
	tools.NewGoogleSearch(os.Getenv("GOOGLE_API_KEY"), os.Getenv("GOOGLE_CSE_ID")),
)

Brave Search (requires API key):

tool := tools.NewWebSearch(tools.NewBraveSearch(os.Getenv("BRAVE_API_KEY")))

Options

tool := tools.NewWebSearch(
	tools.NewWikipediaSearch(),
	tools.WithMaxResults(10),             // default 5
	tools.WithSearchTimeout(60*time.Second), // default 30s
)

FactSource integration

WebSearchTool implements crewai.FactSource. After each successful Call, results are collected as Facts with source URL and payload hash, then attached to TaskOutput.Facts and CrewOutput.Facts:

crew.Guardrails = []crewai.Guardrail{
	func(_ context.Context, out *crewai.CrewOutput) error {
		return crewai.AllFactsProvenanced(out.Facts)
	},
}

SSRF protection

All URLs returned by any provider are filtered through SSRF (Server-Side Request Forgery) protection before being presented to the agent:

  • Non-http(s) schemes are blocked (file://, ftp://, etc.).
  • Userinfo (user:pass@host) is blocked — never useful for public search results and a common SSRF smuggling vector.
  • Loopback addresses (localhost, localhost.localdomain, 127.0.0.1, ::1) and bare metadata aliases (metadata, metadata.google.internal) are blocked by name.
  • Private, link-local, unspecified, multicast, and CGNAT (100.64.0.0/10, RFC 6598) IP addresses are blocked (e.g. 10.x, 192.168.x, 169.254.x — this covers cloud metadata endpoints like 169.254.169.254).
  • DNS rebinding prevention: domain names are resolved via DNS and all resolved IPs are checked. If any resolves to a blocked address, the URL is blocked.
  • Fail-closed: if DNS resolution fails or the host is empty, the URL is blocked by default.

Logging

Tools log via *log/slog. Two logs can leak sensitive content:

  • agent thought (Debug) - the entire LLM response before tool call.
  • tool invoked (Info) / native tool call (Info) - the tool input/args.

Recommendation: in production, keep the log level at LevelError or wrap the logger handler with a redactor. The crewai package provides redactError (used internally for delegation-failed warnings) and examples/logging/ shows a drop-in redaction handler that masks likely-secrets in all attributes before they reach the destination.