Genkit Go
AI SDK for Go • LLM Framework • AI Agent Toolkit
Build production-ready AI-powered applications in Go with a unified interface for text generation, structured output, tool calling, and agentic workflows.
One interface over Google AI and Vertex AI (Gemini), Anthropic (Claude), OpenAI (GPT), xAI (Grok), DeepSeek, DashScope (Qwen), Moonshot (Kimi), Z.ai (GLM), Vertex AI Model Garden (Llama, Mistral), Ollama for local models, and any OpenAI-compatible endpoint.
Documentation • API Reference • Discord
Building with a coding agent? Install the Genkit Go skill first.
npx skills add genkit-ai/skills --skill developing-genkit-goIt teaches your agent the current Genkit Go APIs and common gotchas. Source, manual install and skills for other languages: genkit-ai/skills.
go get github.com/firebase/genkit/goGet up and running in under a minute:
package main
import (
"context"
"fmt"
"log"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
)
func main() {
ctx := context.Background()
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
answer, err := genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Why is Go a great language for AI applications?"),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(answer)
}export GEMINI_API_KEY="your-api-key"
go run main.goEach sample runs with go run .. Start with the basic-* set: together they cover the whole framework.
| Sample | Description |
|---|---|
| basic | Text generation, streaming, and flows |
| basic‑structured | Typed JSON output with GenerateData and GenerateDataStream |
| basic‑formats | Output formats and how each one streams |
| basic‑decisions | Typed decisions with calibrated probabilities from TypeSafe jev |
| basic‑media | Image input, image generation and editing, and video generation |
| basic‑prompts | Handlebars templates, .prompt files, partials, and helpers |
| basic‑prompt‑content | Prompt content built in Go from your data |
| basic‑tools | A tool that streams progress and attaches a chart |
| basic‑agents | Inline, prompt-file, and custom agents with snapshots, background runs, and delegation |
| basic‑agents‑server | Store-backed and stateless agents over HTTP |
| basic‑tool‑interrupts | Human in the loop: a tool that pauses for approval |
| basic‑middleware | Retry and fallback, filesystem, skills, and context compression middleware |
| basic‑errors | Error classification with sentinels and errors.Is |
| basic‑durable‑streaming‑exp | Reconnectable streams with replay (preview) |
Contents · everything Genkit Go can do, at a glance
Agents (preview)
Multi-turn conversations with snapshots you can resume, branch, and run in the background.
Define an Agent · Multi-Turn Conversations · Snapshots · Background Agents · Delegate to Sub-Agents · Load the Prompt from a File · Custom Turn Loops · Redact on the Way Out · Serve Agents over HTTP
Generating Generate and Stream · Structured Output · Output Formats
Tools Define Tools · Tool Interrupts · Middleware
Prompts Define Prompts · Build Prompts from Your Data · Prompt Files
Flows Flows · Traces and Logs · Error Handling
Gemini, Claude, GPT, Grok, DeepSeek, Qwen, Kimi, GLM, Llama, Mistral, local models, and anything OpenAI-compatible.
Agents run multi-turn conversations. Each one owns its turn loop and session state, so your code sends messages and reads results. Every turn writes a snapshot, so a conversation can resume in any process, branch from an earlier turn, recover from a failure, or keep running in the background.
Warning
This API is in preview and may experience breaking changes in minor releases.
Constructors live in genkit/exp (genkitx) and types and options in ai/exp (aix). Enable them with genkit.WithExperimental().
A prompt-backed agent with an inline prompt and a session store:
// *aix.Agent[ChatState]: the State type comes from the session store.
chatAgent := genkitx.DefineAgent(g, "chat",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("You are a sarcastic pirate. Keep responses concise."),
},
aix.WithSessionStore(localstore.NewInMemorySessionStore[ChatState]()),
)
// *aix.AgentOutput[ChatState]
out, err := chatAgent.RunText(ctx, "What's the best way to learn Go?")
fmt.Println(out.Message.Text())Connect opens a streaming session. Send a message, read chunks until TurnEnd, then send the next one:
conn, err := chatAgent.Connect(ctx)
conn.SendText("What is Go's concurrency model?")
for chunk, err := range conn.Receive() {
if chunk.ModelChunk != nil {
fmt.Print(chunk.ModelChunk.Text())
}
if chunk.TurnEnd != nil {
break
}
}
conn.SendText("Show me an example with goroutines.")
// ...
out, err := conn.Output()
fmt.Println(out.Message.Text())Every turn writes a snapshot: the conversation and its state at that point. Continue a session from its latest snapshot in any process, or branch from any earlier one:
plan, err := chatAgent.RunText(ctx, "Plan a day in Kyoto.")
// Continue the session, in this process or another. Options are typed to the
// agent's State, so an option for a different State type does not compile.
next, err := chatAgent.RunText(ctx, "Add a tea ceremony.",
aix.WithSessionID[ChatState](plan.SessionID))
// Branch from the first plan. The tea ceremony is not in this timeline.
rainy, err := chatAgent.RunText(ctx, "Assume it rains.",
aix.WithSnapshotID[ChatState](plan.SnapshotID))
fmt.Println(rainy.Message.Text())Failed and stopped runs land as snapshots too, keeping the work they finished, so you resume them instead of starting over. Background runs, sub-agent tasks, and HTTP clients all address work by snapshot ID. Stores ship for memory, files, and Firestore.
Docs · Stores · Failures · Example
Detach hands the work to the server and returns a snapshot ID right away. Check on it with GetSnapshot, wait with WaitForSnapshot, or stop it with Abort:
conn, err := chatAgent.Connect(ctx)
conn.SendText("Draft a detailed two-week Japan itinerary.")
conn.Detach()
out, err := conn.Output()
// Later, from anywhere. snap is *aix.SessionSnapshot[ChatState].
snap, err := chatAgent.WaitForSnapshot(ctx, out.SnapshotID)The Agents middleware (plugins/middleware/exp) gives an agent one delegate_to_<name> tool per sub-agent:
researcher := genkitx.DefineAgent(g, "researcher",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("Research the topic and summarize well-sourced findings."),
},
aix.WithDescription[any]("Researches a topic."),
)
orchestrator := genkitx.DefineAgent(g, "orchestrator",
aix.InlinePrompt{
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithSystem("Delegate research, then write the final answer."),
ai.WithUse(&middlewarex.Agents{Agents: []aix.AgentRef{researcher.Ref()}}),
},
aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)Set Async: true to run sub-agents in the background, with tools to check, wait for, abort, and continue them.
Docs · Example · Async example
DefinePromptAgent renders the .prompt file named after the agent, so prompt authors can tune it without touching Go:
# prompts/chat.prompt
---
model: googleai/gemini-flash-latest
---
You are a Michelin-starred chef. Keep responses concise.chatAgent := genkitx.DefinePromptAgent(g, "chat",
aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)aix.WithNamedPrompt points several agents at one shared prompt, each with its own input.
DefineCustomAgent hands you the turn body. Session state, snapshots, and background runs still come for free:
// *aix.Agent[any]: the State type comes from the SessionRunner in the signature.
chatAgent := genkitx.DefineCustomAgent(g, "chat",
func(ctx context.Context, resp aix.Responder, sess *aix.SessionRunner[any]) (*aix.AgentResult, error) {
err := sess.Run(ctx, func(ctx context.Context, input *aix.AgentInput) (*aix.TurnResult, error) {
for chunk, err := range genkit.GenerateStream(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithMessages(sess.Messages()...),
) {
if err != nil {
return nil, err
}
if chunk.Done {
sess.AddMessages(chunk.Response.Message)
} else {
resp.SendModelChunk(chunk.Chunk)
}
}
return nil, nil
})
if err != nil {
return nil, err
}
return sess.Result(), nil
},
aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()),
)With a typed State, sess.UpdateCustom changes your own state and streams the change to the client.
WithStateTransform rewrites session state before it leaves the server. Stored snapshots stay raw:
// The store and the transform both set State to ChatState.
// If they disagree, the code does not compile.
chatAgent := genkitx.DefineAgent(g, "chat",
aix.InlinePrompt{ai.WithModelName("googleai/gemini-flash-latest")},
aix.WithSessionStore(store),
aix.WithStateTransform(func(ctx context.Context, s *aix.SessionState[ChatState]) (*aix.SessionState[ChatState], error) {
return redactPII(ctx, s) // ctx carries the caller's identity
}),
)WithStreamTransform does the same for streamed chunks.
AllAgentRoutes serves every agent, one turn per request:
mux := http.NewServeMux()
for _, r := range genkitx.AllAgentRoutes(g) {
mux.HandleFunc(r.Pattern(), r.Handler())
}
// POST /agents/chat one turn (?stream=true for SSE)
// POST /agents/chat/getSnapshot read a snapshot
// POST /agents/chat/waitForSnapshot wait for a snapshot to settle
// POST /agents/chat/abort abort background work
if err := server.Start(ctx, "127.0.0.1:8080", mux); err != nil {
log.Fatal(err)
}Genkit Go gives you everything you need to build AI applications with confidence.
Generate returns the whole response. GenerateStream takes the same options and yields the response chunk by chunk:
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Write a short story about a robot learning to paint."),
)
fmt.Println(resp.Text())
for result, err := range genkit.GenerateStream(ctx, g, /* same options */) {
if result.Done {
break // result.Response holds the same *ai.ModelResponse as Generate
}
fmt.Print(result.Chunk.Text())
}genkit.GenerateText returns only the string, as in the Quick Start. To stream to a callback instead of a loop, pass ai.WithStreaming(cb) to any of them.
Get type-safe JSON output that maps directly to your Go structs:
type Recipe struct {
Title string `json:"title"`
Ingredients []string `json:"ingredients"`
}
// The output schema is inferred from Recipe, and the reply is decoded into a *Recipe.
recipe, resp, err := genkit.GenerateData[Recipe](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Create a recipe for chocolate chip cookies."),
)
fmt.Println(recipe.Title)
for result, err := range genkit.GenerateDataStream[Recipe](ctx, g, /* same options */) {
if result.Done {
break // result.Output is the final Recipe
}
fmt.Println(len(result.Chunk.Ingredients)) // result.Chunk is the Recipe so far
}A format decides how the model's text is parsed and what each streamed chunk holds. With jsonl, every item arrives once, as soon as it is complete:
for result, err := range genkit.GenerateDataStream[[]Character](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Invent four characters for a heist movie."),
ai.WithOutputFormat(ai.OutputFormatJSONL),
) {
for _, c := range result.Chunk { // only the characters finished since the last chunk
fmt.Println(c.Name)
}
}
// WithOutputEnums selects the enum format and limits the answer to these labels.
rating, resp, err := genkit.GenerateData[Rating](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Which audience is this story for? %s", story),
ai.WithOutputEnums(RatingAllAges, RatingYoungAdult, RatingAdult),
)Built in: json (the default for typed output, where each chunk is the whole value so far), jsonl and array for lists, enum, and text. To add your own, implement ai.Formatter, which gives each request an ai.FormatHandler that writes the model instructions and parses the output:
genkit.DefineFormats(g, csvFormatter{})
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("List three colors."),
ai.WithOutputFormat("csv"), // the name that csvFormatter.Name returns
)Docs · Custom formats · Example
Give models the ability to take actions and access external data:
type WeatherInput struct {
Location string `json:"location"`
}
// *ai.ToolAction[WeatherInput, string].
// The model sees a JSON schema inferred from WeatherInput.
weatherTool := genkit.DefineTool(g, "getWeather",
"Gets the current weather for a location",
func(ctx *ai.ToolContext, input WeatherInput) (string, error) {
return fmt.Sprintf("Weather in %s: 72°F and sunny", input.Location), nil
},
)
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("What's the weather like in San Francisco?"),
ai.WithTools(weatherTool),
)
fmt.Println(resp.Text())Return tool.Fail (package ai/tool) to send an error back to the model so it can try again:
pop, err := db.Population(ctx, input.City)
if errors.Is(err, ErrNoSuchCity) {
return 0, tool.Fail(ctx, err) // the model tries another spelling
}
return pop, err // any other error stops the generationtool.SendChunk streams progress while a tool runs, and tool.AttachParts adds media to its response.
Pause a tool for a person's approval, then resume with their answer:
// *ai.ResumableToolAction[TransferInput, string, Approval].
// The third type is the answer that the tool resumes with.
transfer := genkit.DefineResumableTool(g, "transfer", "Transfers money.",
func(ctx context.Context, input TransferInput, approval *Approval) (string, error) {
if approval == nil {
return "", tool.Interrupt(ctx, nil) // pause and ask
}
if !approval.Approved {
return "Transfer cancelled.", nil
}
return "Transfer completed.", nil
},
)
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Transfer $5000 to account ABC123"),
ai.WithTools(transfer),
)
var answers []*ai.Part
for _, part := range resp.Interrupts() {
if call, ok := transfer.Interrupted(part); ok {
// call.Input is a TransferInput, and Restart accepts only an Approval.
approved := askHuman(call.Input)
answers = append(answers, call.Restart(Approval{Approved: approved}))
}
}
resp, err = genkit.Generate(ctx, g,
ai.WithMessages(resp.History()...),
ai.WithTools(transfer),
ai.WithResume(answers...),
)call.Respond answers without running the tool again. The ToolApproval middleware pauses calls the same way.
Middleware wraps generation, model calls, and tool execution. Register the middleware plugin to see the built-ins in the Dev UI, then attach them per call with ai.WithUse:
g := genkit.Init(ctx, genkit.WithPlugins(
&googlegenai.GoogleAI{},
&middleware.Middleware{},
))
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Explain quantum computing."),
ai.WithUse(
&middleware.Retry{MaxRetries: 3},
&middleware.Fallback{Models: []ai.ModelRef{
googlegenai.ModelRef("googleai/gemini-3.5-flash", nil),
}},
),
)Also built in:
ToolApproval: pauses tool calls not on an allow list until a person or a judge model decides.SoftToolErrors: sends tool errors back to the model so it can correct itself.Filesystem: gives the model file tools confined to one directory.Skills: loads Agent SkillsSKILL.mdfiles on demand.
Experimental, in middleware/exp: ContextCompression keeps long conversations and tool loops inside a context budget.
To build your own, implement ai.Middleware or wrap a function with ai.MiddlewareFunc.
Create reusable prompts with Handlebars templates and typed input and output:
jokePrompt := genkit.DefineDataPrompt[JokeRequest, Joke](g, "joke",
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Tell a joke about {{topic}}."),
)
joke, resp, err := jokePrompt.Execute(ctx, JokeRequest{Topic: "cats"})
fmt.Println(joke.Punchline)ExecuteStream streams typed chunks of the output. genkit.DefinePrompt is the untyped form.
Fill any part of a prompt with a Go function instead of a template. What it returns is sent as written, so user text never needs escaping:
supportPrompt := genkit.DefinePrompt(g, "support",
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithInputType(Ticket{}),
ai.WithPromptPartsFn(func(ctx context.Context, t Ticket) ([]*ai.Part, error) {
return []*ai.Part{
ai.NewTextPart(t.Question),
ai.NewMediaPart("image/png", t.Screenshot),
}, nil
}),
)Every slot has a function form: WithSystemFn, WithPromptFn, WithMessagesFn, and WithDocsFn.
Keep prompts in .prompt files with YAML frontmatter, and embed them in your binary:
# prompts/recipe.prompt
---
model: googleai/gemini-flash-latest
input:
schema: RecipeRequest
output:
format: json
schema: Recipe
---
{{role "system"}}
You are an experienced chef.
{{role "user"}}
Create a {{cuisine}} {{dish}} recipe for {{servingSize}} people.//go:embed prompts
var prompts embed.FS
g := genkit.Init(ctx,
genkit.WithPlugins(&googlegenai.GoogleAI{}),
genkit.WithPromptFS(prompts),
)
genkit.DefineSchemasFor(g, RecipeRequest{}, Recipe{})
recipePrompt := genkit.LookupDataPrompt[RecipeRequest, *Recipe](g, "recipe")
recipe, resp, err := recipePrompt.Execute(ctx, RecipeRequest{
Dish: "tacos",
Cuisine: "Mexican",
ServingSize: 4,
})
fmt.Println(recipe.Title)Without genkit.WithPromptFS, prompts load from the prompts directory on disk. genkit.DefinePartial and genkit.DefineHelper add partials and helpers that every prompt shares.
A flow is a typed, traced function that you can test in the Dev UI and serve as an HTTP endpoint with one line:
// *core.Flow[string, string, string].
// The input, output, and stream types come from the function.
storyFlow := genkit.DefineStreamingFlow(g, "story",
func(ctx context.Context, topic string, send core.StreamCallback[string]) (string, error) {
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Write a story about %s", topic),
ai.WithStreaming(func(ctx context.Context, chunk *ai.ModelResponseChunk) error {
return send(ctx, chunk.Text())
}),
)
},
)
mux := http.NewServeMux()
mux.HandleFunc("POST /story", genkit.Handler(storyFlow))
log.Fatal(http.ListenAndServe(":8080", mux))curl -N "http://localhost:8080/story?stream=true" -d '{"data": "a robot who paints"}'genkit.Handler returns a standard http.HandlerFunc, so it works with Gin, Echo, Chi, or any router. genkit.WithStreamManager lets clients reconnect to a stream (preview).
Docs · Example · Durable streaming example
Every flow, model call, and tool call is traced. genkit.Run adds your own steps, and core/logger attaches logs to the span that wrote them:
genkit.DefineFlow(g, "summarize",
func(ctx context.Context, doc string) (string, error) {
logger.Info(ctx, "summarizing", "bytes", len(doc))
// A traced step. Its output type comes from the function, so clean is a string.
clean, err := genkit.Run(ctx, "clean", func() (string, error) {
return strings.TrimSpace(doc), nil
})
return genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Summarize: %s", clean),
)
},
)Set GENKIT_LOG_LEVEL=debug to see Genkit's own model and tool detail in the terminal.
Genkit classifies its failures with sentinels, so you branch with errors.Is instead of matching message text:
text, err := genkit.GenerateText(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Summarize this."),
)
switch {
case errors.Is(err, ai.ErrModelNotFound):
// The plugin providing this model isn't registered in genkit.Init.
case errors.Is(err, ai.ErrToolFailed):
// A tool returned an error. It's wrapped, so errors.As reaches yours.
case errors.Is(err, status.ErrResourceExhausted):
// Rate limited or out of quota: back off and retry.
}A failed generation still returns its partial response, so resp.History() keeps the tool rounds that finished.
Classify your own errors the same way. Over HTTP, the status sets the response code, and only PublicErrorf messages reach the client:
var ErrRecipeNotFound = status.ErrNotFound.Subtype("recipe not found") // HTTP 404
return "", status.PublicErrorf(ErrRecipeNotFound, "no recipe for %q", dish)Genkit provides a unified interface across all major AI providers. Use whichever model fits your needs:
| Provider | Plugin | Models |
|---|---|---|
| Google AI | googlegenai.GoogleAI |
Gemini 3.5 Flash, Gemini 3.1 Pro, and more |
| Vertex AI | googlegenai.VertexAI |
Gemini 3.5 Flash, Gemini 3.1 Pro via Google Cloud |
| Anthropic | anthropic.Anthropic |
Claude Opus 5, Claude Sonnet 5, Claude Fable 5, Claude Haiku 4.5 |
| Vertex AI Model Garden | modelgarden.Anthropic, .Llama, .Mistral |
Claude, Llama, and Mistral via Google Cloud |
| Ollama | ollama.Ollama |
Llama 4, Qwen 3, DeepSeek, and other local models |
| OpenAI Compatible | compat_oai |
GPT-5.6, Grok, DeepSeek, Qwen, Kimi, GLM, the OpenRouter gateway, and any OpenAI-compatible API |
| System One (preview) | systemonex.SystemOne, systemonex.TypeSafe(), systemonex.OpenRouter(), systemonex.Ollama(), systemonex.WorkersAI("") |
Decision models such as TypeSafe's jev, Liquid AI's d1, and Cloudflare's Clef, with calibrated answers (README) |
// Google AI
g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))
// Anthropic
g := genkit.Init(ctx, genkit.WithPlugins(&anthropic.Anthropic{}))
// Ollama (local models)
g := genkit.Init(ctx, genkit.WithPlugins(&ollama.Ollama{
ServerAddress: "http://localhost:11434",
}))
// Multiple providers at once
g := genkit.Init(ctx, genkit.WithPlugins(
&googlegenai.GoogleAI{},
&anthropic.Anthropic{},
))Use ai.WithModelName for simple cases, or pair a model with provider-specific config using ModelRef:
resp, err := genkit.Generate(ctx, g,
ai.WithModel(googlegenai.ModelRef("googleai/gemini-flash-latest", &genai.GenerateContentConfig{
Temperature: genai.Ptr(float32(0.7)),
MaxOutputTokens: 1000,
})),
ai.WithPrompt("Hello!"),
)Use the Genkit CLI to run your app with tracing and a local development UI:
curl -sL cli.genkit.dev | bash
genkit start -- go run main.goThe local developer UI lets you:
- Test flows with different inputs interactively
- Inspect traces to debug complex multi-step operations
- Compare models by switching providers in real-time
- Evaluate prompts against datasets
Built by Google with contributions from the Open Source Community