←back to tutorials

How to Build an Autonomous AI Agent with the Go SDK

Learn how to create an autonomous AI agent using the ChatBotKit Go SDK. Build agents that execute tools, handle multi-step tasks, and give you full control over each iteration-all with minimal code and dependencies.

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:

  1. Observability: Log, monitor, or audit each step
  2. Custom logic: Inject logic between iterations
  3. Rate limiting: Add delays between API calls
  4. Early termination: Custom conditions to stop the loop
  5. 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:

EventDescription
TokenAgentEventStreaming text tokens
ResultAgentEventComplete response text
ToolCallStartEventTool execution starting
ToolCallEndEventTool execution completed
ToolCallErrorEventTool execution failed
AgentExitEventAgent 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,
})
ToolDescription
readRead file contents with optional line ranges
writeWrite or modify file contents
editReplace exact string occurrences in files
execExecute 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_SECRET environment 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:

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.