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
Observationlisting 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), returningErrMaxIterations.
Best practices
- Short names without spaces (
web_search, notWeb 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
| Provider | Constructor | API key | Notes |
|---|---|---|---|
| Wikipedia (default) | NewWikipediaSearch() | Free, none | Searches Wikipedia articles only. NewWikipediaSearchWithLanguage("pt") to set language. |
| LangSearch | NewLangSearch(apiKey) | Free tier | 100% free. General web search. |
| Serpstack | NewSerpstack(apiKey) | Free 1000/month | Google SERP results. |
| DuckDuckGo | NewDuckDuckGoSearch() | Free, none | Optional provider. May be rate-limited or blocked by DuckDuckGo. |
NewGoogleSearch(apiKey, cxID) | Required | Google Custom Search. Needs API key + CX ID. | |
| Brave | NewBraveSearch(apiKey) | Required | Brave 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 like169.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.