Define agents
In Genkit, agents are actions with conversation state. A standard agent renders a prompt on each turn, appends conversation history, calls the model, streams chunks, updates state, and optionally persists a snapshot. A custom agent keeps that runtime shell, but replaces the prompt-backed loop with your own code.
This page covers defining the agent itself. For when to choose an agent over a plain flow, see Full-stack agents.
Constructor choices
Section titled “Constructor choices”ai.defineAgent()defines the prompt and the agent in one place. This is the common path for chat assistants, tool-using agents, and server-backed frontend features.ai.definePromptAgent()wraps a prompt that already exists as a prompt action or Dotprompt file. It keeps prompt copy, model settings, schemas, and tool lists in the prompt layer while the agent adds conversation state and transport.ai.defineCustomAgent()replaces the built-in prompt loop with your own code, for multiple model calls in one turn, custom planning loops, manual history management, or custom streaming.
All three produce an agent that supports the transport-agnostic chat(), loadChat(), getSnapshot(), and abort() surface. The agent is also a bidirectional action that can be served over HTTP.
Define a prompt-backed agent
Section titled “Define a prompt-backed agent”defineAgent() combines prompt definition and agent registration. It accepts normal prompt options, plus agent-specific options such as stateSchema, store, clientTransform, and promptInput.
import { genkit, z, FileSessionStore } from 'genkit/beta';import { googleAI } from '@genkit-ai/google-genai';
const ai = genkit({ plugins: [googleAI()], model: googleAI.model('gemini-flash-latest'),});
const store = new FileSessionStore<WeatherState>('./.genkit/snapshots/weather');
const getWeather = ai.defineTool( { name: 'getWeather', description: 'Get the current weather for a location.', inputSchema: z.object({ location: z.string() }), outputSchema: z.object({ temperatureF: z.number(), conditions: z.string(), }), }, async ({ location }) => { return { temperatureF: 72, conditions: `sunny in ${location}` }; },);
const WeatherStateSchema = z.object({ lastLocation: z.string().optional(),});
type WeatherState = z.infer<typeof WeatherStateSchema>;
export const weatherAgent = ai.defineAgent({ name: 'weatherAgent', description: 'Answers weather questions for a location.', system: 'Answer weather questions. Ask for a location when one is missing.', tools: [getWeather], stateSchema: WeatherStateSchema, store,});Agent-specific options
Section titled “Agent-specific options”nameregisters the prompt and agent action. Use a stable, descriptive name because it appears in action metadata, route helpers, and delegation tools.descriptionis surfaced in action metadata, the developer UI, and the multi-agent middleware. Write it as an operational summary of when another agent should delegate to this one.stateSchemavalidates custom state when loading from a snapshot or client state. Add it when custom state crosses a trust boundary or when schema metadata helps tools inspect the agent.storeswitches the agent to server-managed state. Add a store when you want snapshots, branching, background execution,loadChat(), or smaller client payloads.clientTransformshapes state and stream chunks before they leave the server. Use it for redaction, tenancy checks, or client-specific projections.promptInputsupplies values for prompt input variables. Use it when one prompt definition should power several differently configured agents.
defineAgent() also accepts the prompt options used by definePrompt(), including model, system, messages, tools, config, output, maxTurns, and middleware through use.
Wrap an existing prompt
Section titled “Wrap an existing prompt”Use definePromptAgent() when the prompt already exists. This is common with Dotprompt files because prompt authors can tune model settings, schemas, and template content without touching agent wiring.
export const tripAgent = ai.definePromptAgent({ promptName: 'trip-planner', description: 'Plans short trips with weather-aware recommendations.', promptInput: { tone: 'concise' }, stateSchema: TripStateSchema, store,});The referenced prompt is looked up when the agent is invoked. If the prompt is not registered, the turn fails with an error telling you which prompt name was missing.
Dotprompt keeps prompt copy and model settings close to the content:
---model: googleai/gemini-flash-latestinput: schema: destination: string tone?: stringtools: - getWeather---
Plan a short trip to {{destination}}.Use weather data when it changes the recommendation.Write in a {{tone}} tone.Tools and current session
Section titled “Tools and current session”Tools can read and update the active session by calling ai.currentSession<State>(). The session object exposes getCustom(), updateCustom(fn), getMessages(), addMessages(), setMessages(), getArtifacts(), and addArtifacts().
const addTask = ai.defineTool( { name: 'addTask', description: 'Add a new task to the task list.', inputSchema: z.object({ title: z.string() }), outputSchema: z.object({ id: z.number(), title: z.string(), done: z.boolean(), }), }, async ({ title }) => { const session = ai.currentSession<TaskState>(); let task!: TaskItem;
session.updateCustom((state) => { const next = state ?? { tasks: [], nextId: 1 }; task = { id: next.nextId, title, done: false }; return { tasks: [...next.tasks, task], nextId: next.nextId + 1, }; });
return task; },);Custom-state mutations automatically emit streamed JSON Patch chunks. That keeps chat.state and chunk.custom current while a turn is still running.
Define a custom agent implementation
Section titled “Define a custom agent implementation”Use defineCustomAgent() when the standard prompt loop is too narrow, such as for multiple model calls in one turn, planner and executor loops, or manual history management. A custom agent receives a SessionRunner and helpers for streaming chunks, and still gets snapshot management, client-managed and server-managed state, background execution, and HTTP serving.
export const researchAgent = ai.defineCustomAgent( { name: 'researchAgent', description: 'Breaks a question into subtopics and synthesizes an answer.', stateSchema: ResearchStateSchema, store, }, async (sess, { sendChunk, abortSignal }) => { // Your own per-turn loop. See Custom orchestration for the full pattern. },);See Custom orchestration for the runtime contract, a complete multi-step example, and failure handling.
Constructor choices
Section titled “Constructor choices”genkitx.DefineAgentkeeps inline prompt configuration beside the agent wiring. Use it for most prompt-backed agents.genkitx.DefinePromptAgentwraps a prompt that is already registered, including prompts loaded from Dotprompt files.genkitx.DefineCustomAgentreplaces the prompt loop with your own code, for a custom per-turn loop, direct session control, or multiple model calls.
All agents implement api.BidiAction, so transports and route helpers can serve them directly. Server-managed agents also expose typed snapshot helpers and companion actions.
Define a prompt-backed agent
Section titled “Define a prompt-backed agent”DefineAgent registers a prompt-backed agent from an aix.InlinePrompt. The inline prompt is a list of prompt options.
import ( aix "github.com/firebase/genkit/go/ai/exp" "github.com/firebase/genkit/go/ai/exp/localstore" genkitx "github.com/firebase/genkit/go/genkit/exp")store, err := localstore.NewFileSessionStore[TaskState]("./.genkit/snapshots/tasks")if err != nil { // Fails if the snapshot directory cannot be created or is not writable. log.Fatalf("open task store: %v", err)}
taskAgent := genkitx.DefineAgent(g, "taskAgent", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("Manage a task list. Use tools when changing tasks."), ai.WithTools(addTaskTool, toggleTaskTool), }, aix.WithSessionStore(store), aix.WithDescription[TaskState]("Task management assistant"),)The agent’s custom state type is inferred from typed options such as WithSessionStore[TaskState], WithStateTransform[TaskState], or the explicit type argument on DefineAgent[TaskState].
Agent options
Section titled “Agent options”aix.WithSessionStore(store)persists snapshots and switches the agent to server-managed state.aix.WithStateTransform(fn)redacts or reshapes session state returned to clients and snapshot readers.aix.WithStreamTransform[State](fn)redacts or reshapes each streamed chunk before it is sent to clients.aix.WithDescription[State](text)adds a human-readable description to action metadata and developer tooling.aix.WithNamedPrompt[State](name, input)pointsDefinePromptAgentat a specific registered prompt and renders input.
Typed options are deliberately strict. Passing a state option with the wrong State type fails at compile time.
Wrap an existing prompt
Section titled “Wrap an existing prompt”DefinePromptAgent wraps a prompt already registered in the prompt registry. With no prompt-source option, it uses a prompt with the same name as the agent.
chef := genkitx.DefinePromptAgent[ChefState](g, "chef", aix.WithSessionStore(store), aix.WithDescription[ChefState]("Chef assistant loaded from ./prompts/chef.prompt"),)Use WithNamedPrompt when several agents share one prompt or when the prompt name differs from the agent name.
friendlyChef := genkitx.DefinePromptAgent[ChefState](g, "friendlyChef", aix.WithNamedPrompt[ChefState]("chef", map[string]any{ "personality": "friendly", }), aix.WithSessionStore(store),)The prompt input is rendered at definition time as a smoke test. If it does not satisfy the prompt schema, the constructor panics during setup rather than during the first request.
Define a custom agent implementation
Section titled “Define a custom agent implementation”DefineCustomAgent gives you the agent runtime without the built-in prompt loop. Use it for a custom per-turn loop, multiple model calls in one turn, or direct session control. The function receives a Responder for streaming and a SessionRunner for turn processing, messages, custom state, artifacts, and snapshots.
coder := genkitx.DefineCustomAgent(g, "coder", func(ctx context.Context, resp aix.Responder, sess *aix.SessionRunner[CoderState]) (*aix.AgentResult, error) { // Your own per-turn loop. See Custom orchestration for the full pattern. }, aix.WithSessionStore(store), aix.WithDescription[CoderState]("Concise code helper"),)See Custom orchestration for the runtime contract, a complete example, turn context, responder behavior, and failure handling.
Constructor choices
Section titled “Constructor choices”ai.defineAgent()defines the prompt instructions, tools, and the agent shell in one place. This is the common path for chat assistants, tool-calling agents, and server-backed frontend features.ai.definePromptAgent()wraps a prompt that already exists as a Dotprompt file (or registered prompt). It keeps prompt copy, model settings, schemas, and tool lists in the prompt layer while the agent adds conversation state and transport.ai.defineCustomAgent()replaces the prompt loop with your own code, allowing multiple model calls in one turn, custom planning loops, direct session control, or custom streaming.
All three constructors produce an Agent instance supporting the transport-agnostic chat(), loadChat(), getSnapshot(), and abort() APIs. The agent is also a bidirectional action that can be served over HTTP.
Define a prompt-backed agent
Section titled “Define a prompt-backed agent”defineAgent() registers a prompt-backed agent. It accepts normal prompt-generation options plus agent-specific options such as stateSchema and store.
import 'package:genkit/genkit.dart';import 'package:genkit/io.dart';import 'package:schemantic/schemantic.dart';
part 'weather_agent.g.dart';
@Schema()abstract class $GetWeatherInput { String get location;}
@Schema()abstract class $GetWeatherOutput { String get weather; String get temperature;}
final getWeather = ai.defineTool( name: 'getWeather', description: 'Get the current weather for a location.', inputSchema: GetWeatherInput.$schema, outputSchema: GetWeatherOutput.$schema, fn: (input, _) async => GetWeatherOutput( weather: 'Sunny in ${input.location}', temperature: '71F', ),);
final weatherAgent = ai.defineAgent( name: 'weatherAgent', system: 'You help with weather information. Use the getWeather tool.', tools: [getWeather], store: FileSessionStore('.sessions'),);Agent options
Section titled “Agent options”nameregisters the prompt and agent action. Use a stable, descriptive name because it appears in action metadata, route helpers, and delegation tools.descriptionis surfaced in action metadata and the multi-agent delegation middleware. Write it as a summary of when another agent should delegate to this one.systemprovides the instructions or system prompt for the model loop.toolslists the tools the agent is permitted to call during its turn.maxTurnscaps how many model/tool iterations one turn may run before stopping.stateSchemavalidates custom session state when loading from snapshots or client state.storeswitches the agent to server-managed state. Add a store when you want snapshots, branching, background execution,loadChat(), or smaller client payloads.uselists the middleware applied to the prompt model loops. Register the matching plugin in yourGenkit(plugins: [...])so the reference resolves at runtime (for example,RetryPlugin()forretry(),AgentsPlugin()foragents()).
Tools and current session
Section titled “Tools and current session”Tools can read and update the active session by calling ai.currentSession<State>(). The session object exposes typed getCustom() and updateCustom(fn) along with other state utilities. The State type comes from the agent’s stateSchema, so the updater receives a typed State? value with no casting.
@Schema()abstract class $TaskItem { int get id; String get title; bool get done;}
@Schema()abstract class $TaskState { List<$TaskItem> get tasks; int get nextId;}
@Schema()abstract class $AddTaskInput { String get title;}
final addTask = ai.defineTool( name: 'addTask', description: 'Add a new task to the task list.', inputSchema: AddTaskInput.$schema, outputSchema: TaskItem.$schema, fn: (input, _) async { final session = ai.currentSession<TaskState>()!; late TaskItem newTask;
session.updateCustom((state) { final nextId = state?.nextId ?? 1; newTask = TaskItem(id: nextId, title: input.title, done: false); return TaskState( tasks: [...?state?.tasks, newTask], nextId: nextId + 1, ); });
return newTask; },);Provide the matching stateSchema when defining the agent (here stateSchema: TaskState.$schema) so the session state is typed. Custom-state mutations automatically emit streamed JSON Patch chunks, which keeps client-side state and streamed updates current while a turn is still running.
Wrap an existing prompt
Section titled “Wrap an existing prompt”Use definePromptAgent() when the prompt already exists as a Dotprompt file. This is common because prompt authors can tune the model, schemas, tool list, and template content without touching agent wiring. promptInput supplies values for the prompt template’s input variables, so a single shared .prompt file can be reused and customized by multiple agents.
import 'package:genkit/genkit.dart';
import 'genkit.dart';
final tripPlannerAgent = ai.definePromptAgent( promptName: 'tripPlanner', promptInput: {'tone': 'enthusiastic'}, store: InMemorySessionStore(),);The Dotprompt file keeps the prompt copy, model settings, input schema, and tool list close to the content. Here {{tone}} is filled by the promptInput above:
---model: googleai/gemini-flash-latestinput: schema: tone: stringtools: - getAttractions - getFlightInfo---
{{role "system"}}You are a friendly trip planning assistant. Help users plan trips by suggestingattractions and looking up flight information. Use the available tools to provideaccurate, up-to-date information. Keep your tone {{tone}}.
{{history}}The referenced prompt is looked up when the agent is invoked. If the prompt is not registered, the turn fails with an error telling you which prompt name was missing.
Define a custom agent implementation
Section titled “Define a custom agent implementation”Use defineCustomAgent() when the standard prompt loop is too narrow, such as for multiple model calls in one turn, custom planning loops, or manual history management. A custom agent receives a SessionRunner and handles turn processing directly.
final researchAgent = ai.defineCustomAgent( name: 'researchAgent', fn: (sess, options) async { // Your own per-turn loop. See Custom orchestration for the full pattern. }, store: InMemorySessionStore(),);See Custom orchestration for the runtime contract, a complete example, and failure handling.
Constructor choices
Section titled “Constructor choices”ai.define_agent()defines the prompt instructions, tools, and the agent shell in one place. This is the common path for chat assistants, tool-calling agents, and server-backed frontend features.ai.define_prompt_agent()wraps a prompt that already exists (registered withai.define_prompt()or loaded from a Dotprompt file). It keeps prompt copy, model settings, schemas, and tool lists in the prompt layer while the agent adds conversation state and transport.ai.define_custom_agent()replaces the prompt loop with your own code, allowing multiple model calls in one turn, custom planning loops, direct session control, or custom streaming.
All three constructors produce an Agent that supports the transport-agnostic chat(), load_chat(), get_snapshot(), and abort() APIs. The agent is also a bidirectional action that can be served over HTTP.
Define a prompt-backed agent
Section titled “Define a prompt-backed agent”define_agent() registers a prompt-backed agent. It accepts normal prompt-generation options plus agent-specific options such as state_schema and store.
from pydantic import BaseModel
from genkit import Genkitfrom genkit.agent import FileSessionStorefrom genkit_google_genai import GoogleAI
ai = Genkit(plugins=[GoogleAI()])
class WeatherInput(BaseModel): location: str
class WeatherOutput(BaseModel): temperature_f: float conditions: str
@ai.tool()async def get_weather(input: WeatherInput) -> WeatherOutput: """Get the current weather for a location.""" return WeatherOutput(temperature_f=72, conditions=f'sunny in {input.location}')
class WeatherState(BaseModel): last_location: str | None = None
store = FileSessionStore('./.genkit/snapshots/weather')
weather_agent = ai.define_agent( name='weatherAgent', description='Answers weather questions for a location.', model='googleai/gemini-flash-latest', system='Answer weather questions. Ask for a location when one is missing.', tools=[get_weather], state_schema=WeatherState, store=store,)Agent options
Section titled “Agent options”nameregisters the prompt and agent action. Use a stable, descriptive name because it appears in action metadata, route helpers, and Dev UI listings.descriptionis surfaced in action metadata and the developer UI.systemprovides the instructions or system prompt for the model loop.toolslists the tools the agent is permitted to call during its turn.max_turnscaps how many model/tool iterations one turn may run before stopping.state_schemais a Pydantic model for custom session state. When set,chat.state,response.state, and streamedchunk.customcome back as that model.storeswitches the agent to server-managed state. Add a store when you want snapshots, branching, background execution,load_chat(), or smaller client payloads.uselists middleware applied to the prompt model loops (for exampleToolApprovalfromgenkit_middleware).state_transform/chunk_transformshape state and stream chunks before they leave the server. Use them for redaction or client-specific projections.
Tools and current session
Section titled “Tools and current session”Tools can read and update the active session by calling ai.current_session(). The session exposes update_custom(fn), get_messages(), add_messages(), add_artifacts(), and related helpers.
from pydantic import BaseModel
from genkit import Genkit
class TaskItem(BaseModel): id: int title: str done: bool = False
class TaskState(BaseModel): tasks: list[TaskItem] = [] next_id: int = 1
class AddTaskInput(BaseModel): title: str
@ai.tool()async def add_task(input: AddTaskInput) -> TaskItem: """Add a new task to the list.""" created: TaskItem | None = None
def mutate(custom: dict | None) -> dict: nonlocal created state = custom or {} next_id = state.get('next_id') or state.get('nextId') or 1 tasks = list(state.get('tasks') or []) created = TaskItem(id=next_id, title=input.title) tasks.append(created.model_dump()) return {'tasks': tasks, 'next_id': next_id + 1}
if sess := ai.current_session(): await sess.update_custom(mutate) return created # type: ignore[return-value]Provide the matching state_schema when defining the agent (here state_schema=TaskState) so custom state is returned as that model. Custom-state mutations emit streamed JSON Patch chunks, which keeps chat.state and chunk.custom current while a turn is still running.
Wrap an existing prompt
Section titled “Wrap an existing prompt”Use define_prompt_agent() when the prompt already exists under the same name. This is common with Dotprompt files because prompt authors can tune the model, schemas, tool list, and template content without touching agent wiring.
ai.define_prompt( name='tripPlanner', model='googleai/gemini-flash-latest', system='Plan short trips. Keep recommendations concise.',)
trip_planner_agent = ai.define_prompt_agent( name='tripPlanner', description='Plans short trips with weather-aware recommendations.', store=store,)The referenced prompt is looked up when the agent is invoked. If the prompt is not registered, the turn fails with an error telling you which prompt name was missing.
Define a custom agent implementation
Section titled “Define a custom agent implementation”Use define_custom_agent() when the standard prompt loop is too narrow, such as for multiple model calls in one turn, custom planning loops, or manual history management. A custom agent receives a SessionRunner and an ActionRunContext, and handles turn processing directly.
from genkit import ActionRunContextfrom genkit.agent import AgentResult, SessionRunner
async def research_fn(sess: SessionRunner, ctx: ActionRunContext) -> AgentResult: # Your own per-turn loop. See Custom orchestration for the full pattern. ...
research_agent = ai.define_custom_agent( name='researchAgent', fn=research_fn, store=store,)See Custom orchestration for the runtime contract, a complete example, and failure handling.