Managing prompts with Dotprompt
Prompt engineering is the primary way that you, as an app developer, influence the output of generative AI models. For example, when using LLMs, you can craft prompts that influence the tone, format, length, and other characteristics of the models’ responses.
The way you write these prompts will depend on the model you’re using; a prompt written for one model might not perform well when used with another model. Similarly, the model parameters you set (temperature, top-k, and so on) will also affect output differently depending on the model.
Getting all three of these factors—the model, the model parameters, and the prompt—working together to produce the output you want is rarely a trivial process and often involves substantial iteration and experimentation. Genkit provides a library and file format called Dotprompt, that aims to make this iteration faster and more convenient.
Dotprompt is designed around the premise that prompts are code. You define your prompts along with the models and model parameters they’re intended for separately from your application code. Then, you (or, perhaps someone not even involved with writing application code) can rapidly iterate on the prompts and model parameters using the Genkit Developer UI. Once your prompts are working the way you want, you can import them into your application and run them using Genkit.
Your prompt definitions each go in a file with a .prompt extension. Here’s an
example of what these files look like:
---model: googleai/gemini-flash-latestconfig: temperature: 0.9input: schema: location: string style?: string name?: string default: location: a restaurant---
You are the world's most welcoming AI assistant and are currently working at {{location}}.
Greet a guest{{#if name}} named {{name}}{{/if}}{{#if style}} in the style of {{style}}{{/if}}.The portion in the triple-dashes is YAML front matter, similar to the front
matter format used by GitHub Markdown and Jekyll; the rest of the file is the
prompt, which can optionally use Handlebars templates. The following sections will go into more detail about each of
the parts that make a .prompt file and how to use them.
Before you begin
Section titled “Before you begin”Before reading this page, you should be familiar with the content covered on the Generating content with AI models page.
If you want to run the code examples on this page, first complete the steps in the Getting started guide for your language:
Complete the Get started guide. All examples assume you have already installed Genkit as a dependency in your project.
Creating prompt files
Section titled “Creating prompt files”Although Dotprompt provides several different ways to create
and load prompts, it’s optimized for projects that organize their prompts as
.prompt files within a single directory (or subdirectories thereof). This
section shows you how to create and load prompts using this recommended setup.
Creating a prompt directory
Section titled “Creating a prompt directory”The Dotprompt library expects to find your prompts in a directory at your project root and automatically loads any
prompts it finds there. By default, this directory is named prompts. For example, using the default directory name,
your project structure might look something like this:
If you want to use a different directory, you can specify it when you configure Genkit:
your-project/├── prompts/│ └── hello.prompt├── main.go├── go.mod└── go.sumg := genkit.Init(context.Background(), genkit.WithPromptDir("./llm_prompts"))Embedding prompts in the binary
Section titled “Embedding prompts in the binary”If you would rather ship one file than a directory of prompts beside it, compile
the prompt directory into the binary with go:embed and hand the resulting
fs.FS to genkit.WithPromptFS(). Lookups are unchanged: prompts are still
found by file name, and genkit.WithPromptDir() still names the directory
within the embedded filesystem (prompts by default).
import "embed"
//go:embed prompts/*var promptsFS embed.FS
func main() { g := genkit.Init(context.Background(), genkit.WithPromptFS(promptsFS)) // ...}A prompt file that fails to parse is logged during genkit.Init(); a file that
parses but defines an invalid prompt panics.
Creating a prompt file
Section titled “Creating a prompt file”There are two ways to create a .prompt file: using a text editor, or with the
developer UI.
Using a text editor
Section titled “Using a text editor”If you want to create a prompt file using a text editor, create a text file with
the .prompt extension in your prompts directory: for example,
prompts/hello.prompt.
Here is a minimal example of a prompt file:
---model: googleai/gemini-flash-latest---You are the world's most welcoming AI assistant. Greet the user and offer your assistance.The portion in the dashes is YAML front matter, similar to the front matter format used by GitHub markdown and Jekyll; the rest of the file is the prompt, which can optionally use Handlebars templates. The front matter section is optional, but most prompt files will at least contain metadata specifying a model. The remainder of this page shows you how to go beyond this, and make use of Dotprompt’s features in your prompt files.
Using the Developer UI
Section titled “Using the Developer UI”You can also create a prompt file using the model runner in the developer UI. Start with application code that imports the Genkit library and configures it to use the model plugin you’re interested in:
package main
import ( "context"
"github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/googlegenai")
func main() { genkit.Init(context.Background(), genkit.WithPlugins(&googlegenai.GoogleAI{}))
// Blocks end of program execution to use the developer UI. select {}}Load the developer UI in the same project:
genkit start -- go run .In the Models section, choose the model you want to use from the list of models provided by the plugin.
Then, experiment with the prompt and configuration until you get results you’re happy with. When you’re ready, press the Export button and save the file to your prompts directory.
Running prompts
Section titled “Running prompts”After you’ve created prompt files, you can run them from your application code, or using the tooling provided by Genkit. Regardless of how you want to run your prompts, first start with application code that imports the Genkit library and the model plugins you’re interested in.
If you’re storing your prompts in a directory other than the default, be sure to specify it when you configure Genkit.
Run prompts from code
Section titled “Run prompts from code”To use a prompt, first load it using the genkit.LookupPrompt() function:
helloPrompt := genkit.LookupPrompt(g, "hello")An executable prompt has similar options to that of genkit.Generate() and many
of them are overridable at execution time, including things like input (see the
section about specifying input schemas), configuration, and more:
resp, err := helloPrompt.Execute(context.Background(), ai.WithModelName("googleai/gemini-flash-latest"), ai.WithInput(map[string]any{"name": "John"}), ai.WithConfig(&genai.GenerateContentConfig{Temperature: genai.Ptr[float32](0.5)}),)Streaming prompt execution
Section titled “Streaming prompt execution”You can stream prompt output using ExecuteStream(), which returns an iterator:
helloPrompt := genkit.LookupPrompt(g, "hello")stream := helloPrompt.ExecuteStream(ctx, ai.WithInput(map[string]any{"name": "John"}))
for result, err := range stream { if err != nil { return "", err } if result.Done { return result.Response.Text(), nil } // Process each chunk fmt.Print(result.Chunk.Text())}Typed prompts with DataPrompt
Section titled “Typed prompts with DataPrompt”For strongly-typed input and output, use genkit.LookupDataPrompt[In, Out]() to
wrap your prompt with Go type information. This provides compile-time type safety
and a more idiomatic Go experience:
type GreetingInput struct { Name string `json:"name"`}
type Greeting struct { Message string `json:"message"` Tone string `json:"tone"`}
// Look up a .prompt file with type informationhelloPrompt := genkit.LookupDataPrompt[GreetingInput, *Greeting](g, "hello")
// Execute with strongly-typed input, get strongly-typed outputgreeting, resp, err := helloPrompt.Execute(ctx, GreetingInput{Name: "John"})if err != nil { log.Fatal(err)}log.Printf("Message: %s, Tone: %s\n", greeting.Message, greeting.Tone)The .prompt file should define an output schema that matches your Go type:
---model: googleai/gemini-flash-latestinput: schema: name: stringoutput: schema: message: string tone: string---Greet {{name}} warmly.For streaming with typed prompts, use ExecuteStream() which provides typed chunks:
helloPrompt := genkit.LookupDataPrompt[GreetingInput, *Greeting](g, "hello")
for result, err := range helloPrompt.ExecuteStream(ctx, GreetingInput{Name: "John"}) { if err != nil { return nil, err } if result.Done { // result.Output is *Greeting return result.Output, nil } // result.Chunk is also *Greeting (partial data as it streams) if result.Chunk.Message != "" { fmt.Println("Partial message:", result.Chunk.Message) }}Any parameters you pass to the prompt call will override the same parameters specified in the prompt file.
See Generate content with AI models for descriptions of the available options.
The basic-prompts sample
defines every one of its prompts twice, once inline in code and once as a
.prompt file looked up by name, so the pair shows exactly what moves out of
code.
Using the Developer UI
Section titled “Using the Developer UI”As you’re refining your app’s prompts, you can run them in the Genkit developer UI to quickly iterate on prompts and model configurations, independently from your application code.
Load the developer UI from your project directory:
genkit start -- go run .Once you’ve loaded prompts into the developer UI, you can run them with different input values, and experiment with how changes to the prompt wording or the configuration parameters affect the model output. When you’re happy with the result, you can click the Export prompt button to save the modified prompt back into your project directory.
Model configuration
Section titled “Model configuration”In the front matter block of your prompt files, you can optionally specify model configuration values for your prompt:
---model: googleai/gemini-flash-latestconfig: temperature: 1.4 topK: 50 topP: 0.4 maxOutputTokens: 400 stopSequences: - "<end>" - "<fin>"---These values map directly to the configuration parameters:
resp, err := helloPrompt.Execute(context.Background(), ai.WithConfig(&genai.GenerateContentConfig{ Temperature: genai.Ptr[float32](1.4), TopK: genai.Ptr[int32](50), TopP: genai.Ptr[float32](0.4), MaxOutputTokens: genai.Ptr[int32](400), StopSequences: []string{"<end>", "<fin>"}, }))See Generate content with AI models for descriptions of the available options.
Tool loops and middleware
Section titled “Tool loops and middleware”Beyond model configuration, the front matter can set several execution-level fields that control how a prompt runs its model and tool loop:
maxTurnscaps how many model/tool iterations a single prompt run may perform before stopping. This applies to tool-calling prompts, where the model may call tools across several turns. It defaults to5.returnToolRequestsreturns the model’s tool-call requests instead of automatically executing the tools and continuing the loop. Use it when you want to inspect, gate, or manually handle tool calls before running them. It defaults tofalse.useattaches middleware to the prompt’s model loop by name, with optional config. Each entry is either a bare middleware name or a map with anameand aconfig. This mirrors the code API, whereuse: [retry(maxRetries: 3)]passes a middleware name plus optional configuration.
---model: googleai/gemini-flash-latesttools: - getAttractions - getFlightInfomaxTurns: 10returnToolRequests: falseuse: - skills # bare middleware name - name: retry # name plus config map config: maxRetries: 3---Plan a trip using the available tools.The middleware referenced by use must be registered so the name resolves at
runtime. Register each middleware when you configure Genkit, and see the
Middleware page for the available middleware and their
configuration.
Register the middleware plugin so the names in use resolve at runtime:
import ( "github.com/firebase/genkit/go/genkit" "github.com/firebase/genkit/go/plugins/googlegenai" "github.com/firebase/genkit/go/plugins/middleware")
g := genkit.Init(context.Background(), genkit.WithPlugins( &googlegenai.GoogleAI{}, &middleware.Middleware{}, ),)The built-in middleware are registered under the plugin’s provider prefix, so a
Go .prompt file names them genkit-middleware/retry,
genkit-middleware/fallback, genkit-middleware/filesystem,
genkit-middleware/skills, and genkit-middleware/toolApproval. The keys in
each config map are the JSON field names of the middleware’s config struct,
which do not always match the Go field names:
---model: googleai/gemini-flash-latestuse: - name: genkit-middleware/retry config: maxRetries: 2 - name: genkit-middleware/skills config: skillPaths: - ./skills---{{query}}The equivalent in code is ai.WithUse, which takes the middleware config
structs directly and needs no registration, since it calls them on a local fast
path rather than looking them up by name:
assistantPrompt := genkit.DefinePrompt(g, "assistant", ai.WithModelName("googleai/gemini-flash-latest"), ai.WithPrompt("{{query}}"), ai.WithUse( &middleware.Retry{MaxRetries: 2}, &middleware.Skills{SkillPaths: []string{"./skills"}}, ),)Registering the plugin is still worth doing when you use ai.WithUse alone,
because that is what makes the middleware visible in the Dev UI. To write your
own middleware and address it by name from a .prompt file, register it with
genkit.DefineMiddleware(); the name comes from the type’s Name() method.
The basic-prompts sample attaches the same four middleware both ways, in code and in frontmatter, on a matching pair of prompts.
Input and output schemas
Section titled “Input and output schemas”You can specify input and output schemas for your prompt by defining them in the front matter section:
---model: googleai/gemini-flash-latestinput: schema: theme?: string default: theme: "pirate"output: schema: dishname: string description: string calories: integer allergens(array): string---Invent a menu item for a {{theme}} themed restaurant.These schemas are used in much the same way as those passed to a generate()
request or a flow definition. For example, the prompt defined above produces
structured output:
menuPrompt := genkit.LookupPrompt(g, "menu")if menuPrompt == nil { log.Fatal("no prompt named 'menu' found")}
resp, err := menuPrompt.Execute(ctx, ai.WithInput(map[string]any{"theme": "medieval"}),)if err != nil { log.Fatal(err)}
var output map[string]anyif err := resp.Output(&output); err != nil { log.Fatal(err)}
log.Println(output["dishname"])log.Println(output["description"])You have several options for defining schemas in a .prompt file: Dotprompt’s
own schema definition format, Picoschema; standard JSON Schema; or, as
references to schemas defined in your application code. The following sections
describe each of these options in more detail.
Picoschema
Section titled “Picoschema”The schemas in the example above are defined in a format called Picoschema. Picoschema is a compact, YAML-optimized schema definition format that makes it easy to define the most important attributes of a schema for LLM usage. Here’s a longer example of a schema, which specifies the information an app might store about an article:
schema: title: string # string, number, and boolean types are defined like this subtitle?: string # optional fields are marked with a `?` draft?: boolean, true when in draft state status?(enum, approval status): [PENDING, APPROVED] date: string, the date of publication e.g. '2024-04-09' # descriptions follow a comma tags(array, relevant tags for article): string # arrays are denoted via parentheses authors(array): name: string email?: string metadata?(object): # objects are also denoted via parentheses updatedAt?: string, ISO timestamp of last update approvedBy?: integer, id of approver extra?: any, arbitrary extra data (*): string, wildcard fieldThe above schema is equivalent to the following type definitions:
type Article struct { Title string `json:"title"` Subtitle string `json:"subtitle,omitempty" jsonschema:"required=false"` Draft bool `json:"draft,omitempty"` // True when in draft state Status string `json:"status,omitempty" jsonschema:"enum=PENDING,enum=APPROVED"` // Approval status Date string `json:"date"` // The date of publication e.g. '2025-04-07' Tags []string `json:"tags"` // Relevant tags for article Authors []struct { Name string `json:"name"` Email string `json:"email,omitempty"` } `json:"authors"` Metadata struct { UpdatedAt string `json:"updatedAt,omitempty"` // ISO timestamp of last update ApprovedBy int `json:"approvedBy,omitempty"` // ID of approver } `json:"metadata,omitempty"` Extra any `json:"extra"` // Arbitrary extra data}Picoschema supports scalar types string, integer, number, boolean, and
any. Objects, arrays, and enums are denoted by a parenthetical after the field
name.
Objects defined by Picoschema have all properties required unless denoted
optional by ?, and do not allow additional properties. When a property is
marked as optional, it is also made nullable to provide more leniency for LLMs
to return null instead of omitting a field.
In an object definition, the special key (*) can be used to declare a
“wildcard” field definition. This will match any additional properties not
supplied by an explicit key.
JSON schema
Section titled “JSON schema”Picoschema does not support many of the capabilities of full JSON schema. If you require more robust schemas, you may supply a JSON Schema instead:
output: schema: type: object properties: field1: type: number minimum: 20Schema references defined in code
Section titled “Schema references defined in code”In addition to directly defining schemas in the .prompt file, you can
reference a schema registered with genkit.DefineSchemaFor[T]() by name. This
approach lets you define your types once in Go and reference them in prompt
files, giving you compile-time type safety without duplicating schema definitions.
To register a schema from a Go type:
type MenuItem struct { Dishname string `json:"dishname" jsonschema:"description=The name of the menu item"` Description string `json:"description" jsonschema:"description=A description of the menu item"` Calories int `json:"calories" jsonschema:"description=The estimated number of calories"` Allergens []string `json:"allergens" jsonschema:"description=Any known allergens in the menu item"`}
// Register the schema - the name is inferred from the type name ("MenuItem")genkit.DefineSchemaFor[MenuItem](g)Within your prompt, reference the registered schema by name:
---model: googleai/gemini-flash-latestoutput: schema: MenuItem---Invent a menu item for a pirate themed restaurant.The Dotprompt library will automatically resolve the name to the underlying
registered schema. You can then use LookupDataPrompt to get strongly-typed
access to the prompt:
// Input can be any type; here we use a simple structtype MenuRequest struct { Theme string `json:"theme"`}
// Register the input schema toogenkit.DefineSchemaFor[MenuRequest](g)
// Look up the prompt with type informationmenuPrompt := genkit.LookupDataPrompt[MenuRequest, *MenuItem](g, "menu")
// Execute returns strongly-typed outputitem, resp, err := menuPrompt.Execute(ctx, MenuRequest{Theme: "pirate"})if err != nil { log.Fatal(err)}
// item is *MenuItem - fully typedlog.Printf("Dish: %s, Calories: %d", item.Dishname, item.Calories)To register several types in one call, pass zero values to
genkit.DefineSchemasFor():
genkit.DefineSchemasFor(g, MenuRequest{}, MenuItem{})You can also reference registered schemas programmatically using ai.WithOutputSchemaName():
genkit.DefineSchemaFor[MenuItem](g)
resp, err := genkit.Generate(ctx, g, ai.WithPrompt("Invent a menu item for a pirate themed restaurant."), ai.WithOutputSchemaName("MenuItem"),)Tool calling
Section titled “Tool calling”The Dotprompt frontmatter configuration also allows you to select which tools to enable at generate time. Tools are supplied as a list of tool names that must correspond to tools that have been registered with the Genkit instance executing the prompt:
---model: googleai/gemini-pro-latesttools: [search_flights, search_hotels]input: schema: destination: string---
Plan a trip to {{destination}}, using the available tools to find flights and hotels.Tools can also be passed when calling a prompt programmatically:
myPrompt := genkit.LookupPrompt(g, "my_prompt")
resp, err := myPrompt.Execute(ctx, ai.WithInput(map[string]any{"destination": "Lisbon"}), ai.WithTools(searchFlights, searchHotels),)Tools passed at execution replace the ones the prompt was defined with rather than adding to them, so list every tool this run should have.
Prompt templates
Section titled “Prompt templates”The portion of a .prompt file that follows the front matter (if present) is
the prompt itself, which will be passed to the model. While this prompt could be
a simple text string, very often you will want to incorporate user input into
the prompt. To do so, you can specify your prompt using the
Handlebars
templating language. Prompt templates can include placeholders that refer to the values defined by your prompt’s input schema.
You already saw this in action in the section on input and output schemas:
---model: googleai/gemini-flash-latestinput: schema: theme?: string default: theme: "pirate"output: schema: dishname: string description: string calories: integer allergens(array): string---Invent a menu item for a {{theme}} themed restaurant.In this example, the Handlebars expression, {{theme}},
resolves to the value of the input’s theme property when you run the
prompt. To pass input to the prompt:
menuPrompt := genkit.LookupPrompt(g, "menu")
resp, err := menuPrompt.Execute(context.Background(), ai.WithInput(map[string]any{"theme": "medieval"}),)Note that because the input schema declared the theme property to be optional
and provided a default, you could have omitted the property,
and the prompt would have resolved using the default value.
Handlebars templates also support some limited logical constructs. For example,
as an alternative to providing a default, you could define the prompt using
Handlebars’s #if helper:
---model: googleai/gemini-flash-latestinput: schema: theme?: string---Invent a menu item for a {{#if theme}}{{theme}} themed{{/if}} restaurant.In this example, the prompt renders as “Invent a menu item for a restaurant”
when the theme property is unspecified.
See the Handlebars documentation for information on all of the built-in logical helpers.
In addition to properties defined by your input schema, your templates can also refer to values automatically defined by Genkit. The next few sections describe these automatically-defined values and how you can use them.
Multi-message prompts
Section titled “Multi-message prompts”By default, Dotprompt constructs a single message with a “user” role. However, some prompts are best expressed as a combination of multiple messages, such as a system prompt.
The {{role}} helper provides a simple way to
construct multi-message prompts:
---model: googleai/gemini-flash-latestinput: schema: userQuestion: string---{{role "system"}}You are a helpful AI assistant that really loves to talk about food. Try to workfood items into all of your conversations.{{role "user"}}{{userQuestion}}Note that your final prompt must contain at least one user role.
Placing an ongoing conversation
Section titled “Placing an ongoing conversation”The body of a .prompt file is the prompt’s whole conversation, so a file that
uses {{role}} blocks decides where an ongoing conversation goes. Mark the
spot with {{history}}:
---model: googleai/gemini-flash-latestinput: schema: question: string---{{role "system"}}You are a helpful AI assistant named Walt. Keep replies to a few sentences.
{{role "user"}}Who are you?
{{role "model"}}I am Walt. Ask me anything.
{{history}}
{{role "user"}}{{question}}The messages passed to Execute with ai.WithMessages() land at
{{history}}. A template with no {{history}} marker still receives them:
Dotprompt inserts them immediately before the template’s final user message.
resp, err := chatPrompt.Execute(ctx, ai.WithInput(map[string]any{"question": "What did I just ask you?"}), ai.WithMessages(history...),)Message text passed this way is used verbatim and is never compiled, so a user
turn containing {{ reaches the model as the user wrote it.
A prompt defined in code declares its conversation differently, and that changes where the caller’s messages go. See Where the conversation lands for the full rule.
Multi-modal prompts
Section titled “Multi-modal prompts”For models that support multimodal input, such as images alongside text, you can
use the {{media}} helper:
---model: googleai/gemini-flash-latestinput: schema: photoUrl: string---Describe this image in a detailed paragraph:
{{media url=photoUrl}}The URL can be https: or base64-encoded data: URIs for “inline” image usage.
In code, this would be:
multimodalPrompt := genkit.LookupPrompt(g, "multimodal")
resp, err := multimodalPrompt.Execute(context.Background(), ai.WithInput(map[string]any{"photoUrl": "https://example.com/photo.jpg"}),)See also Multimodal input, on the Generating content
page, for an example of constructing a data: URL.
Partials
Section titled “Partials”Partials are reusable templates that can be included inside any prompt. Partials can be especially helpful for related prompts that share common behavior.
When loading a prompt directory, any file prefixed with an underscore (_) is
considered a partial. So a file _personality.prompt might contain:
You should speak like a {{#if style}}{{style}}{{else}}helpful assistant.{{/if}}.This can then be included in other prompts:
---model: googleai/gemini-flash-latestinput: schema: name: string style?: string---
{{role "system"}}{{>personality style=style}}
{{role "user"}}Give the user a friendly greeting.
User's Name: {{name}}Partials are inserted using the
{{>NAME_OF_PARTIAL args...}}
syntax. If no arguments are provided to the partial, it executes with the same
context as the parent prompt.
Partials accept both named arguments as above or a single positional argument representing the context. This can be helpful for tasks such as rendering members of a list.
_destination.prompt
- {{name}} ({{country}})chooseDestination.prompt
---model: googleai/gemini-flash-latestinput: schema: destinations(array): name: string country: string---Help the user decide between these vacation destinations:
{{#each destinations}}{{>destination this}}{{/each}}Defining partials in code
Section titled “Defining partials in code”You can also define partials in code:
genkit.DefinePartial(g, "personality", "Talk like a {{#if style}}{{style}}{{else}}helpful assistant{{/if}}.")Code-defined partials are available in all prompts. Templates are compiled when a prompt runs, so a partial only has to be registered before the first execution, not before the prompt is defined.
Defining custom helpers
Section titled “Defining custom helpers”You can define custom helpers to process and manage data inside of a prompt. Helpers are registered globally:
genkit.DefineHelper(g, "shout", func(input string) string { return strings.ToUpper(input)})A helper receives the value at the Go type the input arrived as, so prefer
scalar parameters: a slice field arrives as []string when the input was a
struct but as []any when it was a map[string]any. Helpers are also
registered on the Genkit instance and, like partials, only have to be
registered before the prompt first runs. A partial may call a helper, which is
a convenient way to share one voice across a family of prompts.
Once a helper is defined you can use it in any prompt:
---model: googleai/gemini-flash-latestinput: schema: name: string---
HELLO, {{shout name}}!!!Prompt variants
Section titled “Prompt variants”Because prompt files are just text, you can (and should!) commit them to your version control system, allowing you to compare changes over time easily. Often, tweaked versions of prompts can only be fully tested in a production environment side-by-side with existing versions. Dotprompt supports this through its variants feature.
To create a variant, create a [name].[variant].prompt file. For instance, if
you were using Gemini 2.0 Flash in your prompt but wanted to see if Gemini 2.5
Pro would perform better, you might create two files:
my_prompt.prompt: the “baseline” promptmy_prompt.gemini25pro.prompt: a variant namedgemini25pro
To use a prompt variant:
Specify the variant in the prompt name when loading:
myPrompt := genkit.LookupPrompt(g, "my_prompt.gemini25pro")The name of the variant is included in the metadata of generation traces, so you can compare and contrast actual performance between variants in the Genkit trace inspector.
Defining prompts in code
Section titled “Defining prompts in code”All of the examples discussed so far have assumed that your prompts are defined
in individual .prompt files in a single directory (or subdirectories thereof),
accessible to your app at runtime. Dotprompt is designed around this setup, and
its authors consider it to be the best developer experience overall.
However, if you have use cases that are not well supported by this setup, you can also define prompts in code:
For strongly-typed prompts defined in code, use genkit.DefineDataPrompt[In, Out]().
This is the recommended approach as it provides compile-time type safety for both
input and output, making your code more idiomatic and less error-prone:
type GeoQuery struct { CountryCount int `json:"countryCount" jsonschema:"default=10"`}
type CountryList struct { Countries []string `json:"countries" jsonschema:"description=List of country names"`}
geographyPrompt := genkit.DefineDataPrompt[GeoQuery, *CountryList]( g, "GeographyPrompt", ai.WithModel(googlegenai.GoogleAIModelRef("gemini-flash-latest", nil)), ai.WithSystem("You are a geography teacher. Respond only when the user asks about geography."), ai.WithPrompt("Give me the {{countryCount}} biggest countries in the world by inhabitants."),)
// Execute returns strongly-typed output directlylist, resp, err := geographyPrompt.Execute(ctx, GeoQuery{CountryCount: 15})if err != nil { log.Fatal(err)}
log.Printf("Countries: %v", list.Countries)With DefineDataPrompt, the input and output schemas are automatically inferred
from the Go type parameters, and the output is returned directly as the typed value.
For streaming with typed prompts, use ExecuteStream():
for result, err := range geographyPrompt.ExecuteStream(ctx, GeoQuery{CountryCount: 15}) { if err != nil { log.Fatal(err) } if result.Done { log.Printf("Final countries: %v", result.Output.Countries) break } // Stream partial results as they arrive if len(result.Chunk.Countries) > 0 { log.Printf("Got %d countries so far...", len(result.Chunk.Countries)) }}Using DefinePrompt (untyped)
Section titled “Using DefinePrompt (untyped)”For cases where you need more flexibility or dynamic typing, you can use the
untyped genkit.DefinePrompt() function:
geographyPrompt := genkit.DefinePrompt( g, "GeographyPrompt", ai.WithSystem("You are a geography teacher. Respond only when the user asks about geography."), ai.WithPrompt("Give me the {{countryCount}} biggest countries in the world by inhabitants."), ai.WithConfig(&genai.GenerateContentConfig{Temperature: genai.Ptr[float32](0.5)}), ai.WithInputType(GeoQuery{CountryCount: 10}), // Defaults to 10. ai.WithOutputType(CountryList{}),)
resp, err := geographyPrompt.Execute(context.Background(), ai.WithInput(GeoQuery{CountryCount: 15}))if err != nil { log.Fatal(err)}
var list CountryListif err := resp.Output(&list); err != nil { log.Fatal(err)}
log.Printf("Countries: %v", list.Countries)Content slots
Section titled “Content slots”A prompt has four content slots, and it renders them in a fixed order: the system message, then the conversation, then the user prompt. Context documents ride alongside as the request’s retrieved context rather than as a message.
Each slot can be filled from a template string or from a function over the prompt’s input:
| Slot | Template form | Function form |
|---|---|---|
| System | ai.WithSystem | ai.WithSystemFn, ai.WithSystemPartsFn |
| Conversation | ai.WithMessagesTemplate | ai.WithMessagesFn |
| User prompt | ai.WithPrompt | ai.WithPromptFn, ai.WithPromptPartsFn |
| Context documents | none | ai.WithDocsFn |
The static forms ai.WithSystemParts, ai.WithPromptParts, ai.WithMessages,
ai.WithDocs, and ai.WithTextDocs take values you already have, so they are
neither templated nor computed.
func WithSystem(text string, args ...any) PromptingOptionfunc WithSystemFn[In any](fn func(context.Context, In) (string, error)) PromptingOptionfunc WithSystemParts(parts ...*Part) PromptingOptionfunc WithSystemPartsFn[In any](fn func(context.Context, In) ([]*Part, error)) PromptingOption
func WithMessagesTemplate(text string, args ...any) PromptOptionfunc WithMessages(messages ...*Message) CommonGenOptionfunc WithMessagesFn[In any](fn func(context.Context, In) ([]*Message, error)) CommonGenOption
func WithPrompt(text string, args ...any) PromptingOptionfunc WithPromptFn[In any](fn func(context.Context, In) (string, error)) PromptingOptionfunc WithPromptParts(parts ...*Part) PromptingOptionfunc WithPromptPartsFn[In any](fn func(context.Context, In) ([]*Part, error)) PromptingOption
func WithTextDocs(text ...string) DocumentOptionfunc WithDocs(docs ...*Document) DocumentOptionfunc WithDocsFn[In any](fn func(context.Context, In) ([]*Document, error)) PromptOptionThe rule that decides between the two forms: template text is yours, so it is compiled; what a function returns is content you already produced, so it is sent verbatim. Use a template when the wording is fixed and only values vary, and a function when the content itself depends on the data, such as branching on a field, attaching an image, or querying a retriever. Because a function’s result is never compiled, it can safely carry user-supplied text with literal handlebars braces in it.
Every content function declares its own Go input type. Genkit converts
whatever input arrived into that type before calling: a value passed to
ai.WithInput(), the default recorded by ai.WithInputType(), and the
map[string]any the Dev UI and the HTTP reflection API deliver all reach the
function as In. When the value cannot be converted, the function is not
called and the error wraps ai.ErrInputTypeMismatch, naming the option that
rejected it.
Both examples below work from one input type:
type SupportRequest struct { Area string `json:"area"` Tier string `json:"tier"` Question string `json:"question"` Screenshot string `json:"screenshot,omitempty"` ScreenshotType string `json:"screenshotType,omitempty"`}Here is the template form, filling three slots from that input:
triagePrompt := genkit.DefineDataPrompt[SupportRequest, Triage](g, "triage", ai.WithModelName("googleai/gemini-flash-latest"),
// System and user text are compiled against the input, so they can // reference its fields. ai.WithSystem("You are a support triage assistant. Classify the {{area}} question from this {{tier}} customer."),
// Each {{role}} block starts a new message, so one string carries a // worked example ahead of the real conversation. ai.WithMessagesTemplate(`{{role "user"}}My deploys started failing with a 401 right after I rotated keys.{{role "model"}}{"category": "bug", "urgency": "high", "summary": "Deploys fail with a 401 after a key rotation."}{{history}}`),
// An interpolated value is substituted, not compiled, so a question // containing {{#if}} reaches the model as the customer wrote it. ai.WithPrompt("{{question}}"),
ai.WithTextDocs("Invoices are issued on the first of the month."),)And the same four slots in the function form:
supportPrompt := genkit.DefinePrompt(g, "support", ai.WithModelName("googleai/gemini-flash-latest"),
// WithInputType fixes the type every content function receives, and its // values are the defaults when the caller supplies none. ai.WithInputType(SupportRequest{Area: "api", Tier: "free"}),
// System slot: the instruction branches on the data, so a function fits. ai.WithSystemFn(func(ctx context.Context, in SupportRequest) (string, error) { var b strings.Builder b.WriteString("You are a support agent. Answer from the reference material you were given.") if in.Tier == "enterprise" { b.WriteString(" Be thorough, and offer to escalate to an engineer.") } return b.String(), nil }),
// Conversation slot: declaring it means owning it, so read the caller's // messages here and decide what to keep. ai.WithMessagesFn(func(ctx context.Context, in SupportRequest) ([]*ai.Message, error) { history := ai.HistoryFromContext(ctx) if len(history) > maxHistoryMessages { history = history[len(history)-maxHistoryMessages:] } return history, nil }),
// User prompt slot: parts reach non-text content, which a string // function cannot. ai.WithPromptPartsFn(func(ctx context.Context, in SupportRequest) ([]*ai.Part, error) { parts := []*ai.Part{ai.NewTextPart(in.Question)} if in.Screenshot != "" { parts = append(parts, ai.NewMediaPart(in.ScreenshotType, in.Screenshot)) } return parts, nil }),
// Context documents slot: retrieval driven by the input. ai.WithDocsFn(func(ctx context.Context, in SupportRequest) ([]*ai.Document, error) { return retrieve(ctx, in.Area) }),)The basic-prompt-content sample runs both of these side by side against the same input, so a trace shows what each form produced.
Where the conversation lands
Section titled “Where the conversation lands”Messages passed to Prompt.Execute() with ai.WithMessages() are placed by
exactly one of three rules, decided by what the prompt itself declared:
-
The prompt declares no conversation. The caller’s messages are used as the conversation directly, between the system message and the user prompt. This is the common case and needs nothing from you.
-
The prompt declares
ai.WithMessages()orai.WithMessagesFn(). The prompt owns the slot, so the caller’s messages are not spliced in on top of it. A function reads them withai.HistoryFromContext(ctx)and returns them wherever it wants them. -
The prompt declares
ai.WithMessagesTemplate(). The caller’s messages go where the template puts{{history}}, or, if the template has no{{history}}marker, immediately before its finalusermessage.Rule 2 is the one worth remembering. A prompt that prepends few-shot examples with
ai.WithMessages()silently stops receiving the caller’s conversation, because only the prompt knows where its examples end and the real conversation begins. Read the conversation back and place it yourself:
ai.WithMessagesFn(func(ctx context.Context, in SupportRequest) ([]*ai.Message, error) { examples := []*ai.Message{ ai.NewUserTextMessage("Example: my deploys fail with a 401."), ai.NewModelTextMessage("That is usually a rotated key."), } return append(examples, ai.HistoryFromContext(ctx)...), nil})Reading the conversation is also what makes summarizing or truncating it possible, which is why the function-form prompt above keeps only the last few messages.
ai.HistoryFromContext() returns the caller’s own slice, so treat it as
read-only; Render clones whatever it places into the request. Its writing
counterpart, ai.NewHistoryContext(), attaches a conversation for the next
Render call to place. Prompt.Execute() calls it for you, so you only need
it when you drive Render and genkit.GenerateWithRequest() by hand. Scope
it to the Render call alone, never to the generate call, or the conversation
rides into every tool handler and nested prompt inside the generate loop.
Untyped content functions
Section titled “Untyped content functions”The typed signature is the recommended one, but an untyped content function
compiles too. The options are generic in In, so a function taking
input any infers In as any and receives the value untouched. The
aliases ai.PromptFn, ai.MessagesFn, ai.PartsFn, and ai.DocsFn are all
still there.
var systemFn ai.PromptFn = func(ctx context.Context, input any) (string, error) { in, ok := input.(SupportRequest) if !ok { return "You are a support agent.", nil } return "You are a support agent for the " + in.Area + " area.", nil}
genkit.DefinePrompt(g, "support", ai.WithInputType(SupportRequest{}), ai.WithSystemFn(systemFn),)Declaring the concrete type is worth the one-line change, because it removes
that type assertion. An assertion like the one above holds when the prompt is
called in process with a SupportRequest and fails when the same prompt is
run from the Dev UI or over HTTP, where the input arrives as a
map[string]any. The typed form converts for you, so both paths land on the
same value.
How options merge
Section titled “How options merge”Options are ordinary functional options: pass as many as you like, in any order, and they merge left to right under two rules.
Collections accumulate. Repeating an option, or mixing its variants,
appends in the order the options are passed: ai.WithMessages and
ai.WithMessagesFn; ai.WithTools; ai.WithResources; ai.WithUse;
ai.WithDocs, ai.WithTextDocs, and ai.WithDocsFn (fixed documents first,
then computed ones).
Single slots take the last one set. The system slot, the user prompt
slot, ai.WithMessagesTemplate, ai.WithModel and ai.WithModelName,
ai.WithConfig, the output schema and format options, ai.WithToolChoice,
ai.WithMaxTurns, and the input configuration (ai.WithInputType,
ai.WithInputSchema, ai.WithInputSchemaName, which replace schema and
default input together). A zero value never fills a slot, so
ai.WithMaxTurns(0) and ai.WithConfig(nil) are no-ops and cannot unset an
earlier value. Within a slot the loser is cleared rather than merged:
ai.WithSystem() followed by ai.WithSystemParts() means the parts win
outright and the text is not prepended to them.
opts := []ai.PromptOption{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("You are a helpful assistant."), ai.WithTools(searchTool),}opts = append(opts, ai.WithTools(adminTools...)) // accumulates onto searchToolopts = append(opts, ai.WithSystem("Be terse.")) // same slot, so this wins
myPrompt := genkit.DefinePrompt(g, "assistant", opts...)One combination is refused outright: passing ai.WithMessagesTemplate() in
the same DefinePrompt() or DefineDataPrompt() call as ai.WithMessages()
or ai.WithMessagesFn() panics at definition. A template lays the whole
conversation out, down to where {{history}} goes, so separately supplied
messages have no position relative to it. Write them as {{role}} blocks in
the template, or drop the template and build the conversation from
ai.WithMessages() and ai.WithMessagesFn().
At execution time the rules are different: options passed to
Prompt.Execute() override what the prompt was defined with rather than
adding to it. That applies to the model, the config, the tools, and the
context documents. Documents are the one to watch, because passing
ai.WithDocs() or ai.WithTextDocs() to Execute skips the prompt’s
ai.WithDocsFn() entirely rather than resolving it and discarding the
result. That is deliberate, so a caller who already has the documents does
not pay for a retriever query, but it does mean a prompt author who sees
their retriever never run should look at the call site first.
Rendering prompts
Section titled “Rendering prompts”Prompts may also be rendered into a GenerateActionOptions which may then be
processed and passed into genkit.GenerateWithRequest():
actionOpts, err := geographyPrompt.Render(ctx, GeoQuery{CountryCount: 15})if err != nil { log.Fatal(err)}
// Do something with the value...actionOpts.Config = &genai.GenerateContentConfig{Temperature: genai.Ptr[float32](0.8)}
resp, err := genkit.GenerateWithRequest(ctx, g, actionOpts, nil, nil) // No middleware or streamingPrompt options carry over to GenerateActionOptions, including middleware
attached with ai.WithUse(), which travels as a name and config reference.
The exception is the deprecated ai.WithMiddleware(): those values are
ignored by Prompt.Render() and must be passed to
genkit.GenerateWithRequest() as its mw argument instead.
Prompt.Render() is also where ai.NewHistoryContext() comes in. Scope it to
the render call so the prompt can place the conversation, and run the
generation on the original context:
actionOpts, err := chatPrompt.Render(ai.NewHistoryContext(ctx, history), input)if err != nil { return nil, err}
return genkit.GenerateWithRequest(ctx, g, actionOpts, nil, nil)Next steps
Section titled “Next steps”- Learn about tool calling to give your prompts access to external functions and APIs
- Explore retrieval-augmented generation (RAG) to incorporate external knowledge into your prompts
- See creating flows to build complex AI workflows using your prompts
- Check out the evaluation guide for testing and improving your prompt performance