{"slug": "gemini-for-go-developers-building-agentic-backends-with-genkit", "title": "Gemini for Go Developers: Building Agentic Backends with Genkit", "summary": "A developer guide details rebuilding AIDA, an AI Diagnostic Agent that queries operating systems via osquery, as an agentic backend using Google's Genkit Go framework, replacing its original SQLite-based RAG architecture and \"vibe coding\" codebase. The guide instructs developers to install the genkit CLI via the official script at cli.genkit.dev and run programs with `genkit start -- go run .` to access the Genkit Developer UI at http://localhost:4000 for testing flows, prompts, and agents and inspecting traces for prompts, tool calls, token usage, and latency. The rewritten AIDA compiles to a single binary that runs as a standalone app or a deployable web server.", "body_md": "In the [previous chapter of Gemini for Go Developers](https://danicat.dev/posts/gemini-for-go-developers-part-3-building-agents/), we explored how to build an agent in Go using three different libraries: the [GenAI SDK](https://pkg.go.dev/google.golang.org/genai), [Genkit](https://genkit.dev), and [ADK](https://adk.dev). We kept that agent simple on purpose so we could compare the ergonomics of each development style side by side. Now it is time to go deeper into each framework.\n\nIn this article, we will focus on building agentic backends with Genkit. We will cover the development process end to end, from setting up the development environment to deploying your Genkit app to the cloud. We also need to touch on the theory behind the core elements of Genkit (e.g. flows, tools, prompts, plugins, middleware, etc.), but to ensure we stay grounded in what really matters we are going to pair all these concepts with a practical application. For this purpose we are going to revisit and modernise AIDA, the AI Diagnostic Agent that I introduced in this blog last year.\n\n## Revisiting AIDA: the AI Diagnostic Agent[#](#revisiting-aida-the-ai-diagnostic-agent)\n\nAIDA was the [first agent I ever created](https://danicat.dev/posts/diagnostic-agent-uss-enterprise/). Its purpose is to diagnose computer problems by querying the operating system via an open-source tool called [osquery](https://osquery.io/). AIDA is a bit more than a year old now, and even though it went through a few updates over the months, I never really questioned its architecture until today.\n\nWhen AIDA was created, models were at a different level of capability and techniques like agent skills had not been invented yet. To improve response quality, I had to inject on-demand schema and query knowledge into the agent context using a [rudimentary RAG solution powered by SQLite](https://danicat.dev/posts/building-aida-part-2/). It’s funny how something that not so long ago used to be cool and “state of the art” now feels like old and clunky. It was not only the RAG though, the entire codebase of AIDA was hacked instead of engineered. Even worse, everything was done with early stage “vibe coding”.\n\nI have been saying this for over a decade, but now it is more true than ever: if your codebase is holding you back, nuke it and start over. There is something about starting over after enduring the growth pains of a codebase that always makes v2 (or N+1) better: you learn what not to do, you optimise, you simplify. The best part? Nowadays you can do three versions of something before lunch. In the past a rewrite used to take months, and a lot of political capital.\n\nRewriting AIDA in Genkit will not only give us an opportunity to bring her up to modern standards, but also massively improve the maintainability and readability of the code. AIDA will compile to a single binary that can work as a standalone app or a web server that can be deployed to the cloud.\n\n## Setting up your coding agent and local workflow[#](#setting-up-your-coding-agent-and-local-workflow)\n\nBefore writing any application code we need two things: the local `genkit` CLI for the development UI and command line utilities, and the Agent Skills that teach your coding agent how Genkit Go works.\n\n### Genkit CLI and Dev UI[#](#genkit-cli-and-dev-ui)\n\nTo install the `genkit` CLI run the official install script:\n\n```\ncurl -sL cli.genkit.dev | bash\n```\n\nThe `genkit` CLI is the backbone of local development in Genkit Go. If you run your program directly with `go run .`, it works as a normal Go binary, but by prefixing your run command with `genkit start --` you will have access to the **Genkit Developer UI** (defaults to `http://localhost:4000`) where you can visually inspect and test the components of your Genkit program individually:\n\n```\ngenkit start -- go run .\n```\n\nIn the Dev UI, you can test registered flows, prompts, and agents interactively and inspect traces for prompts, tool calls, token usage, and latency.\n\nBesides the Dev UI, the `genkit` CLI also provides command line helpers for executing flows, visualising traces and browsing the documentation. This last one is particularly useful to prevent model hallucination during the implementation phase.\n\nHere are some examples:\n\n```\n# Run a specific flow once from the terminal and exit (--stream or --wait optional)\ngenkit flow:run diagnose '{\"userQuestion\": \"how is my disk usage?\"}' --non-interactive -- go run .\n\n# Browse and read the embedded Genkit Go documentation offline\ngenkit docs:list go\ngenkit docs:read go/middleware.md\n```\n\n### Recommended skills[#](#recommended-skills)\n\nThe gold standard for SDKs nowadays is to ship them with official agent skills and Genkit is no different. If you are developing a Genkit Go program this is the one you are looking for:\n\n- [`developing-genkit-go`](https://github.com/genkit-ai/skills) : the official Genkit Go skill covering flows, dotprompt, built-in middleware, and experimental agents.\n\nSince we are developing with Gemini models, it is also useful to install the `gemini-api-dev` skill:\n\n- [`gemini-api-dev`](https://github.com/google-gemini/gemini-skills) : the official Gemini skill for current model IDs and thinking configuration.\n\nYou can install them into your workspace using the Vercel [`skills` CLI](https://github.com/vercel-labs/skills):\n\n```\nnpx skills add genkit-ai/skills --skill developing-genkit-go\nnpx skills add google-gemini/gemini-skills --skill gemini-api-dev\n```\n\nOr register the catalogs with [`kungfu`](https://github.com/danicat/kungfu) for just-in-time (JIT) loading:\n\n```\nkungfu catalog add genkit-ai/skills\nkungfu catalog add google-gemini/gemini-skills\n```\n\nIf you prefer using an MCP server instead of skills, the CLI also ships an [MCP server](https://genkit.dev/docs/go/mcp-server/) (`genkit mcp`). These days I personally have been leaning more towards skills, but MCPs still have value for certain niche use cases (e.g. documentation retrieval).\n\n## Core concepts[#](#core-concepts)\n\nGenkit is an open-source framework for generative AI applications originally created by the Firebase team before growing into its own project. While its original language is JS, Genkit Go feels surprisingly idiomatic for seasoned Go developers, even if it still has a few rough edges here and there. The typical Genkit program won’t be much different than a traditional web service, thanks to adapters like `genkit.Handler` and familiar constructs like middleware and strongly typed flows and prompts.\n\n### Flows[#](#flows)\n\nThe fundamental unit in Genkit is a **flow**: a strongly typed function that wraps generative AI calls along with any pre- or post-processing logic. Let’s start AIDA as a single-shot diagnostic flow equipped with a tool to query the operating system via `osqueryi`:\n\n```\ntype AIDARequest struct {\n\tOS           string `json:\"os,omitempty\" jsonschema_description:\"The target operating system (defaults to runtime.GOOS)\"`\n\tUserQuestion string `json:\"userQuestion\" jsonschema_description:\"The diagnostic question to investigate\"`\n}\n\ntype AIDAResponse struct {\n\tMessage string `json:\"message\" jsonschema_description:\"The diagnostic findings and recommendations\"`\n}\n\ntype OsqueryInput struct {\n\tQuery string `json:\"query\" jsonschema_description:\"The SQL query to execute in osquery.\"`\n}\n\ng := genkit.Init(ctx,\n\tgenkit.WithPlugins(&googlegenai.VertexAI{}),\n\tgenkit.WithDefaultModel(\"vertexai/gemini-3.8-flash\"),\n)\n\nrunOsquery := genkit.DefineTool(g, \"runOsquery\", \"Runs a system inspection query using osquery.\",\n\tfunc(ctx *ai.ToolContext, in OsqueryInput) (string, error) {\n\t\tout, err := exec.CommandContext(ctx, \"osqueryi\", \"--json\", in.Query).CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"osqueryi failed: %w: %s\", err, out)\n\t\t}\n\t\treturn string(out), nil\n\t},\n)\n\ndiagnoseFlow := genkit.DefineFlow(\n\tg,\n\t\"diagnose\",\n\tfunc(ctx context.Context, req AIDARequest) (AIDAResponse, error) {\n\t\tif req.OS == \"\" {\n\t\t\treq.OS = runtime.GOOS\n\t\t}\n\t\tresp, err := genkit.Generate(ctx, g,\n\t\t\tai.WithSystem(fmt.Sprintf(\"You are the Emergency Diagnostic Agent for diagnosing %s operating system failures. Your name is AIDA.\", req.OS)),\n\t\t\tai.WithPrompt(req.UserQuestion),\n\t\t\tai.WithTools(runOsquery),\n\t\t)\n\t\tif err != nil {\n\t\t\treturn AIDAResponse{}, fmt.Errorf(\"diagnose flow failed: %w\", err)\n\t\t}\n\t\treturn AIDAResponse{Message: resp.Text()}, nil\n\t},\n)\n```\n\n`genkit.DefineTool` registers the `runOsquery` tool using the `OsqueryInput` struct to generate its JSON schema, while `genkit.DefineFlow` registers the flow by name with a callback function. If you look at the callback signature, it feels very similar to a typed HTTP handler — it takes a `context.Context` and a strongly typed request struct, and returns a typed response alongside a standard Go `error`.\n\nNotice the `g` variable passed as the first argument to `DefineTool`, `DefineFlow`, and `Generate`. `g` is a `*genkit.Genkit` instance, which acts as the central registry for all flows, prompts, tools, models, and middleware in your program. You initialise `g` once in `main` with `genkit.Init` and pass it explicitly.\n\nI’ll be honest, I’m not a huge fan of `genkit.WithDefaultModel`, as I prefer code to be explicit, but it does help reducing a bit of boilerplate if you are defining many flows. We are going to see a better option to hardcoding models in flows when discussing prompts.\n\n`genkit.Init` sets up both the runtime registry and the observability stack. Every model and tool call inside a flow is automatically recorded as a child span under the flow’s trace. If your flow also performs custom pre- or post-processing — like querying a database or calling an external API — wrapping that logic in `genkit.Run` records it as its own named sub-step in the trace waterfall.\n\nFor development purposes you can run a flow via the CLI or in the Dev UI. In production, you may invoke flows as a regular Go function call (e.g. `myFlow.Run`), or exposed as an HTTP endpoint. Turning a flow into an HTTP handler takes a single line with `genkit.Handler`:\n\n```\nmux := http.NewServeMux()\nmux.HandleFunc(\"POST /diagnose\", genkit.Handler(diagnoseFlow))\n\nport := os.Getenv(\"PORT\")\nif port == \"\" {\n\tport = \"3400\"\n}\n\n// Note: \"server\" requires the server plugin:\n// import \"github.com/firebase/genkit/go/plugins/server\"\nlog.Fatal(server.Start(ctx, \"0.0.0.0:\"+port, mux))\n```\n\nIf you want to expose every flow registered in your application at once, you can iterate over `genkit.ListFlows(g)`:\n\n```\nmux := http.NewServeMux()\nfor _, flow := range genkit.ListFlows(g) {\n\tmux.HandleFunc(\"POST /\"+flow.Name(), genkit.Handler(flow))\n}\nlog.Fatal(server.Start(ctx, \"0.0.0.0:\"+port, mux))\n```\n\nPlease note that `genkit.Handler` performs no authentication of its own, so wrap public endpoints in your standard `net/http` auth middleware.\n\n### Prompts[#](#prompts)\n\nHardcoding prompts and model configuration in the flow code works for small projects, but it can get hard to maintain them as the project grows. Even if coding agents are doing most of the heavy work these days, we should not throw away years of engineering best practices, and code organisation is one of them.\n\nGenkit has support for prompt templates using a format called [dotprompt](https://genkit.dev/docs/go/dotprompt/) (`*.prompt` files). Prompts in `dotprompt` may contain more than just the instructions for the model; for example, they may include metadata for selecting and configuring the model, schema definitions, system instructions, and conversation history.\n\nExtracting AIDA’s prompt, tools, and model configuration out of `diagnoseFlow` into `prompts/aida.prompt` looks like this:\n\n```\n---\nmodel: vertexai/gemini-3.8-flash\ntools:\n  - runOsquery\nconfig:\n  thinkingConfig:\n    thinkingLevel: low\ninput:\n  schema:\n    os: string\n    userQuestion: string\noutput:\n  format: json\n  schema:\n    message: string\n---\n{{role \"system\"}}\nYou are the Emergency Diagnostic Agent for diagnosing {{os}} operating system failures. Your name is AIDA.\n\n{{role \"user\"}}\n{{userQuestion}}\n```\n\nBy default, `genkit.Init` automatically loads `.prompt` files from `./prompts` in your working directory. If you prefer to distribute your agent as a single binary, you can embed your `prompts` directory directly into the binary with `//go:embed` and pass it to `genkit.WithPromptFS`:\n\n``` js\nimport \"embed\"\n\n//go:embed prompts/*\nvar promptsFS embed.FS\n\nfunc main() {\n\tg := genkit.Init(context.Background(), genkit.WithPromptFS(promptsFS))\n\t// ...\n}\n```\n\nYou can look up prompts in code via `genkit.LookupPrompt` and invoke them with `myPrompt.Execute`:\n\n```\naida := genkit.LookupPrompt(g, \"aida\")\nresp, err := aida.Execute(ctx, ai.WithInput(map[string]any{\n\t\"os\":           runtime.GOOS,\n\t\"userQuestion\": \"how is my disk usage?\",\n}))\n```\n\nIf you have been writing Go for a while, that `map[string]any` probably bothers you as much as it bothers me. Fortunately, Genkit lets us bind the `AIDARequest` and `AIDAResponse` structs we defined earlier directly to `aida.prompt` using **data prompts** (`genkit.LookupDataPrompt`) and `genkit.DefineSchemaFor`:\n\n```\n// In prompts/aida.prompt, reference the Go types by name:\n//\n// input:\n//   schema: AIDARequest\n// output:\n//   schema: AIDAResponse\n//\n// Register both schemas with Genkit so the prompt file can resolve them:\ngenkit.DefineSchemaFor[AIDARequest](g)\ngenkit.DefineSchemaFor[AIDAResponse](g)\n\naida := genkit.LookupDataPrompt[AIDARequest, *AIDAResponse](g, \"aida\")\nresp, _, err := aida.Execute(ctx, AIDARequest{\n\tOS:           runtime.GOOS,\n\tUserQuestion: \"how is my disk usage?\",\n})\n// resp is of type *AIDAResponse\n// Note: The second return value is the raw *ai.ModelResponse\n```\n\nWith `DefineSchemaFor` and `LookupDataPrompt`, you define your schema once in Go and keep full compile-time type safety from input to parsed output.\n\n### Plugins[#](#plugins)\n\nYou extend Genkit with packages under the `plugins/` namespace. While the most common case for a plugin is to register a model family, we also have plugins for broader capabilities like `server` and `middleware`. `server` is a special case in the sense it doesn’t need to be registered to be useful, but most plugins will be initialised in the `genkit.Init` call:\n\n```\nimport (\n\t\"github.com/firebase/genkit/go/genkit\"\n\t\"github.com/firebase/genkit/go/plugins/googlegenai\"\n\t\"github.com/firebase/genkit/go/plugins/middleware\"\n)\n\ng := genkit.Init(ctx,\n\tgenkit.WithPlugins(\n\t\t&googlegenai.VertexAI{},  // Gemini via Vertex AI\n\t\t&middleware.Middleware{}, // Registers Retry, Fallback, Skills, etc.\n\t),\n)\n```\n\nThe concept of middleware should be familiar to most Go developers, but it is important to single out this plugin as it is the gateway for critical capabilities like fallbacks, retries and agent skills. Let’s have a deeper look at it next.\n\n### Middleware and Agent Skills[#](#middleware-and-agent-skills)\n\nAIDA can already execute `osquery` commands through the `runOsquery` tool we defined earlier, but it still relies on the model’s general training data to guess which tables and columns exist on each operating system (`darwin`, `linux`, and `windows`). To give AIDA accurate schema knowledge, we will be providing one skill for each OS in the `./skills` folder. These skills were generated by giving Antigravity the link to the [osquery repo](https://github.com/osquery/osquery) and asking it to generate operating system specific skills based on the table availability. The skills will include the tables and respective schemas.\n\nGenkit attaches middleware to generation calls using `ai.WithUse`, and the `plugins/middleware` package provides built-in implementations for retries, model fallbacks, and [Agent Skills](https://agentskills.io):\n\n```\nresp, err := genkit.Generate(ctx, g,\n\tai.WithSystem(fmt.Sprintf(\"You are AIDA, a %s operating system diagnostic assistant.\", runtime.GOOS)),\n\tai.WithPrompt(\"My machine is running out of file descriptors. How do I inspect open files with osquery?\"),\n\tai.WithTools(runOsquery),\n\tai.WithUse(\n\t\t&middleware.Retry{MaxRetries: 3, InitialDelayMs: 1000, BackoffFactor: 2},\n\t\t&middleware.Fallback{\n\t\t\tModels: []ai.ModelRef{\n\t\t\t\tgooglegenai.ModelRef(\"vertexai/gemini-3.5-flash-lite\", nil),\n\t\t\t},\n\t\t},\n\t\t&middleware.Skills{\n\t\t\tSkillPaths: []string{\"./skills\"},\n\t\t},\n\t),\n)\n```\n\nLike HTTP middleware, `ai.WithUse` composes in order: placing `Retry` before `Fallback` retries the primary model before falling back to the secondary model.\n\nWhen you point `&middleware.Skills{}` at `./skills`, Genkit scans each subdirectory, injects the skill descriptions into the system prompt, and registers a `use_skill` tool so AIDA can load the right OS schema and query patterns on demand.\n\nPlease note that, unlike `dotprompt`, at the moment the `Skills` middleware does not support embedding filesystems, so you still need to resort to bundling a skills directory into your deployment.\n\nBecause we registered `&middleware.Middleware{}` during `genkit.Init`, we can also declare these middlewares directly inside `prompts/aida.prompt`:\n\n```\n---\nmodel: vertexai/gemini-3.8-flash\ntools:\n  - runOsquery\nuse:\n  - name: genkit-middleware/retry\n    config:\n      maxRetries: 2\n  - genkit-middleware/skills\n---\n```\n\nFor advanced use cases, you can also write custom middleware by implementing the `ai.Middleware` interface to intercept and wrap generation, model, or tool calls with your own logic.\n\n### The developer UI[#](#the-developer-ui)\n\nYou can find the complete code up to this point in the companion repository under [`github.com/danicat/gemini-for-go-developers/part-4/01-flow`](https://github.com/danicat/gemini-for-go-developers/tree/main/part-4/01-flow).\n\nTo test AIDA in the Genkit Developer UI, launch the application from that directory with `genkit start`:\n\n```\nexport GOOGLE_CLOUD_PROJECT=your-project-id\nexport GOOGLE_CLOUD_LOCATION=global # required for Gemini 3.x\ngenkit start -- go run .\n```\n\nOpen `http://localhost:4000`, select the **diagnose** flow from the sidebar, and run it with the following test payload:\n\n```\n{\n  \"userQuestion\": \"what is my battery status?\"\n}\n```\n\nHere is the output of this prompt on my machine (observe the trace on the right panel showing that the skill was activated):\n\n## Stateful agents (experimental)[#](#stateful-agents-experimental)\n\nI typically like to stay clear of experimental features, but this one is an exception. While it’s perfectly possible to implement agents based on flows alone, developing more complex patterns — like chatbots, serial agents and parallel agents — does take a bit of effort crafting the right patterns using goroutines, synchronisation, error handling and others.\n\nThe introduction of the agent abstraction simplifies this process by providing us with a much more ergonomic API focused on the declarative aspect of agent creation instead.\n\n### Defining an agent[#](#defining-an-agent)\n\nBecause the API is still experimental, you need to enable it with `genkit.WithExperimental()` when initialising Genkit. Here is how we turn AIDA into a stateful, multi-turn terminal agent backed by a session store:\n\n```\nimport (\n\taix \"github.com/firebase/genkit/go/ai/exp\"\n\t\"github.com/firebase/genkit/go/ai/exp/localstore\"\n\tgenkitx \"github.com/firebase/genkit/go/genkit/exp\"\n)\n\ng := genkit.Init(ctx,\n\tgenkit.WithExperimental(),\n\tgenkit.WithPlugins(&googlegenai.VertexAI{}, &middleware.Middleware{}),\n)\n\nstore := localstore.NewInMemorySessionStore[struct{}]()\n\naida := genkitx.DefineAgent(g, \"aida\",\n\taix.InlinePrompt{\n\t\tai.WithModelName(\"vertexai/gemini-3.8-flash\"),\n\t\tai.WithSystem(fmt.Sprintf(\"You are AIDA, a %s operating system diagnostic assistant. Use runOsquery to inspect the host.\", runtime.GOOS)),\n\t\tai.WithTools(runOsquery),\n\t\tai.WithUse(&middleware.Skills{SkillPaths: []string{\"./skills\"}}),\n\t},\n\taix.WithSessionStore(store),\n)\n\n// Interactive terminal session maintaining state across turns\nscanner := bufio.NewScanner(os.Stdin)\nvar sessionID string\n\nfmt.Print(\"AIDA> \")\nfor scanner.Scan() {\n\tinput := strings.TrimSpace(scanner.Text())\n\tif input == \"\" || input == \"exit\" {\n\t\tbreak\n\t}\n\n\tvar opts []aix.InvocationOption[struct{}]\n\tif sessionID != \"\" {\n\t\topts = append(opts, aix.WithSessionID[struct{}](sessionID))\n\t}\n\n\tout, err := aida.RunText(ctx, input, opts...)\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"error: %v\\n\", err)\n\t\tfmt.Print(\"AIDA> \")\n\t\tcontinue\n\t}\n\n\tsessionID = out.SessionID\n\tfmt.Printf(\"\\n%s\\n\\nAIDA> \", out.Message.Text())\n}\n```\n\nStoring `out.SessionID` and passing it into subsequent turns preserves the conversation history across the session. The `[struct{}]` type parameter simply tells Genkit that we only want to persist the message history without attaching custom session state.\n\nThe same techniques used to improve the code around flows can be used with agents. To avoid hardcoding the prompts, you can use `genkitx.DefinePromptAgent` to bind `prompts/aida.prompt` directly to the agent.\n\nTo expose AIDA over HTTP so a remote frontend can connect to it, `genkitx.AllAgentRoutes` mounts turn, snapshot, and abort endpoints onto standard Go routers:\n\n```\nmux := http.NewServeMux()\nfor _, route := range genkitx.AllAgentRoutes(g) {\n\tmux.HandleFunc(route.Pattern(), route.Handler())\n}\n```\n\nYou can find the complete stateful agent implementation in the companion repository under [`github.com/danicat/gemini-for-go-developers/part-4/02-agent`](https://github.com/danicat/gemini-for-go-developers/tree/main/part-4/02-agent).\n\n### Multi-agent delegation[#](#multi-agent-delegation)\n\nThere are cases where you will need more than one agent to perform a task. Benefits of multi-agent architecture include separation of concerns — to improve agent attention and reduce context rot — and parallelisation — to improve response times.\n\nUsing the experimental `middlewarex.Agents` middleware in Genkit, we can split AIDA’s investigative work between two specialists while keeping `aida` as the main coordinator persona:\n\n- **`inspector`** : Focuses exclusively on querying host telemetry via` runOsquery` and the OS-specific`osquery` skills.\n- **`researcher`** : Uses Google Search grounding to look up unfamiliar processes, binaries, error messages, and CVEs on the web without bloating AIDA’s main conversation history.\n\n```\n// requires:\n// import (\n// \t\"google.golang.org/genai\"\n// \tmiddlewarex \"github.com/firebase/genkit/go/plugins/middleware/exp\"\n// )\n\ninspector := genkitx.DefineAgent(g, \"inspector\",\n\taix.InlinePrompt{\n\t\tai.WithModelName(\"vertexai/gemini-3.8-flash\"),\n\t\tai.WithSystem(fmt.Sprintf(\"Inspect %s system telemetry using osquery and report diagnostic findings.\", runtime.GOOS)),\n\t\tai.WithTools(runOsquery),\n\t\tai.WithUse(\n\t\t\t&middleware.Retry{MaxRetries: 5},\n\t\t\t&middleware.Skills{SkillPaths: []string{\"./skills\"}},\n\t\t),\n\t},\n\taix.WithDescription[struct{}](\"Inspects host metrics, running processes, and system configuration using osquery.\"),\n)\n\nresearcher := genkitx.DefineAgent(g, \"researcher\",\n\taix.InlinePrompt{\n\t\tai.WithModelName(\"vertexai/gemini-3.8-flash\"),\n\t\tai.WithSystem(\"Research unfamiliar processes, binaries, error messages, and CVEs using Google Search and report concise findings.\"),\n\t\tai.WithConfig(&genai.GenerateContentConfig{\n\t\t\tTools: []*genai.Tool{\n\t\t\t\t{GoogleSearch: &genai.GoogleSearch{}},\n\t\t\t},\n\t\t}),\n\t},\n\taix.WithDescription[struct{}](\"Searches the web for information on unfamiliar processes, binaries, error codes, and CVEs.\"),\n)\n\naida := genkitx.DefineAgent(g, \"aida\",\n\taix.InlinePrompt{\n\t\tai.WithModelName(\"vertexai/gemini-3.8-flash\"),\n\t\tai.WithSystem(fmt.Sprintf(\"You are AIDA, a %s operating system diagnostic assistant. Delegate host inspection to inspector and web research to researcher, then synthesize findings for the user.\", runtime.GOOS)),\n\t\tai.WithUse(\n\t\t\t&middleware.Retry{MaxRetries: 5},\n\t\t\t&middlewarex.Agents{\n\t\t\t\tAgents:         []aix.AgentRef{inspector.Ref(), researcher.Ref()},\n\t\t\t\tMaxDelegations: 5,\n\t\t\t},\n\t\t),\n\t},\n\taix.WithSessionStore(store),\n)\n```\n\nDelegation runs synchronously by default, though you can set `Async: true` on `middlewarex.Agents` if you want AIDA to kick off long-running specialist tasks in the background and join their results across turns.\n\nOne thing to keep in mind is the latency trade-off. When I timed the exact same `\"what is my battery status?\"` query against both versions (with `thinkingLevel` set to `low`), the single-agent version (` 02-agent`) finished in **9.56s** across 3 LLM calls, while this multi-agent version (`03-multi-agent`) took **17.02s** across 5 LLM calls. The per-call latency is basically identical (~3.3s), so the extra ~7.5 seconds come purely from the two extra coordinator round-trips as `aida` delegates to `inspector` and then summarises its report.\n\nMuch like in parallel programming, the coordination overhead for a small *N* can make a simple task slower. For a toy query, a single agent is faster; in a real-world system, however, splitting an “uber-agent” into focused sub-agents is often worth those extra round-trips to isolate noisy tool outputs, optimise context usage, and improve overall response quality.\n\nYou can find the complete multi-agent example in the companion repository under [`github.com/danicat/gemini-for-go-developers/part-4/03-multi-agent`](https://github.com/danicat/gemini-for-go-developers/tree/main/part-4/03-multi-agent).\n\n## Deploying to production[#](#deploying-to-production)\n\n### Runtimes[#](#runtimes)\n\nBecause a Genkit Go application compiles to a standard Go binary using `net/http`, there is no proprietary runtime lock-in. This means that basically any platform you use to host your HTTP servers is fair game for Genkit apps.\n\nMy first preference is always a serverless container runtime. At Google, I use primarily **Google Cloud Run**, only choosing more complex platforms (like **Google Kubernetes Engine (GKE)** or **Gemini Enterprise**) when I hit a critical blocker that requires a feature that Cloud Run doesn’t give to me.\n\nIt is also worth saying that in the past these types of decisions used to be more important than they are today. Replatforming took a lot of effort, so people had the tendency of wanting to “get it right” from the beginning. With coding agents we can adapt an app to a new platform in a matter of days (if not hours), so there are no more excuses to add complexity early to a project.\n\nThis is the theory. In practice, over the last year I never found a case that I couldn’t do with Cloud Run. It is simply that convenient! Things will be a bit different when we discuss ADK in a future article, as the Gemini Enterprise ecosystem does have some synergies with ADK that are worth paying attention to.\n\nNow, typically agents — especially Gemini-based ones like Antigravity — don’t need help deploying to Cloud Run. But if your agent is being grumpy, you can always install the Cloud Run skill from [github.com/google/skills](https://github.com/google/skills/tree/main/skills/cloud/cloud-run-basics) to give it a nudge.\n\n### Observability[#](#observability)\n\nDuring local development, `genkit start` records full execution traces and structured logs in `.genkit/traces` (which you should add to `.gitignore`) so you can inspect them in the Dev UI. In production, you want that same visibility into token usage, latency, and step-by-step traces in your monitoring backend.\n\nSince Genkit is built on [OpenTelemetry](https://opentelemetry.io/), you have two main options: if you are deploying to Google Cloud, the `googlecloud` plugin (or the `firebase` plugin, which runs the same exporter under the hood) sends everything to Cloud Logging, Cloud Trace, and Cloud Monitoring. If your team uses another backend like Datadog, Grafana, or Honeycomb, you can attach a standard OTLP exporter directly to the Genkit tracer provider instead.\n\nFor a typical Cloud Run deployment, enable the Google Cloud exporter before calling `genkit.Init`:\n\n```\nimport \"github.com/firebase/genkit/go/plugins/googlecloud\"\n\nfunc main() {\n\tctx := context.Background()\n\n\tgooglecloud.EnableGoogleCloudTelemetry(&googlecloud.GoogleCloudTelemetryOptions{\n\t\t// Raw prompts and responses are logged by default; disable this for sensitive data:\n\t\tDisableLoggingInputAndOutput: true,\n\t\t// Set to true if you want to test Cloud export locally under `genkit start`:\n\t\tForceDevExport: false,\n\t})\n\n\tg := genkit.Init(ctx,\n\t\t// if you are deploying to GCP, auth via Gemini Enterprise (aka Vertex AI) is preferred\n\t\tgenkit.WithPlugins(&googlegenai.VertexAI{}),\n\t)\n\t// ...\n}\n```\n\nIf you prefer an OTLP backend instead, register a standard OpenTelemetry span processor before `genkit.Init` and `defer` its shutdown function:\n\n```\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"github.com/firebase/genkit/go/core/tracing\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n)\n\nfunc registerOTLP(ctx context.Context) func(context.Context) error {\n\t// Configured via standard OTEL_EXPORTER_OTLP_* environment variables.\n\texp, err := otlptracegrpc.New(ctx)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to build OTLP exporter: %v\", err)\n\t}\n\n\ttp := tracing.TracerProvider()\n\ttp.RegisterSpanProcessor(sdktrace.NewBatchSpanProcessor(exp))\n\treturn tp.Shutdown\n}\n```\n\n## Conclusions[#](#conclusions)\n\nTo be honest, while this exercise of rebuilding AIDA allowed us to touch most of the core functionality of Genkit, this is far from being the ultimate deep dive I was planning to do. That said, the problem is of the good kind: Genkit has so many interesting features that it is impossible to talk about all of them in a single article. This is why I decided to focus this article on the backend aspect of agent design, and I reserved frontend with A2UI and interaction design for the next article.\n\nNevertheless, this article not only showcases what Genkit is capable of, but it also shows us how the industry itself evolved in the past 12 months or so. What previously required a custom RAG pipeline, an embedding model and hundreds of lines of Python code of questionable quality (this is not a criticism of Python, it is criticism of my own code), now can be solved with type-safe Go and a call to the agent skills middleware. I particularly love how snappy it feels to have a compiled agent, especially when paired with a fast model like Gemini 3.8 Flash.\n\nI don’t want to make this a Python versus Go piece, and won’t try to convince you to use one or the other, but if you are planning to build agents in Go, for whatever reason it might be, I highly recommend Genkit. For all the reasons you saw in this article, plus the ones I will cover next time, and all the others I haven’t even discovered yet. :)", "url": "https://wpnews.pro/news/gemini-for-go-developers-building-agentic-backends-with-genkit", "canonical_source": "https://danicat.dev/posts/building-agentic-backends-with-genkit-go/?utm_source=rss&utm_medium=feed&utm_campaign=rss_reader", "published_at": "2026-10-06 00:00:00+00:00", "updated_at": "2026-10-06 21:47:22.337956+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "generative-ai", "ai-products"], "entities": ["Genkit", "AIDA", "Go", "osquery", "Genkit Developer UI", "GenAI SDK", "ADK", "danicat.dev"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/gemini-for-go-developers-building-agentic-backends-with-genkit", "markdown": "https://wpnews.pro/news/gemini-for-go-developers-building-agentic-backends-with-genkit.md", "text": "https://wpnews.pro/news/gemini-for-go-developers-building-agentic-backends-with-genkit.txt", "jsonld": "https://wpnews.pro/news/gemini-for-go-developers-building-agentic-backends-with-genkit.jsonld"}}