TurfAITurfAI Developers
Guides

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_documents has something to retrieve. The agent's search_documents runs 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 both
const 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:

SymptomLikely causeFix
tools_used is emptyAgent answered from the model aloneSharpen the goal so it must use a tool; confirm available_tools is set
Wrong tool chosenToo many tools widen the search spaceTrim available_tools to the minimum the job needs
observe.status: "error" in the traceTool prerequisite missingFor 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 answersGoal too broad, or each tool result invites another callNarrow the goal; lower max_iterations so it commits to an answer
iteration_count == max_iterationsHit the cap before finishingRaise max_iterations (hard cap 20) or split into smaller agents/squad
Multi-turn agent forgets contextsession_id not stable across callsReuse 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: true and pass a stable session_id for multi-turn memory.
  • More tools. Add external capabilities with MCP.

Reference

On this page