DEV Community

Cover image for Prompts Are Code. Genkit Makes the Runtime Reviewable.
Raju Dandigam
Raju Dandigam

Posted on

Prompts Are Code. Genkit Makes the Runtime Reviewable.

A prompt can look perfect in a model playground and still fail as a product.

The production input arrives in a different shape. Authentication data leaks into the prompt. The model returns prose where the UI expects JSON. A three-line wording change improves two examples and breaks eight others. Nobody can explain which version generated yesterday’s result.

The lesson is not that prompts are unimportant.

It is that a prompt is one artifact inside a larger runtime.

Genkit is useful for TypeScript teams because it connects prompts, flows, schemas, tools, context, traces, and evaluations in one application model. The result is not automatically reliable—but the important boundaries become reviewable.

Series note: This is Part 8 of Reliable Google AI Agents in TypeScript. The examples were checked against Genkit 1.42.0 and @genkit-ai/google-genai 1.42.0 in September 2026.

Put the prompt in a versioned artifact

Genkit’s Dotprompt format stores template content, model configuration, schemas, and tool-loop settings in .prompt files.

---
model: googleai/gemini-flash-latest
config:
  temperature: 0.3
input:
  schema:
    hotelName: string
    oldPrice: number
    newPrice: number
output:
  schema:
    title: "string"
    body: string
---
Write a concise price-drop alert for {{hotelName}}.
The price changed from {{oldPrice}} to {{newPrice}}.
Do not invent amenities, policies, or availability.
Enter fullscreen mode Exit fullscreen mode

Keeping this in prompts/price-alert.prompt produces several practical benefits:

  • prompt changes appear in Git review;
  • model and generation settings travel with the template;
  • input and output expectations are visible;
  • the Developer UI can iterate on the prompt without burying it in service code;
  • variants can be evaluated against the same examples.

But a .prompt file should not own authorization, retries, billing rules, or irreversible side effects.

Wrap the prompt in a typed flow

A Genkit flow creates the application boundary around generation. It has a stable name, validated input and output, trace identity, and a deployment surface.

import { genkit, z } from "genkit";
import { googleAI } from "@genkit-ai/google-genai";

const ai = genkit({
  plugins: [googleAI()],
});

const AlertInput = z.object({
  hotelName: z.string().min(1),
  oldPrice: z.number().positive(),
  newPrice: z.number().positive(),
});

const AlertCopy = z.object({
  title: z.string().min(1),
  body: z.string().min(1),
});

const AlertOutput = AlertCopy.extend({
  shouldSend: z.boolean(),
});

const priceAlertPrompt = ai.prompt("price-alert");

export const priceAlertFlow = ai.defineFlow(
  {
    name: "priceAlertFlow",
    inputSchema: AlertInput,
    outputSchema: AlertOutput,
  },
  async (input) => {
    if (input.newPrice >= input.oldPrice) {
      return { title: "", body: "", shouldSend: false };
    }

    const { output } = await priceAlertPrompt(input);
    const copy = AlertCopy.parse(output);

    return { ...copy, shouldSend: true };
  },
);
Enter fullscreen mode Exit fullscreen mode

The price rule runs before the model. Zod validates the generated structure after it. Gemini is responsible for bounded language generation—not for redefining whether a price actually dropped.

That separation is the difference between a prompt demo and an application workflow.

Keep execution context out of model input

AI applications often have data the code needs but the model does not.

An auth token may be needed to call a user-scoped service. A tenant ID may be required for a database query. Neither should be interpolated into the prompt merely because a tool needs it.

Genkit’s context object provides a side channel that propagates through flows, prompts, and tools.

const TripSummary = z.object({
  tripId: z.string(),
  destination: z.string(),
});

const searchUserTrips = ai.defineTool(
  {
    name: "searchUserTrips",
    description: "Search trips owned by the signed-in user",
    inputSchema: z.object({ query: z.string() }),
    outputSchema: z.array(TripSummary),
  },
  async ({ query }, { context }) => {
    const userId = context.auth?.uid;

    if (!userId) {
      throw new Error("Authentication required");
    }

    return tripStore.search({ userId, query });
  },
);
Enter fullscreen mode Exit fullscreen mode

The model can decide to call searchUserTrips. The tool derives ownership from trusted execution context rather than a model-generated user ID.

That is a small design decision with a large security effect.

Context is still not automatically trusted. The HTTP or function binding that invokes the flow must authenticate the request and populate context.auth. Genkit propagates the object; your boundary establishes its integrity.

Treat model output as untrusted data

An output schema improves the boundary, but structured output is not business authorization.

For example, the model could generate a valid alert body that mentions an unavailable amenity. Or it could return a valid title for a user who opted out of notifications.

Use different controls for different failure classes:

Failure Appropriate control
Wrong field type Zod/output schema
User opted out Deterministic policy before generation
Unsupported factual claim Grounding plus evidence validation
Poor tone Evaluator or human review
Duplicate delivery Idempotent side-effect boundary

A schema answers “Can downstream code safely parse this?” It does not answer “Should the business act on it?”

Observe the whole flow

Genkit traces flows, model calls, tools, and intermediate operations in the Developer UI. Its telemetry is based on OpenTelemetry, while production export depends on the telemetry and observability integration you configure.

That creates two complementary views:

  • The native Genkit trace shows the actual flow, prompt/model operation, tools, latency, and schema failures.
  • A framework-neutral evidence layer can express a stable cross-framework contract for pull-request review or CI.

For example:

priceAlertFlow
├─ validate_input
├─ business_rule          price_drop=true
├─ prompt.price-alert     version=git:8f3c2a1
├─ validate_output        schema=AlertCopy
└─ return                 shouldSend=true
Enter fullscreen mode Exit fullscreen mode

AgentInspect can complement the native trace when the application maps these important operations into a run and wants deterministic required/forbidden checks or a redacted review artifact. It should not duplicate every span, and this article does not imply a first-class Genkit adapter already exists.

The stable contract is more important than owning every telemetry signal.

Evaluate changes against a dataset

Prompt review by intuition is not enough.

Genkit supports evaluation datasets and evaluators through its tooling. A useful price-alert dataset should include:

[
  {
    "input": {
      "hotelName": "Example Hotel",
      "oldPrice": 240,
      "newPrice": 190
    },
    "reference": {
      "shouldSend": true
    }
  },
  {
    "input": {
      "hotelName": "Example Hotel",
      "oldPrice": 190,
      "newPrice": 240
    },
    "reference": {
      "shouldSend": false
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

The real suite should also include:

  • equal prices;
  • missing or malformed input;
  • unusually large values;
  • prompt injection inside hotelName;
  • unsupported amenity claims;
  • an unauthenticated tool request;
  • a consent-policy block;
  • a duplicate delivery attempt.

Some checks should remain deterministic: schema validity, correct shouldSend, no raw auth data, and no notification for a price increase. Other qualities—clarity, tone, or faithfulness to retrieved evidence—may use an evaluator or human review.

A useful evaluation suite does not require every sentence to match. It asks whether the workflow still satisfies its product contract.

Deploy the flow, not a loose prompt

Genkit flows can be served from a Node.js application with the supported Express integration and deployed to a platform such as Cloud Run.

import { startFlowServer } from "@genkit-ai/express";

startFlowServer({
  flows: [priceAlertFlow],
});
Enter fullscreen mode Exit fullscreen mode

The deployed unit is the workflow with its schema, context handling, tools, and telemetry—not a text template floating independently from the code that constrains it.

That matters operationally:

  • a rollback can restore compatible prompt and code together;
  • a trace identifies the named flow;
  • an evaluation compares versions using the same dataset;
  • a security review can see where trusted context enters;
  • a release gate can distinguish structural failures from subjective quality changes.

The runtime is the product

“Prompts are code” is a useful starting point because it moves prompt work into version control.

Production engineering has to go further.

The prompt is the instruction artifact. The flow is the application contract. Context carries trusted execution data. Tools provide bounded capabilities. Schemas protect downstream code. Traces explain the run. Evaluations protect behavior as the system changes.

Genkit brings those pieces into one TypeScript development model.

That is the level at which a Gemini application becomes maintainable—not when its prompt sounds impressive, but when its complete workflow can be reviewed, tested, observed, and safely changed.

References

Earlier in the series: Gemini Function Calling Is Not an Agent Runtime · Testing Google ADK TypeScript Agents Without Chasing Sentences · From Local Traces to Production Observability for Google AI Agents

Top comments (0)