How to Build an Autonomous AI Agent with the Go SDK
In this tutorial, you'll learn how to build an autonomous AI agent using the ChatBotKit Go SDK. By the end, you'll have an agent that can call tools, handle multi-step tasks, and gives you full control over each iteration of the agentic loop-all with minimal code and dependencies.
What You'll Learn
By the end of this tutorial, you will be able to:
- Install and configure the ChatBotKit Go SDK
- Define custom tools that the AI can call
- Manually drive the agentic loop for full control
- Add observability, rate limiting, and custom logic between iterations
- Understand the architectural benefits of server-side AI processing
Why Build with the Go SDK?
The ChatBotKit Go SDK offers unique architectural advantages that make your AI agents lighter and more future-proof:
-
šŖ¶ Lightweight Agents: Complex AI processing happens server-side, not in your application. This means faster load times, smaller binaries, and simpler maintenance.
-
š”ļø Robust & Streamlined: Server-side processing provides built-in error handling, automatic retries, and consistent behavior across all platforms.
-
š Backward & Forward Compatible: As AI technology evolves-new models, new capabilities, new paradigms-your agents automatically benefit without code changes.
-
š® Future-Proof: Agents you build today remain capable tomorrow. When ChatBotKit adds support for new AI models or capabilities, your existing agents gain those powers without updates.
This means you can focus on building great experiences while ChatBotKit handles the complexity of the ever-changing AI landscape.
Prerequisites
Before starting, make sure you have:
- Go version 1.21 or later installed
- A ChatBotKit account with an API key
- Basic familiarity with Go programming
Estimated time: 15-20 minutes
Step 1: Set Up Your Project
Create a new directory for your agent and initialize a Go module:
mkdir my-agent
cd my-agent
go mod init my-agent
Install the ChatBotKit Go SDK:
go get github.com/chatbotkit/go-sdk
Optionally, install the godotenv package to load environment variables from a .env file:
go get github.com/joho/godotenv
Set your API key as an environment variable:
export CHATBOTKIT_API_SECRET="your-api-key-here"
Tip: You can get your API key from the ChatBotKit Dashboard under Settings ā API Keys.
Step 2: Create Your Agent
Create a file named main.go with the following code. This agent demonstrates how to manually drive the agentic loop, giving you full control over each iteration:
package main
import (
"context"
"fmt"
"os"
"github.com/chatbotkit/go-sdk/agent"
"github.com/chatbotkit/go-sdk/sdk"
"github.com/joho/godotenv"
)
func main() {
godotenv.Load()
apiSecret := os.Getenv("CHATBOTKIT_API_SECRET")
if apiSecret == "" {
fmt.Fprintln(os.Stderr, "Error: CHATBOTKIT_API_SECRET environment variable is not set")
os.Exit(1)
}
client := sdk.New(sdk.Options{Secret: apiSecret})
// Define simple tools that the AI can call
tools := agent.Tools{
"get_weather": {
Description: "Get the current weather for a location",
Parameters: &agent.Parameters{
Properties: map[string]agent.Property{
"location": {
Type: "string",
Description: "The city and state, e.g. San Francisco, CA",
},
},
Required: []string{"location"},
},
// Return mock weather data
Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
location := args["location"].(string)
return map[string]interface{}{
"location": location,
"temperature": 72,
"conditions": "sunny",
"humidity": 45,
}, nil
},
},
"get_time": {
Description: "Get the current time for a timezone",
Parameters: &agent.Parameters{
Properties: map[string]agent.Property{
"timezone": {
Type: "string",
Description: "The timezone, e.g. America/Los_Angeles",
},
},
Required: []string{"timezone"},
},
// Return mock time data
Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
timezone := args["timezone"].(string)
return map[string]interface{}{
"timezone": timezone,
"time": "2:30 PM",
"date": "Monday, January 26, 2026",
}, nil
},
},
}
// Initial message that requires multiple tool calls
messages := []agent.Message{
{Type: "user", Text: "What is the weather in San Francisco and what time is it in Los Angeles?"},
}
maxIterations := 10 // Safety limit to prevent infinite loops
iterationCount := 0
fmt.Println("Starting manually-driven agentic loop...")
fmt.Println("User:", messages[0].Text)
fmt.Println("---")
// The manual agentic loop
for iterationCount < maxIterations {
iterationCount++
fmt.Printf("\n[Iteration %d]\n", iterationCount)
// Run a single iteration with MaxIterations=1
// This ensures we get control back after each agentic step
events, errs := agent.ExecuteWithTools(context.Background(), client, agent.ExecuteWithToolsOptions{
Model: "claude-4.5-sonnet",
Messages: messages,
Tools: tools,
MaxIterations: 1, // Return after each iteration
})
var exitCode int
var exitMessage string
var responseText string
hasExited := false
// Process events from this iteration
for event := range events {
switch e := event.(type) {
case agent.TokenAgentEvent:
fmt.Print(e.Token)
responseText += e.Token
case agent.ResultAgentEvent:
responseText = e.Text
case agent.ToolCallStartEvent:
fmt.Printf("\nš§ Calling %s...\n", e.Name)
case agent.ToolCallEndEvent:
fmt.Printf(" ā %s returned\n", e.Name)
case agent.ToolCallErrorEvent:
fmt.Printf(" ā %s error: %s\n", e.Name, e.Error)
case agent.AgentExitEvent:
hasExited = true
exitCode = e.Code
exitMessage = e.Message
}
}
// Check for errors
if err := <-errs; err != nil {
fmt.Fprintf(os.Stderr, "\nError: %v\n", err)
os.Exit(1)
}
// Here you could add custom logic between iterations:
// - Log to a monitoring system
// - Check for custom termination conditions
// - Add delays for rate limiting
// - Persist intermediate state
// - Transform or filter the response
// Check if the agent signaled completion via exit
if hasExited {
if exitCode == 0 {
fmt.Printf("\nā Agent completed successfully")
if exitMessage != "" {
fmt.Printf(": %s", exitMessage)
}
fmt.Println()
} else {
fmt.Printf("\nā Agent exited with code %d: %s\n", exitCode, exitMessage)
}
break
}
// Add bot response to messages for next iteration
if responseText != "" {
messages = append(messages, agent.Message{
Type: "bot",
Text: responseText,
})
}
// Add continuation prompt
messages = append(messages, agent.Message{
Type: "user",
Text: "Continue. If you are done, call the exit function.",
})
fmt.Println("\nā Iteration limit hit, continuing manually...")
}
if iterationCount >= maxIterations {
fmt.Println("\nā ļø Reached maximum iteration limit")
}
fmt.Println("\n---")
fmt.Println("Agentic loop complete!")
}
Step 3: Understanding the Architecture
This agent demonstrates a powerful pattern for agentic AI applications.
The Manual Agentic Loop
By setting MaxIterations: 1, you gain control after each agentic step:
// Manual agentic loop with iteration control
for iterationCount < maxIterations {
events, errs := agent.ExecuteWithTools(ctx, client, agent.ExecuteWithToolsOptions{
// ...
MaxIterations: 1, // Return after each iteration
})
// Process events
// Add custom logic
// Decide whether to continue
}
This pattern enables:
- Observability: Log, monitor, or audit each step
- Custom logic: Inject logic between iterations
- Rate limiting: Add delays between API calls
- Early termination: Custom conditions to stop the loop
- State persistence: Save state between iterations
Defining Tools
Tools are defined with a description, parameters, and a handler function:
tools := agent.Tools{
"tool_name": {
Description: "What this tool does",
Parameters: &agent.Parameters{
Properties: map[string]agent.Property{
"param1": {
Type: "string",
Description: "Description of this parameter",
},
},
Required: []string{"param1"},
},
Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
// Execute the tool and return a result
return map[string]interface{}{"result": "value"}, nil
},
},
}
Processing Events
The agent emits various events during execution:
| Event | Description |
|---|---|
TokenAgentEvent | Streaming text tokens |
ResultAgentEvent | Complete response text |
ToolCallStartEvent | Tool execution starting |
ToolCallEndEvent | Tool execution completed |
ToolCallErrorEvent | Tool execution failed |
AgentExitEvent | Agent signaled completion |
Step 4: Run Your Agent
Start your agent:
go run main.go
You should see output similar to:
Starting manually-driven agentic loop...
User: What is the weather in San Francisco and what time is it in Los Angeles?
---
[Iteration 1]
š§ Calling get_weather...
ā get_weather returned
š§ Calling get_time...
ā get_time returned
The weather in San Francisco is sunny with a temperature of 72°F and 45% humidity.
The current time in Los Angeles is 2:30 PM on Monday, January 26, 2026.
ā Agent completed successfully
---
Agentic loop complete!
Step 5: Using Default Tools (Optional)
For file and shell operations, the SDK provides built-in tools:
// Get the default tools (read, write, edit, exec)
tools := agent.DefaultTools()
events, errs := agent.ExecuteWithTools(ctx, client, agent.ExecuteWithToolsOptions{
Model: "claude-4.5-sonnet",
Messages: []agent.Message{{Type: "user", Text: "Create a hello.txt file"}},
Backstory: "You are an autonomous task executor.",
Tools: tools,
MaxIterations: 20,
})
| Tool | Description |
|---|---|
read | Read file contents with optional line ranges |
write | Write or modify file contents |
edit | Replace exact string occurrences in files |
exec | Execute shell commands with timeout |
System Tools
ExecuteWithTools automatically includes three system tools:
- plan: Create or update a task execution plan
- progress: Track completed steps and current status
- exit: Exit the execution with a status code
Step 6: Add a Backstory (Optional)
Define your agent's personality and behavior with a backstory:
backstory := `You are a helpful assistant that can check the weather and time.
## Your Capabilities
- Get current weather conditions for any city
- Get the current time in any timezone
## Your Behavior
- Always provide clear, concise answers
- When asked about multiple things, gather all information before responding
- Format temperatures in Fahrenheit with the °F symbol`
events, errs := agent.ExecuteWithTools(ctx, client, agent.ExecuteWithToolsOptions{
Model: "claude-4.5-sonnet",
Messages: messages,
Backstory: backstory,
Tools: tools,
// ...
})
Troubleshooting
Authentication Errors
If you see Error: 401 Unauthorized, verify that:
- Your
CHATBOTKIT_API_SECRETenvironment variable is set correctly - The API key has not expired
- You're using the correct API key from your account
Tool Execution Errors
If tools fail to execute:
- Check the handler function returns the correct types
- Ensure required parameters are provided
- Handle errors gracefully in your handler
Go Module Issues
If you encounter module errors:
go mod tidy
This resolves most dependency issues.
Next Steps
Now that you have a working autonomous agent, consider exploring:
- More SDK examples including chatbots and file operations
- Building interactive chatbots for conversational interfaces
- Integrating with messaging platforms like Slack, Discord, or WhatsApp
- Creating multi-agent systems for complex workflows
- Using the Terraform provider for infrastructure-as-code deployments
The Go SDK makes it easy to build powerful, autonomous AI agents with minimal code. Because the heavy lifting happens on ChatBotKit's servers, your agents stay lean, maintainable, and automatically gain new capabilities as the platform evolves.