Building a Garden Automation Agent with ADK Go 1.0
Google just dropped ADK Go 1.0. I learned it by building a garden automation agent.
Why I Built This
I've been meaning to experiment with automating my garden watering for a while. Nothing fancy, just something that checks the conditions and tells me whether to water or not. When Google dropped ADK Go 1.0 literally few days ago, I figured: why not learn a new framework by solving a real problem? I'm a senior software engineer, mostly TypeScript and AWS in my day job and I'd never written a line of Go before this. That's kind of the point. This post is my honest account of building something real with ADK Go 1.0 from scratch, including the errors, the wrong package paths, and eventually, a working agent.
What is ADK Go?
ADK (Agent Development Kit) is Google's open-source framework for building production-grade AI agents. The key word is production-grade, this isn't a chatbot wrapper. ADK is designed for systems where agents call real tools, make real decisions, and need to be observable and debuggable. The Go version just hit 1.0, which brings:
YAML agent config - iterate on agent behaviour without recompiling
Native OpenTelemetry (OTel) - every tool call and model decision becomes a structured trace
Plugin system - retry-and-reflect, logging, and more out of the box
Human-in-the-loop (HITL) - flag sensitive actions for confirmation before execution
A2A protocol - multi-agent communication across Go, Java, and Python agents
For Go developers specifically, ADK feels natural. It's idiomatic Go structs, interfaces, and context propagation; not a port of a Python API bolted onto Go syntax.
What the Agent Does
The garden agent does three things:
Checks soil moisture via a sensor tool (mocked for now. Part 2 wires a real ESP32)
Fetches the Melbourne rain forecast from the Open-Meteo API (free, no key required)
Reasons over both readings and outputs a clear WATER or SKIP recommendation
The decision logic lives in the agent's instruction, not hardcoded in the tool which means you can adjust the thresholds in plain English without touching the Go code.
Project Setup Prerequisites
Go 1.21+
A Gemini API key (free at aistudio.google.com)
mkdir garden-agent
cd garden-agent
go mod init github.com/suharsha/garden-agent
go get google.golang.org/[email protected]
go get github.com/joho/godotenv
Project Structure
Secrets management - never export your API key in plain shell. Use a .env file and godotenv to load it, and make sure .env is in your .gitignore before your first commit. I've seen too many tutorials skip this step.
#.env
GOOGLE_API_KEY=your_key_here
Building the Tools
In ADK Go, tools are typed Go functions wrapped with functiontool.New. The framework automatically infers the JSON schema from your struct types, no manual schema definition needed.
One thing that tripped me up: the handler must take tool.Context (from google.golang.org/adk/tool), not context.Context. ADK injects its own context type which carries session state, confirmation hooks, and tracing. The compiler error message which looks like below isn't obvious about this, so I'm saving you 20 minutes.
Tool 1 - Soil Moisture (Mocked)
// tools/soil_sensor.go
type SoilReading struct {
MoisturePercent float64
Status string
}
func GetSoilMoisture(ctx context.Context) (*SoilReading, error) {
moisture := 35.0
status := classifyMoisture(moisture)
fmt.Printf("š± Soil moisture reading: %.1f%% (%s)\n", moisture, status)
return &SoilReading{MoisturePercent: moisture, Status: status}, nil
}
func classifyMoisture(moisture float64) string {
switch {
case moisture < 20:
return "critical ā immediate watering needed"
case moisture < 40:
return "low ā watering recommended"
case moisture < 70:
return "optimal"
default:
return "saturated ā no watering needed"
}
}
The classifyMoisture helper is intentional. I want deterministic thresholds in code, not vibes-based reasoning from the model. The agent decides what to do, not what the reading means.
Tool 2 - Rain Forecast (Live)
// tools/weather.go
type WeatherForecast struct {
RainfallMM float64
Description string
}
func GetRainForecast(ctx context.Context) (*WeatherForecast, error) {
url := "https://api.open-meteo.com/v1/forecast" +
"?latitude=-37.840935&longitude=144.946457" +
"&daily=precipitation_sum" +
"&forecast_days=1" +
"&timezone=Australia%2FSydney"
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch weather data: %w", err)
}
defer resp.Body.Close()
var result struct {
Daily struct {
PrecipitationSum []float64 `json:"precipitation_sum"`
} `json:"daily"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to parse weather response: %w", err)
}
if len(result.Daily.PrecipitationSum) == 0 {
return nil, fmt.Errorf("no precipitation data returned")
}
rainfall := result.Daily.PrecipitationSum[0]
description := describeRainfall(rainfall)
fmt.Printf("š¦ Rain forecast for Melbourne: %.1fmm (%s)\n", rainfall, description)
return &WeatherForecast{
RainfallMM: rainfall,
Description: description,
}, nil
}
func describeRainfall(mm float64) string {
switch {
case mm == 0:
return "no rain expected"
case mm < 2:
return "light rain ā may not be enough"
case mm < 5:
return "moderate rain ā watering probably not needed"
default:
return "heavy rain ā skip watering"
}
}
Open-Meteo is genuinely great for this, free, no API key, returns clean JSON. The Melbourne coordinates are -37.840935, 144.946457.
Testing the Tools
Before wiring up the agent I tested both tools in isolation:
cd tools && go test ./... -v
=== RUN TestGetSoilMoisture
š± Soil moisture reading: 35.0% (low ā watering recommended)
--- PASS: TestGetSoilMoisture (0.00s)
=== RUN TestGetRainForecast
š¦ Rain forecast for Melbourne: 0.0mm (no rain expected)
--- PASS: TestGetRainForecast (1.19s)
Real data - 0mm of rain forecast for Melbourne today. Perfect conditions for testing a watering agent.
Wiring Up the Agent
Here's where ADK Go earns its keep. You define the agent with typed tool bindings. The framework handles LLM orchestration, tool dispatch, and response aggregation.
// agent.go
type SoilArgs struct{}
type WeatherArgs struct{}
func buildGardenAgent(ctx context.Context) (agent.Agent, error) {
soilTool, err := functiontool.New(functiontool.Config{
Name: "get_soil_moisture",
Description: "Returns the current soil moisture reading from the garden sensor.",
}, func(ctx adktool.Context, args SoilArgs) (*tools.SoilReading, error) {
return tools.GetSoilMoisture(ctx)
})
weatherTool, err := functiontool.New(functiontool.Config{
Name: "get_rain_forecast",
Description: "Returns the 24-hour rainfall forecast for Melbourne in millimetres.",
}, func(ctx adktool.Context, args WeatherArgs) (*tools.WeatherForecast, error) {
return tools.GetRainForecast(ctx)
})
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
return llmagent.New(llmagent.Config{
Name: "garden_agent",
Model: model,
Instruction: `You are a garden automation agent for a home in Melbourne, Australia.
Your job is to decide whether the garden needs watering today.
Follow this logic:
1. Always check soil moisture first using get_soil_moisture
2. Always check the rain forecast using get_rain_forecast
3. Make a watering recommendation based on both readings:
- If rainfall forecast is 5mm or more ā skip watering, rain will handle it
- If soil moisture is above 70% ā skip watering, soil is saturated
- If soil moisture is between 40-70% and no rain ā light watering suggested
- If soil moisture is below 40% and no rain ā watering recommended
- If soil moisture is below 20% ā urgent, flag for immediate attention
4. Always explain your reasoning clearly
5. End with a clear recommendation: WATER or SKIP`,
Tools: []adktool.Tool{soilTool, weatherTool},
})
if err != nil {
return nil, fmt.Errorf("failed to create agent: %w", err)
}
log.Println("Garden agent built successfully")
return gardenAgent, nil
}
Notice the empty SoilArgs and WeatherArgs structs - functiontool.New is a generic function that needs to infer its type parameters. Even tools with no arguments need a struct. This is one of those things that's obvious in hindsight but cost me a compiler error or two.
Running It
go run .
šæ Garden Agent starting up...
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š Prompt: Please check the garden conditions and tell me if I need to water today.
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š± Soil moisture reading: 35.0% (low ā watering recommended)
š¦ Rain forecast for Melbourne: 0.0mm (no rain expected)
The soil moisture is currently at 35%, and there is no rain
expected today (0mm forecast). Since the soil moisture is below
40% and no rain is expected, watering is recommended.
Recommendation: WATER
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā
Garden Agent done.
The agent called both tools, reasoned over the results, and gave a clear recommendation. The whole thing took about 2 seconds.
What I Learned
ADK Go 1.0 is surprisingly mature for a 1.0. The API is clean, the error messages are reasonable, and the fact that tool schemas are inferred from Go types rather than defined manually saves a lot of boilerplate.
The tool.Context vs context.Context distinction is the biggest gotcha. The compiler error when you get this wrong (cannot infer TArgs and TResults) doesn't point directly at the cause. Now you know.
Separating classification logic from the agent instruction is good practice. The classifyMoisture function in the tool keeps deterministic thresholds in code. The agent's job is to reason and decide, not to interpret raw numbers.
Go is more approachable than I expected. Coming from TypeScript, the strict typing felt familiar. The biggest adjustment was the error handling pattern. Returning (value, error) pairs everywhere instead of throwing exceptions.
What's Next
Part 2 - Wire a real ESP32 capacitive soil moisture sensor. Replace the mocked tool with a real HTTP endpoint. The agent code stays the same, only the tool implementation changes.
Part 3 - Deploy to AWS. Containerise the agent, run it on ECS Fargate, schedule it with EventBridge, and pipe the OTel traces to CloudWatch. Yes, on AWS, not Google Cloud Run. Because that's where my expertise lives and someone needs to write that tutorial.
Code
Full source on GitHub: github.com/suharsha/garden-agent
I'm building things in public and writing about what I find. Follow along at suharsha.dev.

