Build an agent with tools
Create a ReAct agent that searches your knowledge base and emails answers.
What you'll build
A support agent that, given a question, searches your knowledge base and emails the answer — deciding for itself which tools to call. You'll create it, call it over the public chat endpoint, give it conversational memory, embed it in a workflow, and debug it from the trace. Read Agents first for the ReAct model.
Prerequisites
- A TurfAI JWT for the create call:
Authorization: Bearer $TURFAI_JWT. - Base URL
https://apisandbox.turfai.in/api. - A knowledge base with indexed documents so
search_documentshas something to retrieve. The agent'ssearch_documentsruns RAG over the owning user's indexed content.
export TURFAI_JWT="eyJhbGci…"
export BASE="https://apisandbox.turfai.in/api"1. Create the agent
Give it a clear goal, the smallest set of available_tools, and sensible limits. The create
response includes the public ag_ key — save it.
curl -X POST "$BASE/agents" \
-H "Authorization: Bearer $TURFAI_JWT" \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "Support Answerer",
"goal": "Answer the user question using the knowledge base, then email the answer to {{recipient}}.",
"available_tools": ["search_documents", "send_email"],
"model": "vertex",
"max_iterations": 8,
"temperature": 0.2
}
}'import os, requests
base = os.environ["BASE"]
r = requests.post(
f"{base}/agents",
headers={"Authorization": f"Bearer {os.environ['TURFAI_JWT']}"},
json={
"data": {
"name": "Support Answerer",
"goal": "Answer the user question using the knowledge base, then email the answer to {{recipient}}.",
"available_tools": ["search_documents", "send_email"],
"model": "vertex",
"max_iterations": 8,
"temperature": 0.2,
}
},
)
agent = r.json()["data"]
print(agent["slug"], agent["api_key"]) # save bothconst base = process.env.BASE!;
const res = await fetch(`${base}/agents`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TURFAI_JWT}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
data: {
name: "Support Answerer",
goal: "Answer the user question using the knowledge base, then email the answer to {{recipient}}.",
available_tools: ["search_documents", "send_email"],
model: "vertex",
max_iterations: 8,
temperature: 0.2,
},
}),
});
const { data: agent } = await res.json();
console.log(agent.slug, agent.api_key); // save both{ "data": { "id": 42, "slug": "support-answerer", "api_key": "ag_…", "…": "…" } }Save the slug and the ag_ api_key (export AGENT_KEY="ag_…").
2. The ReAct loop
When invoked, the agent reasons, calls a tool, observes, and repeats until it can answer:
3. Call the agent
Over the public chat endpoint with the ag_ key in the X-Agent-Key header:
curl -X POST "$BASE/agents/support-answerer/public-chat" \
-H "X-Agent-Key: $AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "What is our refund window? Email it to ops@example.com." }'import os, requests
base = os.environ["BASE"]
r = requests.post(
f"{base}/agents/support-answerer/public-chat",
headers={"X-Agent-Key": os.environ["AGENT_KEY"]},
json={"query": "What is our refund window? Email it to ops@example.com."},
)
out = r.json()
print(out["answer"])
print(out["tools_used"]) # ["search_documents", "send_email"]const base = process.env.BASE!;
const res = await fetch(`${base}/agents/support-answerer/public-chat`, {
method: "POST",
headers: {
"X-Agent-Key": process.env.AGENT_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "What is our refund window? Email it to ops@example.com.",
}),
});
const out = await res.json();
console.log(out.answer);
console.log(out.tools_used); // ["search_documents", "send_email"]{
"answer": "Refunds are accepted within 30 days of purchase. I've emailed this to ops@example.com.",
"trace": "Thought: I should search the KB for refund policy → Action: search_documents → …",
"tools_used": ["search_documents", "send_email"],
"session_id": "sess-9f2"
}trace and tools_used show the loop — your main debugging signal (see
Troubleshooting below).
4. Make it conversational
Pass a stable session_id on every call. The agent loads the prior turns and answers in
context — no need to resend earlier messages. (Set conversational: true on the agent for
full memory + context resolvers.)
# Turn 1
curl -X POST "$BASE/agents/support-answerer/public-chat" \
-H "X-Agent-Key: $AGENT_KEY" -H "Content-Type: application/json" \
-d '{ "query": "What is our refund window?", "session_id": "sess-42" }'
# Turn 2 — same session_id; "it" resolves from memory
curl -X POST "$BASE/agents/support-answerer/public-chat" \
-H "X-Agent-Key: $AGENT_KEY" -H "Content-Type: application/json" \
-d '{ "query": "Does it cover digital goods too?", "session_id": "sess-42" }'import os, requests
base, key = os.environ["BASE"], os.environ["AGENT_KEY"]
url = f"{base}/agents/support-answerer/public-chat"
h = {"X-Agent-Key": key}
sid = "sess-42"
requests.post(url, headers=h, json={"query": "What is our refund window?", "session_id": sid})
# Turn 2 reuses the same session_id — the agent remembers the topic
r = requests.post(url, headers=h, json={"query": "Does it cover digital goods too?", "session_id": sid})
print(r.json()["answer"])const base = process.env.BASE!;
const url = `${base}/agents/support-answerer/public-chat`;
const headers = { "X-Agent-Key": process.env.AGENT_KEY!, "Content-Type": "application/json" };
const session_id = "sess-42";
await fetch(url, { method: "POST", headers,
body: JSON.stringify({ query: "What is our refund window?", session_id }) });
// Turn 2 reuses the same session_id — the agent remembers the topic
const r = await fetch(url, { method: "POST", headers,
body: JSON.stringify({ query: "Does it cover digital goods too?", session_id }) });
console.log((await r.json()).answer);5. Use it inside a workflow
Reference the saved agent from an agent_task node; inline fields override the saved
defaults:
{
"id": "answer-1",
"type": "agent_task",
"data": {
"label": "Answer question",
"task_type": "agent_task",
"config": {
"agent_id": 42,
"goal": "Answer {{question}} and email {{recipient}}.",
"available_tools": ["search_documents", "send_email"],
"max_iterations": 6
}
}
}The node returns answer, reasoning_trace, tools_used, and iteration_count for
downstream steps. See Workflows & activities.
6. Troubleshooting
Start from trace / tools_used, then check the table:
| Symptom | Likely cause | Fix |
|---|---|---|
tools_used is empty | Agent answered from the model alone | Sharpen the goal so it must use a tool; confirm available_tools is set |
| Wrong tool chosen | Too many tools widen the search space | Trim available_tools to the minimum the job needs |
observe.status: "error" in the trace | Tool prerequisite missing | For search_documents: index docs first. For send_email: valid to/subject/body. For extract_from_document: a reachable file_url + correct mime_type |
| Agent loops, never answers | Goal too broad, or each tool result invites another call | Narrow the goal; lower max_iterations so it commits to an answer |
iteration_count == max_iterations | Hit the cap before finishing | Raise max_iterations (hard cap 20) or split into smaller agents/squad |
| Multi-turn agent forgets context | session_id not stable across calls | Reuse the same session_id; set conversational: true |
max_iterations is capped at 20 server-side. If a task genuinely needs more steps,
it's a sign to split the work across narrower agents or a squad rather than one long loop.
Tips
- One job per agent. Compose multiple narrow agents (or a squad) rather than one do-everything agent.
- Fewest tools. Each extra tool widens the search space and slows the loop.
- Conversational mode. Set
conversational: trueand pass a stablesession_idfor multi-turn memory. - More tools. Add external capabilities with MCP.
Reference
- Concept + full config table: Agents
- Multi-agent teams: Build a squad
- External tools: Connect MCP servers
- Endpoints + contracts: Agent API