Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
150842 stories
·
33 followers

How to Build AI-Powered Flutter Applications with Genkit Dart – Full Handbook for Devs

1 Share

There's a particular kind of frustration that every mobile developer has felt at some point. You're building a Flutter application, and you want to add an AI feature.

Perhaps it's something that reads a photo and describes what's in it, or something that analyzes text and returns a structured result.

Suddenly you're drowning in provider-specific SDKs, ad-hoc JSON parsing, hand-rolled HTTP wrappers, and zero visibility into what the model is actually doing under the hood. You're not building your app anymore. You are building infrastructure.

This is the problem Genkit was created to solve. And with the arrival of Genkit Dart, the same solution is now in the hands of every Dart and Flutter developer on the planet.

In this guide, you'll learn what Genkit Dart is, how it thinks, every major thing it can do, and why those things matter before a single line of Flutter code is written.

Then, once that foundation is solid, you'll build a complete item identification application that opens the device camera, captures a photo, sends it to a multimodal AI model, and returns a structured, typed description of whatever was photographed.

Table of Contents

Prerequisites

To follow this guide and build the item identification project, you'll need to meet several technical requirements. Make sure your environment is configured with the following versions or higher:

  1. Dart SDK version 3.5.0 or later is required to support the latest macro and type system features.

  2. Flutter SDK version 3.24.0 or later ensures compatibility with the latest plugin architectures.

  3. An API key from a supported provider is necessary. For this guide, I recommend a Google AI Studio API key for Gemini.

  4. Basic familiarity with asynchronous programming in Dart is expected, specifically the use of Future and await keywords.

You will also need a physical device or an emulator with camera support to test the project. Because we'll be capturing images and processing them, a physical mobile device typically provides the most reliable testing experience.

What Is Genkit?

Genkit is an open-source framework built by Google for constructing AI-powered applications. It wasn't designed for any single language or runtime. The framework has been available for TypeScript and Go since its initial release, and it has since expanded to Python and, most recently, Dart.

Each language implementation follows the same philosophy: give developers a consistent, provider-agnostic way to define, run, test, and deploy AI logic.

The word "framework" here means something specific. Genkit isn't a thin wrapper around a single provider's API. It's a full toolkit that includes a model abstraction layer, a flow definition system, a schema system for type-safe structured output, a tool-calling interface, streaming support, multi-agent pattern utilities, retrieval-augmented generation helpers, and an observability layer that tracks every call and every token as it moves through your application.

It also ships with a visual developer interface that runs on localhost so you can inspect and test everything without writing a test file.

The reason this matters for Dart developers is that Genkit Dart isn't a port of the TypeScript version with Dart syntax substituted in. It's a native Dart implementation, built to feel like idiomatic Dart code, and it plugs directly into the Flutter development workflow through its CLI tooling.

The Problem It Solves

When you use a model provider directly, every provider is its own world. If you start with Google's Gemini API and later decide you want to compare results with Anthropic's Claude, you are adding a second SDK, learning a second API contract, and writing adapter code to normalize the two different response shapes.

If you then decide that for one particular flow you want to use xAI's Grok because it handles a specific kind of reasoning better, you add a third SDK.

Three SDKs, three authentication patterns, three response parsing strategies, and zero unified observability across any of them.

Genkit collapses this into a single interface. You initialize Genkit with a list of plugins representing the providers you want to use. From that point on, you call ai.generate() regardless of which provider is underneath. You switch providers by changing one argument. The rest of your application code stays exactly as it was.

This is model-agnostic design, and it's the single most important architectural decision in Genkit's design.

Why Genkit Dart Changes Everything for Flutter Developers

Flutter's core premise has always been that you write your application logic once and it runs correctly on Android, iOS, web, macOS, Windows, and Linux. Genkit Dart extends this premise to AI logic specifically. You write your AI flows once, in Dart, and you run them wherever Dart runs.

This has a practical consequence that's easy to underestimate. In most mobile AI architectures, there's a hard wall between the mobile client and the AI backend. The client is in Kotlin, Swift, or Dart. The backend is in Python, TypeScript, or Go. The schemas defined on the backend to describe what a flow expects as input and what it returns as output don't exist on the client. The client sends JSON and receives JSON, and both sides maintain their own understanding of what that JSON means. When the backend schema changes, the client breaks silently.

With Genkit Dart, the backend and the Flutter client are both in Dart. They share the same schema definitions. When your AI flow expects a ScanRequest object and returns an ItemDescription object, both the server and the Flutter app use the same generated Dart classes. Change the schema in one place and the Dart type system catches every mismatch everywhere. This is end-to-end type safety across the client-server boundary, and it is possible only because Dart runs on both sides.

The other thing worth saying plainly is that Genkit Dart is still in preview as of this writing. It's not version 1.0. Some APIs may shift. But the fundamentals, the flow system, the model abstraction, the schemantic integration, and the CLI tooling are already stable enough to build serious applications with, and the trajectory is clearly toward a production-ready release.

Core Concepts

Before looking at code in detail, it helps to have a clear mental model of the four entities that every Genkit application is built from.

The Genkit Instance

Everything in a Genkit application starts with creating a Genkit instance. This is the object that holds the configuration for your application, including which model provider plugins are active.

You pass a list of plugins when constructing it, and from that point forward you use the instance to register flows, define tools, and call models.

import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';

final ai = Genkit(plugins: [googleAI()]);

The Genkit constructor takes a plugins list. Each plugin registers its models and capabilities with the instance. Once the plugin is registered, its models are available through the instance's generate method.

Plugins

Plugins are the bridge between the generic Genkit API and a specific provider's actual HTTP endpoint.

The googleAI() function, for example, configures the plugin that knows how to talk to the Google Generative AI service, authenticate requests using your API key from the environment, and translate Genkit's model calls into the specific request format that the Gemini API expects. You never write that translation code yourself. The plugin handles it entirely.

Flows

A flow is the primary unit of AI work in Genkit. A flow is a Dart function that accepts a typed input, performs AI-related work (which might be a model call, a sequence of model calls, tool use, or a combination of all three), and returns a typed output.

What makes a flow different from a regular function is the scaffolding Genkit wraps around it: tracing, observability, Developer UI integration, the ability to expose the flow as an HTTP endpoint, and schema enforcement on both the input and the output.

You define a flow using ai.defineFlow(). You call a flow exactly like a function.

Schemas

Schemas define the shape of data that flows with into and out of AI operations. They are defined using the schemantic package, which uses Dart code generation to produce strongly typed classes from abstract class definitions annotated with @Schema(). This means your AI inputs and outputs are not maps or dynamic objects. They are real Dart types with compile-time safety.

Every AI Provider Supported by Genkit Dart

This is one of Genkit's greatest strengths, and it deserves a full treatment. As of the current preview, Genkit Dart supports the following providers as plugins.

Google Generative AI (Gemini)

Package: genkit_google_genai

This is the plugin for Google's Gemini family of models accessed through the Google AI Studio API key. It covers the full Gemini lineup including Gemini 2.5 Flash, Gemini 2.5 Pro, and multimodal variants capable of processing text, images, audio, and video. The free tier for the Gemini API is generous, which makes it the default recommendation for getting started.

import 'package:genkit_google_genai/genkit_google_genai.dart';

final ai = Genkit(plugins: [googleAI()]);

final result = await ai.generate(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: 'What is the capital of Nigeria?',
);

The API key is read automatically from the GEMINI_API_KEY environment variable. You set it once and every subsequent call to this plugin uses it without any explicit configuration in the code.

Google Vertex AI

Package: genkit_vertexai

Vertex AI is Google's enterprise-grade AI platform. Unlike the Google AI Studio endpoint, Vertex AI is authenticated through Google Cloud credentials, making it the appropriate choice for production systems that need access controls, audit logs, regional data residency options, and integration with other Google Cloud services. It also gives access to Gemini models through a Google Cloud project, plus embedding models for vector search.

import 'package:genkit_vertexai/genkit_vertexai.dart';

final ai = Genkit(plugins: [
  vertexAI(projectId: 'your-project-id', location: 'us-central1'),
]);

final result = await ai.generate(
  model: vertexAI.gemini('gemini-2.5-pro'),
  prompt: 'Summarize the following contract clause...',
);

Anthropic (Claude)

Package: genkit_anthropic

Anthropic's Claude models are available directly through the Anthropic plugin. Claude is known for strong reasoning, careful instruction-following, and a tendency to be conservative rather than hallucinate. If you're building an application where accuracy and careful handling of ambiguous instructions is more important than raw speed, Claude is worth including in your provider options.

import 'package:genkit_anthropic/genkit_anthropic.dart';

final ai = Genkit(plugins: [anthropic()]);

final result = await ai.generate(
  model: anthropic.model('claude-opus-4-5'),
  prompt: 'Review this code for security vulnerabilities.',
);

The API key is read from the ANTHROPIC_API_KEY environment variable.

OpenAI (GPT) and OpenAI-Compatible APIs

Package: genkit_openai

This single package covers two distinct use cases, and understanding both is important.

The first is straightforward: it gives you access to OpenAI's GPT-4o, GPT-4 Turbo, and the rest of the OpenAI model catalog. Many teams already have OpenAI integrations in other parts of their infrastructure. This plugin lets you bring those models into the Genkit interface alongside your other providers without learning a second SDK.

import 'package:genkit_openai/genkit_openai.dart';
 
final ai = Genkit(plugins: [openAI()]);
 
final result = await ai.generate(
  model: openAI.model('gpt-4o'),
  prompt: 'Write a unit test for the following function.',
);

The API key is read from the OPENAI_API_KEY environment variable.

The second use case is where this plugin really earns its value. The openAI plugin accepts a custom baseUrl parameter, which means it can communicate with any HTTP API that follows the OpenAI request and response format. This includes a large number of providers and services that have adopted the OpenAI protocol as a standard interface.

The practical consequence is that all of the following become available in Genkit Dart without any additional package:

xAI's Grok models are reached by pointing the plugin at xAI's API endpoint. Grok is designed with strong reasoning and real-time information access, and it's straightforward to include as an alternative or comparison provider.

final ai = Genkit(plugins: [
  openAI(
    apiKey: Platform.environment['XAI_API_KEY']!,
    baseUrl: 'https://api.x.ai/v1',
    models: [
      CustomModelDefinition(
        name: 'grok-3',
        info: ModelInfo(
          label: 'Grok 3',
          supports: {'multiturn': true, 'tools': true, 'systemRole': true},
        ),
      ),
    ],
  ),
]);
 
final result = await ai.generate(
  model: openAI.model('grok-3'),
  prompt: 'Explain the current state of fusion energy research.',
);

DeepSeek's models, particularly DeepSeek-R1 and DeepSeek-V3, have drawn significant attention for delivering strong results on reasoning and coding tasks at comparatively low cost. They're accessed the same way:

final ai = Genkit(plugins: [
  openAI(
    apiKey: Platform.environment['DEEPSEEK_API_KEY']!,
    baseUrl: 'https://api.deepseek.com/v1',
    models: [
      CustomModelDefinition(
        name: 'deepseek-chat',
        info: ModelInfo(
          label: 'DeepSeek Chat',
          supports: {'multiturn': true, 'tools': true, 'systemRole': true},
        ),
      ),
    ],
  ),
]);
 
final result = await ai.generate(
  model: openAI.model('deepseek-chat'),
  prompt: 'Optimize this Dart function for memory efficiency.',
);

Groq's inference platform is also reachable through the same pattern. Groq is known for extremely fast inference speeds, which can be valuable in applications where response latency is the primary constraint:

final ai = Genkit(plugins: [
  openAI(
    apiKey: Platform.environment['GROQ_API_KEY']!,
    baseUrl: 'https://api.groq.com/openai/v1',
    models: [
      CustomModelDefinition(
        name: 'llama-3.3-70b-versatile',
        info: ModelInfo(
          label: 'Llama 3.3 70B (Groq)',
          supports: {'multiturn': true, 'tools': true, 'systemRole': true},
        ),
      ),
    ],
  ),
]);

Together AI and other OpenAI-compatible inference providers follow the identical pattern. You change the baseUrl, the apiKey environment variable name, and the model name string. Everything else in your application – the flows, the schemas, the tool definitions – stays exactly the same.

It's worth being clear about what AWS Bedrock and Azure AI Foundry don't yet support. Both platforms have dedicated plugins in Genkit's TypeScript version. Neither has a Dart plugin as of the current preview.

If your organization's AI infrastructure lives on AWS or Azure, the current path is to host a TypeScript Genkit backend on those platforms and have your Flutter client call it as a remote flow, which is a valid and production-appropriate pattern described in the Flutter architecture section of this guide.

Local Models with llamadart

Package: genkit_llamadart (community plugin)

For scenarios where you need to run models entirely on-device or on your own hardware without any cloud dependency, genkit_llamadart is a community plugin that runs GGUF-format models locally through the llamadart inference engine. This is the appropriate path when data privacy requirements prohibit sending any content to a third-party API, when you need offline-capable AI features, or when you want a development environment that doesn't consume API quota.

import 'package:genkit/genkit.dart';
import 'package:genkit_llamadart/genkit_llamadart.dart';
 
void main() async {
  final plugin = llamaDart(
    models: [
      LlamaModelDefinition(
        name: 'local-llm',
        // Path to a locally downloaded GGUF model file
        modelPath: '/models/llama-3.2-3b-instruct.gguf',
        modelParams: ModelParams(contextSize: 4096),
      ),
    ],
  );
 
  final ai = Genkit(plugins: [plugin]);
 
  final result = await ai.generate(
    model: llamaDart.model('local-llm'),
    prompt: 'Summarize the key points of this document.',
    config: LlamaDartGenerationConfig(
      temperature: 0.3,
      maxTokens: 512,
      enableThinking: false,
    ),
  );
 
  print(result.text);
 
  // Dispose the plugin when done to release native resources
  await plugin.dispose();
}

The plugin supports text generation, streaming, tool calling loops, constrained JSON output for structured responses, and text embeddings. GGUF models can be sourced from Hugging Face or other model hubs.

Good starting points for local experimentation include Llama 3.2 3B Instruct (compact and capable), Phi-3 Mini (very small footprint), and Gemma 3 2B (Google's small open-weight model).

Chrome Built-In AI (Gemini Nano in the Browser)

Package: genkit_chrome (community plugin)

For Flutter Web applications specifically, there's a plugin that runs Google's Gemini Nano model directly inside Chrome using the browser's built-in AI capabilities. This requires no API key, no network call, and no server. The model runs entirely within the browser process.

import 'package:genkit/genkit.dart';
import 'package:genkit_chrome/genkit_chrome.dart';
 
void main() async {
  final ai = Genkit(plugins: [ChromeAIPlugin()]);
 
  final stream = ai.generateStream(
    model: modelRef('chrome/gemini-nano'),
    prompt: 'Suggest three improvements to this paragraph.',
  );
 
  await for (final chunk in stream) {
    print(chunk.text);
  }
}

This plugin requires Chrome 128 or later with specific browser flags enabled. It's experimental and text-only as of the current release. The use cases are niche but real: offline-first web features, low-latency autocomplete where even a round trip to a fast server adds too much delay, and privacy-sensitive features where the user's text should never leave the device.

A Note on the Provider Landscape

The Genkit Dart plugin ecosystem is intentionally focused in this preview period. The four first-party plugins cover the most widely used providers, the OpenAI-compatible mechanism extends reach to a large number of additional services without requiring new packages, and the community plugins fill in local and browser-native use cases. The TypeScript version has more plugins, and as Genkit Dart matures toward a stable release, that gap will narrow.

Watching the pub.dev package namespace for new genkit_* packages is the most reliable way to track what's added.

Switching Between Providers

The single most powerful thing about this provider list is what you can do with it at the Genkit instance level. You can load multiple plugins simultaneously and use different providers for different flows within the same application.

import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
import 'package:genkit_anthropic/genkit_anthropic.dart';
import 'package:genkit_openai/genkit_openai.dart';

final ai = Genkit(plugins: [
  googleAI(),
  anthropic(),
  openAI(),
]);

// Use Gemini for multimodal tasks
final visionResult = await ai.generate(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: [
    Part.media(url: imageUrl),
    Part.text('What is in this image?'),
  ],
);

// Use Claude for document review
final reviewResult = await ai.generate(
  model: anthropic.model('claude-opus-4-5'),
  prompt: contractText,
);

// Use GPT-4o for code generation
final codeResult = await ai.generate(
  model: openAI.model('gpt-4o'),
  prompt: featureDescription,
);

All three calls use the same ai.generate() method. No adapter code. No conversion utilities. No separate authentication setup for each. The provider difference is expressed purely in the model argument.

Flows: The Heart of Genkit

The flow is the most important concept in Genkit. Understanding what a flow is, what wrapping your AI logic inside one gives you, and how flows compose means that you understand most of what Genkit does.

Defining a Basic Flow

At its most stripped-down form, a flow is defined with ai.defineFlow():

import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
import 'package:schemantic/schemantic.dart';

part 'main.g.dart';

@Schema()
abstract class $BookSummaryInput {
  String get title;
  String get author;
}

@Schema()
abstract class $BookSummaryOutput {
  String get summary;
  String get keyThemes;
  int get estimatedReadTimeMinutes;
}

void main() async {
  final ai = Genkit(plugins: [googleAI()]);

  final bookSummaryFlow = ai.defineFlow(
    name: 'bookSummaryFlow',
    inputSchema: BookSummaryInput.$schema,
    outputSchema: BookSummaryOutput.$schema,
    fn: (input, context) async {
      final response = await ai.generate(
        model: googleAI.gemini('gemini-2.5-flash'),
        prompt: 'Provide a summary of the book "${input.title}" '
                'by ${input.author}. Include key themes and estimated '
                'reading time.',
        outputSchema: BookSummaryOutput.$schema,
      );

      if (response.output == null) {
        throw Exception('The model did not return a valid structured response.');
      }

      return response.output!;
    },
  );

  final summary = await bookSummaryFlow(
    BookSummaryInput(title: 'Things Fall Apart', author: 'Chinua Achebe'),
  );

  print(summary.summary);
  print('Key themes: ${summary.keyThemes}');
  print('Estimated reading time: ${summary.estimatedReadTimeMinutes} minutes');
}

Let's walk through each piece of this code.

The @Schema() annotation on \(BookSummaryInput and \)BookSummaryOutput tells the schemantic package that these abstract classes should have concrete Dart classes generated for them. The convention is to prefix the abstract class name with a dollar sign.

After running dart run build_runner build, the generator creates BookSummaryInput and BookSummaryOutput as concrete classes with constructors, JSON serialization, and Genkit schema definitions attached as the $schema static property.

The part 'main.g.dart' directive at the top of the file is the Dart code generation include that brings the generated code into scope.

ai.defineFlow() takes a name, an inputSchema, an outputSchema, and the function fn that contains the actual logic. The name is what identifies this flow in the Developer UI and in CLI commands. The schemas attach type enforcement: Genkit will validate the input before calling fn and validate the output before returning it to the caller.

Inside fn, input is already typed as BookSummaryInput. You access its properties directly through the type system. No input['title'], no null checks on dynamic maps.

The ai.generate() call inside the flow specifies the model, the prompt string, and the same output schema. The model is instructed through schema guidance to return JSON that matches BookSummaryOutput. Genkit validates the returned JSON and makes it available as a typed BookSummaryOutput instance through response.output.

The final call await bookSummaryFlow(BookSummaryInput(...)) invokes the flow exactly like a function call. The return value is typed as BookSummaryOutput.

Why Not Just Call ai.generate() Directly?

This is a reasonable question. If you only need one model call with no surrounding logic, the extra definition step can look like ceremony. Here's what wrapping that call in a flow actually gives you.

First, the Developer UI can discover and test flows but can't discover and test bare ai.generate() calls. When you define a flow, it immediately becomes visible and executable in the local web interface without any additional setup.

Second, flows can be exposed as HTTP endpoints with one line of code. A bare ai.generate() call can't. The deployment story for Genkit AI logic is fundamentally built around flows.

Third, tracing and observability work at the flow level. When you look at a trace in the Developer UI, you see the entire flow execution as a tree: which model was called, with what prompt, what it returned, how long it took, and how many tokens were used. This is not possible with ad-hoc generate calls.

Fourth, flows are the unit of composition in multi-step AI logic. You can call a flow from within another flow, build sequences of AI operations, and have each level of the hierarchy traced and observable independently.

Multi-Step Flows

A flow doesn't have to be a single model call. It can contain any amount of Dart logic, including multiple model calls, conditionals, loops, and calls to external APIs. The entire sequence is traced as a single flow execution.

final productResearchFlow = ai.defineFlow(
  name: 'productResearchFlow',
  inputSchema: ProductQuery.$schema,
  outputSchema: ProductReport.$schema,
  fn: (input, context) async {
    // First model call: extract structured search terms
    final searchTermsResponse = await ai.generate(
      model: googleAI.gemini('gemini-2.5-flash'),
      prompt: 'Extract the top 5 search keywords from this product query: '
              '"${input.query}". Return them as a comma-separated list.',
    );

    final keywords = searchTermsResponse.text;

    // External API call: fetch product data using the keywords
    final products = await fetchProductsFromDatabase(keywords);

    // Second model call: synthesize the findings into a structured report
    final reportResponse = await ai.generate(
      model: googleAI.gemini('gemini-2.5-pro'),
      prompt: 'Based on these products: $products\n\n'
              'Write a concise competitive analysis for: ${input.query}',
      outputSchema: ProductReport.$schema,
    );

    if (reportResponse.output == null) {
      throw Exception('Report generation failed.');
    }

    return reportResponse.output!;
  },
);

Notice that this flow makes two different model calls using two different Gemini variants (Flash for the cheaper extraction task, Pro for the more complex synthesis). It also calls an external Dart function in between. The entire execution, both model calls and the external call, is captured as a single trace.

Type Safety with Schemantic

The schemantic package is what makes Genkit Dart feel genuinely Dart-idiomatic rather than feeling like a TypeScript port. Understanding it fully is important because it underpins every structured output and flow definition in Genkit Dart.

How Schemantic Works

Schemantic is a code generation library. You write abstract classes with getter declarations and annotate them with @Schema(). When you run dart run build_runner build, the generator reads those abstract classes and produces concrete implementation classes with:

  • A constructor that accepts named parameters for each field

  • fromJson(Map<String, dynamic> json) factory constructor for deserialization

  • toJson() method for serialization

  • A static $schema property that holds the Genkit schema definition Genkit uses at runtime to validate inputs and outputs and to instruct models on the expected output format

The @Field() annotation lets you add metadata to individual properties. The most important piece of metadata is the description string, which Genkit includes in the prompt instructions it sends to the model. Better field descriptions produce better structured output because the model understands more precisely what each field should contain.

import 'package:schemantic/schemantic.dart';

part 'schemas.g.dart';

@Schema()
abstract class $ProductScan {
  @Field(description: 'The common name of the product or object identified')
  String get productName;

  @Field(description: 'The primary material the object appears to be made from')
  String get material;

  @Field(description: 'Estimated retail price range in USD')
  String get estimatedPriceRange;

  @Field(description: 'Any visible brand names or logos')
  String? get brandName;

  @Field(description: 'Short description of the item condition')
  String get condition;

  @Field(description: 'Confidence score between 0.0 and 1.0')
  double get confidence;
}

After running the build runner, you can use ProductScan as a concrete class:

final scan = ProductScan(
  productName: 'Stainless Steel Water Bottle',
  material: 'Stainless steel',
  estimatedPriceRange: '\\(20 - \\)40',
  brandName: 'Hydro Flask',
  condition: 'Like new',
  confidence: 0.94,
);

print(scan.toJson());
// {productName: Stainless Steel Water Bottle, material: Stainless steel, ...}

And in a flow:

final response = await ai.generate(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: [...imageAndTextParts],
  outputSchema: ProductScan.$schema,
);

final ProductScan? result = response.output;
if (result != null) {
  print(result.productName);
  print('Confidence: ${result.confidence}');
}

response.output is typed as ProductScan?. The compiler knows this. There's no casting, no dynamic map access, and no runtime surprises about field names.

Nullable Fields

Properties declared as nullable with ? in the abstract class become nullable in the generated class. Genkit communicates this nullability to the model through the schema, so the model understands which fields are optional. This reduces false null values and prevents validation failures for fields the model legitimately can't determine from the input.

Lists and Nested Types

Schemantic handles lists and nested schema types correctly. A property declared as List<String> generates the appropriate array type in the schema definition. A property declared as another @Schema()-annotated type generates the appropriate nested object schema.

@Schema()
abstract class $ItemAnalysis {
  String get name;
  List<String> get tags;
  List<$RelatedItem> get relatedItems;  // nested schema reference
}

@Schema()
abstract class $RelatedItem {
  String get name;
  String get relationship;
}

Tool Calling

Tool calling is the mechanism that lets a model take actions and retrieve information during the course of generating a response.

When you define tools and make them available to a model call, the model can decide, based on the conversation, that it needs to use one of those tools. It issues a structured request to call the tool, Genkit executes the tool function, returns the result to the model, and the model continues generating with the new information.

This is what transforms a model from a static knowledge base into something capable of fetching live data, querying databases, calling external APIs, and performing real work.

Defining a Tool

import 'package:schemantic/schemantic.dart';

part 'tools.g.dart';

@Schema()
abstract class $StockPriceInput {
  @Field(description: 'The stock ticker symbol, e.g. AAPL, GOOG')
  String get ticker;
}

// Register the tool with the Genkit instance
ai.defineTool(
  name: 'getStockPrice',
  description: 'Retrieves the current market price for a given stock ticker symbol',
  inputSchema: StockPriceInput.$schema,
  fn: (input, context) async {
    // In a real app, this would call a financial data API
    final price = await StockDataService.fetchPrice(input.ticker);
    return 'Current price of ${input.ticker}: \$$price';
  },
);

The description field for both the tool itself and its input schema fields is critically important. The model uses these descriptions to decide whether to call the tool and how to construct the input. Vague descriptions produce unreliable tool use.

Using a Tool in a Flow

final marketAnalysisFlow = ai.defineFlow(
  name: 'marketAnalysisFlow',
  inputSchema: AnalysisRequest.$schema,
  outputSchema: MarketReport.$schema,
  fn: (input, context) async {
    final response = await ai.generate(
      model: googleAI.gemini('gemini-2.5-pro'),
      prompt: 'Perform a brief market analysis for the following companies: '
              '${input.companyTickers.join(', ')}. '
              'Check the current price for each before writing the analysis.',
      toolNames: ['getStockPrice'],
      outputSchema: MarketReport.$schema,
    );

    if (response.output == null) {
      throw Exception('Market analysis generation failed.');
    }

    return response.output!;
  },
);

The toolNames parameter is a list of tool names (matching the name you gave when calling defineTool) that you're making available for this specific model call. The model sees the tool descriptions and schemas and decides autonomously when and how to call them during the generation process.

The Tool Calling Loop

When you provide tools, a single ai.generate() call may involve multiple round trips to the model. The sequence is:

  1. Genkit sends the prompt and tool schemas to the model.

  2. The model responds with a request to call one or more tools instead of (or before) generating final text.

  3. Genkit executes the requested tools and collects their outputs.

  4. Genkit sends the tool outputs back to the model.

  5. The model either calls more tools or generates its final response.

Genkit handles all of this automatically. From your application code, ai.generate() is still a single awaited call. The tool loop runs internally.

Streaming Responses

Large language models generate text one token at a time. In most API calls, the client waits for the entire response to be assembled before receiving anything.

For short responses this is fine. For long responses, this creates noticeable latency that degrades the user experience. Streaming solves this by delivering tokens to the client as they're generated.

Genkit supports streaming at both the ai.generate() level and the flow level.

Streaming at the Generate Level

final stream = ai.generateStream(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: 'Write a detailed history of the Benin Kingdom.',
);

await for (final chunk in stream) {
  // Each chunk contains the new text since the last chunk
  process.stdout.write(chunk.text);
}

// The complete assembled response is available after the stream ends
final completeResponse = await stream.onResult;
print('\n\nTotal tokens used: ${completeResponse.usage?.totalTokens}');

The generateStream() method returns immediately with a stream object. Iterating over it with await for processes each chunk as it arrives. The stream.onResult future resolves with the complete assembled response after the stream is exhausted.

Streaming at the Flow Level

Flows can also stream intermediate results. This is useful for flows that contain multi-step logic where you want to show progress to the user before the flow completes entirely.

@Schema()
abstract class $StoryRequest {
  String get genre;
  String get protagonist;
}

@Schema()
abstract class $StoryResult {
  String get title;
  String get fullText;
}

final storyGeneratorFlow = ai.defineFlow(
  name: 'storyGeneratorFlow',
  inputSchema: StoryRequest.$schema,
  outputSchema: StoryResult.$schema,
  streamSchema: JsonSchema.string(),
  fn: (input, context) async {
    // Stream the story text as it is generated
    final stream = ai.generateStream(
      model: googleAI.gemini('gemini-2.5-flash'),
      prompt: 'Write a \({input.genre} short story featuring \){input.protagonist}.',
    );

    final buffer = StringBuffer();

    await for (final chunk in stream) {
      buffer.write(chunk.text);
      if (context.streamingRequested) {
        // Send each chunk to the stream consumer
        context.sendChunk(chunk.text);
      }
    }

    final fullText = buffer.toString();

    // Generate a title as a separate quick call
    final titleResponse = await ai.generate(
      model: googleAI.gemini('gemini-2.5-flash'),
      prompt: 'Generate a one-line title for this story: $fullText',
    );

    return StoryResult(title: titleResponse.text.trim(), fullText: fullText);
  },
);

The streamSchema parameter on defineFlow declares the type of data that will be streamed through context.sendChunk(). Here it's a string, meaning each chunk is a string of text. You could also define a schema for structured streaming chunks if your use case requires streaming typed objects.

To consume a streaming flow:

final streamResponse = storyGeneratorFlow.stream(
  StoryRequest(genre: 'science fiction', protagonist: 'a Lagos street vendor'),
);

// Print streamed chunks as they arrive
await for (final chunk in streamResponse.stream) {
  process.stdout.write(chunk);
}

// Get the complete typed output after the stream ends
final finalResult = await streamResponse.output;
print('\n\nTitle: ${finalResult.title}');

In a Flutter context, each chunk arriving through streamResponse.stream would trigger a setState() call to update a Text widget, creating a typewriter effect in the UI without waiting for the full response.

Multimodal Input

Many modern models accept more than just text. They can receive images, audio, video, and documents as part of the prompt.

Genkit handles multimodal input through the Part class. A prompt that was previously a string becomes a list of parts, where each part is either text, a media reference, or raw data.

Providing an Image by URL

final response = await ai.generate(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: [
    Part.media(url: 'https://example.com/product.jpg'),
    Part.text('What product is shown in this image? '
              'Include the brand name if visible.'),
  ],
);

print(response.text);

Providing an Image as Raw Bytes

When the image is captured on-device or loaded from the file system, you supply it as base64-encoded bytes with an explicit MIME type:

import 'dart:convert';
import 'dart:io';

final imageFile = File('/path/to/photo.jpg');
final imageBytes = await imageFile.readAsBytes();
final base64Image = base64Encode(imageBytes);

final response = await ai.generate(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: [
    Part.media(
      url: 'data:image/jpeg;base64,$base64Image',
    ),
    Part.text('Identify this item and describe it in detail.'),
  ],
);

The data: URL scheme encodes the binary image data directly into the prompt part. No intermediate upload to a storage service is required for this approach.

Multimodal with Structured Output

Multimodal prompts compose cleanly with structured output schemas:

final response = await ai.generate(
  model: googleAI.gemini('gemini-2.5-flash'),
  prompt: [
    Part.media(url: 'data:image/jpeg;base64,$base64Image'),
    Part.text('Analyze this item thoroughly.'),
  ],
  outputSchema: ProductScan.$schema,
);

final ProductScan? scan = response.output;

The model receives both the image and the text instruction and is constrained to respond in the ProductScan JSON structure. This combination – multimodal input feeding into typed structured output – is the core mechanism of the item identification application that we'll build later in this guide.

Structured Output

Structured output deserves additional treatment beyond what we covered in the Schemantic section. This is because the mechanics of how Genkit communicates schema requirements to the model are worth understanding.

When you pass outputSchema to ai.generate(), Genkit does two things. First, it includes schema guidance in the prompt itself, instructing the model to respond with JSON that matches the specified structure. Second, after the model responds, Genkit parses the response and validates it against the schema. If the output doesn't match, Genkit can optionally retry the generation or raise an exception.

This is why the @Field(description: '...') annotation on each property matters so much. The description is included in the schema guidance sent to the model. A property named confidence with no description leaves the model to guess what scale to use. A property named confidence with the description 'A decimal value between 0.0 and 1.0 representing identification certainty' tells the model precisely what to put there.

The practical advice is: write field descriptions as if they are instructions to a developer who has never seen your code. Be explicit about units, ranges, formats, and any domain-specific meaning.

The Developer UI

The Developer UI is a localhost web application included with the Genkit CLI. It's one of the features that makes Genkit genuinely easier to work with than raw API integration, and it deserves its own detailed section.

Starting the Developer UI

From your project directory, after installing the Genkit CLI:

genkit start -- dart run

This command starts your Dart application and launches the Developer UI simultaneously, with the UI connected to your running application. The terminal prints the URL, which is http://localhost:4000 by default.

For Flutter applications specifically, the CLI provides a dedicated command:

genkit start:flutter -- -d chrome

This starts the Genkit UI, runs your Flutter app in Chrome, generates a genkit.env file containing the server configuration, and passes those environment variables into the Flutter runtime. All of this happens with one command.

What the Developer UI Shows You

The left sidebar lists every flow defined in your application. Clicking a flow name opens its detail view.

The Run tab shows the flow's input schema as a structured form. You fill in the fields and click Run. The flow executes and the output appears in the response panel. For streaming flows, you see the output arrive incrementally in real time. This lets you test your flows without writing test code or using curl.

The Traces tab shows the execution history of every flow run. Each trace is a tree. At the top level is the flow. Inside it you see each ai.generate() call, the exact prompt that was sent, the exact response that came back, the model used, the token counts, and the latency. For multi-step flows that make several model calls, you see each call as a node in the tree with its own details.

The traces are the debugging tool you reach for when a flow produces unexpected output. Rather than adding print statements and re-running, you look at the trace and see the exact prompt the model received. Often the problem is immediately obvious: a template string was interpolated incorrectly, a variable was empty, or a field description was misleading the model. Fix the prompt, re-run, check the new trace.

Running Genkit in Flutter: Three Architecture Patterns

Genkit Dart supports three distinct patterns for integrating AI logic with a Flutter application. The right choice depends on the sensitivity of your prompts, the complexity of your AI logic, and the stage of development you're in.

Pattern 1: Fully Client-Side (Prototyping Only)

In this pattern, all Genkit logic runs inside the Flutter app. The Genkit instance is created in the Flutter code, the AI flows are defined there, and the model API calls are made directly from the device.

// Inside your Flutter app
class AIService {
  late final Genkit _ai;
  late final dynamic _identificationFlow;

  AIService() {
    _ai = Genkit(plugins: [googleAI()]);
    _identificationFlow = _ai.defineFlow(
      name: 'identifyItem',
      inputSchema: ScanInput.$schema,
      outputSchema: ItemResult.$schema,
      fn: (input, _) async {
        final response = await _ai.generate(
          model: googleAI.gemini('gemini-2.5-flash'),
          prompt: [
             MediaPart(media: Media(url: 'data:image/jpeg;base64,${input.imageBase64}'),
            TextPart(text:'Identify and describe this item.'),
          ],
          outputSchema: ItemResult.$schema,
        );
        return response.output!;
      },
    );
  }
}

This works and is convenient for development. But it should never be shipped to production. The API key must be embedded in the application to make this work, and mobile applications can be decompiled. Anyone with enough motivation can extract the key from the binary.

For prototyping on your own device where you control the key, this is acceptable. For any published application, use one of the server-based patterns below.

Pattern 2: Remote Models (Hybrid Approach)

This pattern separates the model calls onto a secure server while keeping the flow orchestration logic in the Flutter client.

You host a Genkit Shelf backend that exposes model endpoints. The Flutter app defines remote models that point to those endpoints. The Flutter code orchestrates the flow, but the actual model API calls happen on the server where the keys are kept.

On the server (Dart with Shelf):

import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
import 'package:genkit_shelf/genkit_shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf_io.dart' as io;

void main() async {
  final ai = Genkit(plugins: [googleAI()]);

  final router = Router()
    ..all('/googleai/<path|.*>', serveModel(ai));

  await io.serve(router.call, '0.0.0.0', 8080);
}

In the Flutter app:

final ai = Genkit();  // No plugins needed on the client

final remoteGemini = ai.defineRemoteModel(
  name: 'remoteGemini',
  url: 'https://your-backend.com/googleai/gemini-2.5-flash',
);

final identificationFlow = ai.defineFlow(
  name: 'identifyItem',
  inputSchema: ScanInput.$schema,
  outputSchema: ItemResult.$schema,
  fn: (input, _) async {
    final response = await ai.generate(
      model: remoteGemini,
      prompt: [
         MediaPart(media: Media(url: 'data:image/jpeg;base64,${input.imageBase64}')),
        TextPart(text:'Identify this item.'),
      ],
      outputSchema: ItemResult.$schema,
    );
    return response.output!;
  },
);

The Flutter app never touches the Gemini API key. All it knows is the URL of the model endpoint. The server holds the key and proxies the model calls.

Pattern 3: Server-Side Flows (Most Secure)

This is the recommended architecture for production applications. The entire AI flow, prompts, model calls, tool use, and output schema lives on the server. The Flutter app is a thin client that sends a request and receives a typed response.

On the server:

import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
import 'package:genkit_shelf/genkit_shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf_io.dart' as io;

void main() async {
  final ai = Genkit(plugins: [googleAI()]);

  final identificationFlow = ai.defineFlow(
    name: 'identifyItem',
    inputSchema: ScanInput.$schema,
    outputSchema: ItemResult.$schema,
    fn: (input, _) async {
      final response = await ai.generate(
        model: googleAI.gemini('gemini-2.5-flash'),
        prompt: [
          MediaPart(media: Media(url: 'data:image/jpeg;base64,${input.imageBase64}')),
          TextPart(text:'Identify and describe this item in detail.'),
        ],
        outputSchema: ItemResult.$schema,
      );
      return response.output!;
    },
  );

  final router = Router()
    ..post('/identifyItem', shelfHandler(identificationFlow));

  await io.serve(router.call, '0.0.0.0', 8080);
}

In the Flutter app (using the shared schema package):

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'shared_schemas.dart';  // schemas shared between client and server

class IdentificationService {
  static Future<ItemResult> identifyItem(String base64Image) async {
    final request = ScanInput(imageBase64: base64Image);

    final httpResponse = await http.post(
      Uri.parse('https://your-backend.com/identifyItem'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'data': request.toJson()}),
    );

    final body = jsonDecode(httpResponse.body);
    return ItemResult.fromJson(body['result']);
  }
}

Because both the server and the Flutter client are in Dart, you can keep ScanInput and ItemResult in a shared Dart package referenced by both. When the schema changes, you update it in one place and the compiler flags every mismatch on both sides.

Deployment

One of the practical advantages of Genkit Dart is that Dart server applications have several mature deployment targets.

Shelf

The genkit_shelf package integrates Genkit flows with the Shelf HTTP server library. shelfHandler() converts a Genkit flow into a Shelf request handler. You add it to a router and start a Shelf server. That's the entire deployment layer.

import 'package:genkit_shelf/genkit_shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf_io.dart' as io;

final router = Router()
  ..post('/api/identifyItem', shelfHandler(identificationFlow))
  ..post('/api/generateReport', shelfHandler(reportFlow));

await io.serve(router.call, '0.0.0.0', 8080);

Each flow becomes a POST endpoint. Clients send {"data": {...}} and receive {"result": {...}} with the typed output serialized to JSON.

Cloud Run

Google Cloud Run is the most straightforward deployment target for Genkit Dart backends. You containerize the Shelf application with a Dockerfile, push the image to Google Container Registry or Artifact Registry, and deploy it to Cloud Run. Cloud Run handles scaling, HTTPS termination, and regional distribution.

FROM dart:stable AS build
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart compile exe bin/server.dart -o bin/server

FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/server
EXPOSE 8080
CMD ["/app/bin/server"]

Firebase

The Firebase plugin allows you to deploy Genkit flows as Firebase Cloud Functions. This is convenient if your application already uses Firebase for authentication, Firestore, or other services, since the AI flows live in the same project and benefit from the same IAM setup.

AWS Lambda and Azure Functions

The framework also provides documentation for AWS Lambda and Azure Functions deployments, making it possible to host Genkit Dart backends in either of the major cloud ecosystems, depending on where your organization's infrastructure already lives.

Observability and Tracing

Every flow execution in Genkit generates a trace. A trace is a structured record of everything that happened during the execution: the input received, every model call made, the exact prompt for each call, the exact response, token counts, latency at each step, and the final output.

In development, these traces are visible in the Developer UI's Traces tab. In production, you export them to Google Cloud Operations (formerly Stackdriver) using the genkit_google_cloud plugin, or to any OpenTelemetry-compatible backend.

import 'package:genkit_google_cloud/genkit_google_cloud.dart';

final ai = Genkit(plugins: [
  googleAI(),
  googleCloud(),  // Exports traces and metrics to Google Cloud
]);

With this configuration, every flow execution sends its trace data to Google Cloud. You can use Cloud Trace to visualize flow performance over time, identify bottlenecks, and correlate AI behavior with application-level metrics.

For production applications handling real users, this observability layer isn't optional. It's how you detect when a model change silently degrades the quality of your flows.

Building a Real-Time Item Identification App

Before starting the project section, make sure you have the following in place.

Dart SDK

You'll need Dart SDK 3.10.0 or later. If you have Flutter installed, check your Dart version with:

dart --version

If the version is below 3.10.0, update Flutter:

flutter upgrade

Flutter ships its own Dart SDK, so upgrading Flutter upgrades Dart as well.

Flutter SDK

You'll need Flutter 3.22.0 or later. Verify with:

flutter --version

The project uses the camera plugin for image capture. That plugin requires at minimum Flutter 3.x and works on Android API 21 and above, iOS 11 and above.

Genkit CLI

curl -sL cli.genkit.dev | bash

After installation, restart your terminal and verify:

genkit --version

Gemini API Key

Go to aistudio.google.com/apikey, sign in with a Google account, and generate a new API key. Copy it somewhere safe. You won't need a credit card for this. The Gemini API free tier is sufficient for building and testing the application.

Set the key as an environment variable:

export GEMINI_API_KEY=your_actual_key_here

For persistence across terminal sessions, add that line to your shell profile file (~/.bashrc, ~/.zshrc, and so on).

Assumed Knowledge

This part of the tutorial assumes you're comfortable with Dart's async/await syntax, that you've built at least one Flutter application before, and that you understand the concept of a widget tree. It doesn't assume any prior experience with AI APIs or LLMs. We'll introduce every AI-related concept as it appears.

The application we're building is called LensID. The user opens the app, points the camera at any object, taps a capture button, and receives a structured analysis of what the camera saw: the item name, the condition, usage type, and a confidence score.

Image of the app

This covers the full stack of what Genkit Dart enables in a Flutter context: capturing device input, sending multimodal data to a model through a typed flow, and rendering structured typed output in the UI.

For this guide, the AI logic runs fully client-side to keep the project self-contained, since it's a learning exercise. In a shipped app, you would move the flow to a server following Pattern 3 described earlier.

Project Structure

lens_id/
  lib/
    main.dart
    screens/
      camera_screen.dart
      result_screen.dart
      splash_screen.dart
    services/
      identification_service.dart
    models/
      scan_models.dart
      scan_models.g.dart
    widgets/
      result_card.dart
  pubspec.yaml

Step 1: Create the Flutter Project

flutter create lens_id
cd lens_id

Step 2: Add Dependencies

Open pubspec.yaml and update the dependencies and dev_dependencies sections:

dependencies:
  flutter:
    sdk: flutter
  genkit: ^0.12.1
  genkit_google_genai: ^0.2.4
  camera: ^0.12.0+1
  permission_handler: ^12.0.1
  google_fonts: ^8.0.2
 image_picker: ^1.2.1
  

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.13.1
  schemantic: 0.1.1

Run the install:

flutter pub get

Step 3: Configure Platform Permissions

Android (android/app/src/main/AndroidManifest.xml):

Add these permissions inside the <manifest> tag, above <application>:

<!-- Permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Media/Storage permissions (Android 13+ granular permissions) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Legacy storage permission for Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

<!-- Camera hardware feature (not required so app installs on all devices) -->
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />

Also, ensure the minSdkVersion in android/app/build.gradle is at least 21:

defaultConfig {
    minSdkVersion 21
}

iOS (ios/Runner/Info.plist):

Add these keys inside the <dict> tag:

<key>NSCameraUsageDescription</key>
<string>LensID needs camera access to scan and identify items.</string>

<key>NSPhotoLibraryUsageDescription</key>
<string>LensID needs access to your photo library to upload images for identification.</string>

Step 4: Define the Data Schemas

Create lib/models/scan_models.dart:

import 'package:schemantic/schemantic.dart';

part 'scan_models.g.dart';

/// Input: image to analyze
@Schema()
abstract class $ScanRequest {
  @Field(description: 'Base64-encoded JPEG image of the item to identify')
  String get imageBase64;
}

/// Minimal output for a minimal UI
@Schema()
abstract class $ItemIdentification {
  /// Simple name only
  @Field(description: 'The name of the item')
  String get itemName;

  /// Short condition (keep it very simple)
  @Field(description: 'The condition of the item in a short phrase')
  String get condition;

  /// What it is used for (1 short line)
  @Field(description: 'What the item is used for in one short sentence')
  String get usage;

  /// Optional confidence (keep but do not overuse)
  @Field(
    description:
        'Confidence score between 0% and 100% representing certainty of identification',
  )
  double get confidenceScore;
}

This file defines the data contract for the LensID identification flow. The two @Schema() abstract classes control what goes into the AI and what comes out of it.

$ScanRequest represents the input. It tells the system that the only thing the model needs is a base64 encoded image. There's no extra metadata or complexity, just the image itself.

$ItemIdentification represents the output. It defines the exact structure the AI must return and enforces a minimal response. Instead of generating a detailed analysis, the model is limited to four fields which are itemName, condition, usage, and confidenceScore.

Each @Field() annotation includes a description, and these descriptions act as instructions that are sent directly to the model through Genkit. They guide how the model should fill each field and keep the output consistent.

The itemName field tells the model to return a simple and recognizable name rather than a long description. The condition field ensures the response stays short and clear, such as New or Worn. The usage field limits the output to one concise sentence explaining what the item is used for. The confidenceScore field defines the expected range and format so the model returns a consistent numeric value.

Because the schema is minimal and the descriptions are precise, the model has very little room to generate unnecessary information. This keeps the response clean, predictable, and aligned with the simple UI.

The part 'scan_models.g.dart' directive connects the generated code file to this file once the build runner creates it.

Now run the code generator:

dart run build_runner build --delete-conflicting-outputs

This creates lib/models/scan_models.g.dart, which contains the concrete ScanRequest and ItemIdentification classes with constructors, JSON serialization methods, and the $schema static properties that Genkit uses.

Step 5: Create the Identification Service

Create lib/services/identification_service.dart:

import 'dart:convert';
import 'dart:io';

import 'package:genkit/genkit.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';

import '../models/scan_models.dart';

/// Wraps the Genkit flow that sends an image to Gemini and returns
/// a structured [ItemIdentification] result.
class IdentificationService {
  late final Genkit _ai;
  late final Future<ItemIdentification> Function(ScanRequest) _identifyFlow;

  IdentificationService() {
    // Read the API key injected at build time via --dart-define.
    // Falls back to the GEMINI_API_KEY environment variable when running
    // with `dart run` or `genkit start`.
    const dartDefineKey = String.fromEnvironment('GEMINI_API_KEY');
    final apiKey = dartDefineKey.isNotEmpty
        ? dartDefineKey
        : Platform.environment['GEMINI_API_KEY'];

    _ai = Genkit(
      plugins: [
        googleAI(apiKey: apiKey),
      ],
    );

    // Define the flow once; it is reused for every scan.
    _identifyFlow = _ai.defineFlow(
      name: 'identifyItemFlow',
      inputSchema: ScanRequest.$schema,
      outputSchema: ItemIdentification.$schema,
      fn: _runIdentification,
    ).call;
  }

  /// Core flow logic: builds a multimodal prompt and calls Gemini 2.5 Flash.
  Future<ItemIdentification> _runIdentification(
    ScanRequest request,
    // ignore: avoid_dynamic_calls
    dynamic context,
  ) async {
    // Embed the image directly as a data URL — no storage upload needed.
    final imagePart = MediaPart(
      media: Media(
        url: 'data:image/jpeg;base64,${request.imageBase64}',
        contentType: 'image/jpeg',
      ),
    );

    // The text part sets the model's role and gives clear instructions.
    // Field descriptions in the schema reinforce these instructions.
    final instructionPart = TextPart(
      text: 'You are a product identification assistant. '
          'Carefully analyse the item in this image and provide a thorough '
          'identification based only on what is clearly visible. '
          'Do not invent brand names if none are legible.',
    );

    final response = await _ai.generate(
      model: googleAI.gemini('gemini-2.5-flash'),
      messages: [
        Message(
          role: Role.user,
          content: [imagePart, instructionPart],
        ),
      ],
      outputSchema: ItemIdentification.$schema,
    );

    if (response.output == null) {
      throw Exception(
        'Gemini did not return a valid structured response. '
        'Try again with a clearer, well-lit image.',
      );
    }

    return response.output!;
  }

  /// Public entry point: accepts a captured [File] and returns a typed result.
  Future<ItemIdentification> identifyFromFile(File imageFile) async {
    final bytes = await imageFile.readAsBytes();
    final base64Image = base64Encode(bytes);
    return _identifyFlow(ScanRequest(imageBase64: base64Image));
  }
}

This file defines the service that connects your Flutter app to the AI model using Genkit. It's responsible for taking an image, sending it to the model, and returning a structured result that matches your schema.

The IdentificationService class sets up a Genkit instance and prepares a reusable flow for identifying items. During initialization, it reads the API key either from a build-time value using --dart-define or from the environment. This makes it flexible for both local development and production use.

The _identifyFlow is defined once using defineFlow. It links the input schema and output schema to a function called _runIdentification. This ensures that every request going through the flow follows the exact structure defined in your models, which keeps the system consistent and predictable.

The _runIdentification method contains the core logic. It takes the base64 image from the request and embeds it directly into a data URL. This avoids the need to upload the image to external storage.

The image is then combined with a text instruction that tells the model how to behave. The instruction is simple and focused, guiding the model to analyze only what's visible and avoid making assumptions.

The request is sent to the Gemini model using Genkit’s generate method. The model processes both the image and the instruction together and returns a structured response that matches the ItemIdentification schema. Because the output schema is enforced, the response is automatically parsed into a typed object.

There's a safety check to ensure that the model actually returns a valid structured response. If it doesn't, an exception is thrown with a clear message so the app can handle the failure properly.

The identifyFromFile method is the public entry point used by your UI. It takes an image file, converts it into base64, and passes it into the flow. The result returned is already structured and ready to be displayed on your result screen.

Overall, this service acts as the bridge between your UI and the AI model, ensuring that images are processed correctly and that responses remain clean, structured, and aligned with your minimal design.

Step 6: Build the Camera Screen

Create lib/screens/camera_screen.dart:

import 'dart:io';

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';

import '../services/identification_service.dart';
import 'result_screen.dart';

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});

  @override
  State<CameraScreen> createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen>
    with WidgetsBindingObserver {
  CameraController? _controller;
  List<CameraDescription> _cameras = [];
  bool _isCameraReady = false;
  bool _isCapturing = false;
  String? _initError;

  final _service = IdentificationService();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _initCamera();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _controller?.dispose();
    super.dispose();
  }

  // Release and reclaim the camera when the app goes to the background.
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.inactive) {
      _controller?.dispose();
      if (mounted) setState(() => _isCameraReady = false);
    } else if (state == AppLifecycleState.resumed && _cameras.isNotEmpty) {
      _setupController(_cameras.first);
    }
  }

  Future<void> _initCamera() async {
    final status = await Permission.camera.request();
    if (!status.isGranted) {
      if (mounted) {
        setState(() =>
            _initError = 'Camera permission is required to identify items.\nYou can still upload images below.');
      }
      return;
    }

    try {
      _cameras = await availableCameras();
    } catch (e) {
      if (mounted) setState(() => _initError = 'Could not list cameras: $e\nYou can still upload images below.');
      return;
    }

    if (_cameras.isEmpty) {
      if (mounted) setState(() => _initError = 'No cameras found on device.\nYou can still upload images below.');
      return;
    }

    await _setupController(_cameras.first);
  }

  Future<void> _setupController(CameraDescription camera) async {
    await _controller?.dispose();

    final controller = CameraController(
      camera,
      ResolutionPreset.high,
      enableAudio: false,
      imageFormatGroup: ImageFormatGroup.jpeg,
    );

    try {
      await controller.initialize();
      _controller = controller;
      if (mounted) setState(() => _isCameraReady = true);
    } catch (e) {
      if (mounted) setState(() => _initError = 'Camera init failed: $e');
    }
  }

  Future<void> _captureAndIdentify() async {
    if (_isCapturing || !_isCameraReady) return;
    if (_controller == null || !_controller!.value.isInitialized) return;

    setState(() => _isCapturing = true);

    try {
      final xFile = await _controller!.takePicture();
      final imageFile = File(xFile.path);

      if (!mounted) return;
      _showLoadingDialog();

      final result = await _service.identifyFromFile(imageFile);

      if (!mounted) return;
      Navigator.of(context).pop(); // close loading dialog

      await Navigator.of(context).push(
        MaterialPageRoute(
          builder: (_) => ResultScreen(
            imageFile: imageFile,
            identification: result,
          ),
        ),
      );
    } catch (error) {
      if (mounted && Navigator.of(context).canPop()) {
        Navigator.of(context).pop();
      }
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Identification failed: $error'),
            backgroundColor: Colors.red.shade700,
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    } finally {
      if (mounted) setState(() => _isCapturing = false);
    }
  }

  Future<void> _pickImage() async {
    if (_isCapturing) return;

    final picker = ImagePicker();
    final xFile = await picker.pickImage(source: ImageSource.gallery);
    if (xFile == null) return;

    setState(() => _isCapturing = true);

    try {
      final imageFile = File(xFile.path);

      if (!mounted) return;
      _showLoadingDialog();

      final result = await _service.identifyFromFile(imageFile);

      if (!mounted) return;
      Navigator.of(context).pop(); // close loading dialog

      await Navigator.of(context).push(
        MaterialPageRoute(
          builder: (_) => ResultScreen(
            imageFile: imageFile,
            identification: result,
          ),
        ),
      );
    } catch (error) {
      if (mounted && Navigator.of(context).canPop()) {
        Navigator.of(context).pop();
      }
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Identification failed: $error'),
            backgroundColor: Colors.red.shade700,
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    } finally {
      if (mounted) setState(() => _isCapturing = false);
    }
  }

  void _showLoadingDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (_) => const Center(
        child: Card(
          margin: EdgeInsets.symmetric(horizontal: 48),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 32, vertical: 28),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                CircularProgressIndicator(),
                SizedBox(height: 20),
                Text(
                  'Identifying item…',
                  style: TextStyle(fontSize: 16),
                ),
                SizedBox(height: 6),
                Text(
                  'Powered by Gemini',
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        fit: StackFit.expand,
        children: [
          // Camera preview / error / loading 
          if (_initError != null)
            _ErrorPlaceholder(message: _initError!)
          else if (_isCameraReady && _controller != null)
            CameraPreview(_controller!)
          else
            const _LoadingPlaceholder(),

          // Viewfinder corners with scanning line
          if (_isCameraReady) const _ViewfinderCorners(),

          // Bottom Controls
          Positioned(
            bottom: 40,
            left: 0,
            right: 0,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                _CaptureButton(
                  isCapturing: _isCapturing,
                  enabled: _isCameraReady && !_isCapturing,
                  onTap: _captureAndIdentify,
                ),
                const SizedBox(height: 16),
                GestureDetector(
                  onTap: _pickImage,
                  child: const Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.upload_rounded, color: Colors.white60, size: 16),
                      SizedBox(width: 4),
                      Text(
                        'UPLOAD',
                        style: TextStyle(
                          color: Colors.white60,
                          fontSize: 12,
                          fontWeight: FontWeight.w700,
                          letterSpacing: 1.0,
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// Supporting widgets

class _LoadingPlaceholder extends StatelessWidget {
  const _LoadingPlaceholder();

  @override
  Widget build(BuildContext context) => const Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CircularProgressIndicator(color: Colors.white54),
          SizedBox(height: 16),
          Text('Starting camera…',
              style: TextStyle(color: Colors.white54, fontSize: 14)),
        ],
      );
}

class _ErrorPlaceholder extends StatelessWidget {
  final String message;
  const _ErrorPlaceholder({required this.message});

  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.camera_alt_outlined,
                color: Colors.white38, size: 64),
            const SizedBox(height: 20),
            Text(
              message,
              textAlign: TextAlign.center,
              style: const TextStyle(color: Colors.white70, fontSize: 15),
            ),
            const SizedBox(height: 24),
            OutlinedButton(
              style: OutlinedButton.styleFrom(
                foregroundColor: Colors.white,
                side: const BorderSide(color: Colors.white38),
              ),
              onPressed: () => openAppSettings(),
              child: const Text('Open Settings'),
            ),
          ],
        ),
      );
}

class _ViewfinderCorners extends StatelessWidget {
  const _ViewfinderCorners();

  @override
  Widget build(BuildContext context) {
    const size = 48.0;
    const thickness = 2.0;
    const color = Color(0xFFD67123);

    Widget corner({required bool top, required bool left}) {
      return Positioned(
        top: top ? 0 : null,
        bottom: top ? null : 0,
        left: left ? 0 : null,
        right: left ? null : 0,
        child: SizedBox(
          width: size,
          height: size,
          child: CustomPaint(
            painter: _CornerPainter(
                top: top, left: left, color: color, thickness: thickness),
          ),
        ),
      );
    }

    final screenSize = MediaQuery.of(context).size;
    final boxSize = screenSize.width * 0.75;
    final offsetX = (screenSize.width - boxSize) / 2;
    final offsetY = (screenSize.height - boxSize) / 2 - 40;

    return Positioned(
      left: offsetX,
      top: offsetY,
      width: boxSize,
      height: boxSize,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          corner(top: true, left: true),
          corner(top: true, left: false),
          corner(top: false, left: true),
          corner(top: false, left: false),
          // Scanner line
          const Positioned.fill(
            child: _ScannerLine(),
          ),
        ],
      ),
    );
  }
}

class _CornerPainter extends CustomPainter {
  final bool top;
  final bool left;
  final Color color;
  final double thickness;

  const _CornerPainter({
    required this.top,
    required this.left,
    required this.color,
    required this.thickness,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = color
      ..strokeWidth = thickness
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.square;

    final path = Path();
    final h = size.height;
    final w = size.width;

    if (top && left) {
      path.moveTo(0, h);
      path.lineTo(0, 0);
      path.lineTo(w, 0);
    } else if (top && !left) {
      path.moveTo(0, 0);
      path.lineTo(w, 0);
      path.lineTo(w, h);
    } else if (!top && left) {
      path.moveTo(0, 0);
      path.lineTo(0, h);
      path.lineTo(w, h);
    } else {
      path.moveTo(0, h);
      path.lineTo(w, h);
      path.lineTo(w, 0);
    }

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(_CornerPainter old) => false;
}

class _ScannerLine extends StatefulWidget {
  const _ScannerLine();

  @override
  State<_ScannerLine> createState() => _ScannerLineState();
}

class _ScannerLineState extends State<_ScannerLine>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Align(
          alignment: Alignment(0, -1.0 + (_controller.value * 2.0)),
          child: Container(
            height: 2,
            width: double.infinity,
            decoration: BoxDecoration(
              color: const Color(0xFFD67123),
              boxShadow: [
                BoxShadow(
                  color: const Color(0xFFD67123).withAlpha(120),
                  blurRadius: 10,
                  spreadRadius: 2,
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

class _CaptureButton extends StatelessWidget {
  final bool isCapturing;
  final bool enabled;
  final VoidCallback onTap;

  const _CaptureButton({
    required this.isCapturing,
    required this.enabled,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: enabled ? onTap : null,
      child: Container(
        width: 80,
        height: 80,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.transparent,
          border: Border.all(
            color: Colors.white.withAlpha(150),
            width: 3,
          ),
        ),
        child: Center(
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 150),
            width: isCapturing ? 40 : 64,
            height: isCapturing ? 40 : 64,
            decoration: const BoxDecoration(
              shape: BoxShape.circle,
              color: Color(0xFFBA2226),
            ),
            child: isCapturing
                ? const Center(
                    child: SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(
                        strokeWidth: 2.0,
                        color: Colors.white,
                      ),
                    ),
                  )
                : null,
          ),
        ),
      ),
    );
  }
}

This screen handles the full camera lifecycle. The WidgetsBindingObserver mixin lets the widget respond to app lifecycle events so the camera is properly released when the app goes to the background and re-initialized when it comes back. This prevents camera resource conflicts on Android.

_initializeCamera() requests permission through permission_handler before trying to access the camera. Attempting camera access without permission on iOS causes an unrecoverable crash. On Android it causes a silent failure. The explicit permission request with user-facing error handling produces a professional experience.

CameraController is initialized with ResolutionPreset.high and ImageFormatGroup.jpeg. High resolution gives the model more detail to work with during identification. JPEG format is specified because that's what the model receives through the data:image/jpeg;base64,... URL format in the service.

_captureAndIdentify() takes the picture, shows a loading dialog, calls the service, navigates to the result screen, and handles errors. The try / catch / finally structure ensures that _isCapturing is always reset to false regardless of whether the flow succeeded or threw an exception.

Step 7: Build the Result Screen

Create lib/screens/result_screen.dart:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

import '../models/scan_models.dart';

class ResultScreen extends StatelessWidget {
  final File imageFile;
  final ItemIdentification identification;

  const ResultScreen({
    super.key,
    required this.imageFile,
    required this.identification,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // Top Image
                    Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: AspectRatio(
                        aspectRatio: 1.0,
                        child: ClipRRect(
                          borderRadius: BorderRadius.zero,
                          child: Image.file(
                            imageFile,
                            fit: BoxFit.cover,
                          ),
                        ),
                      ),
                    ),

                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 24.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const SizedBox(height: 8),
                          // Subtitle
                          Text(
                            'IDENTIFIED_ASSET',
                            style: GoogleFonts.rajdhani(
                              color: const Color(0xFFDA292E),
                              fontSize: 10,
                              fontWeight: FontWeight.w900,
                              letterSpacing: 1.5,
                            ),
                          ),
                          const SizedBox(height: 4),

                          // Main Title
                          Text(
                            identification.itemName.toUpperCase(),
                            style: GoogleFonts.bebasNeue(
                              color: Colors.black,
                              fontSize: 32,
                              fontWeight: FontWeight.w900,
                              fontStyle: FontStyle.italic,
                              height: 1.0,
                              letterSpacing: -1.0,
                            ),
                          ),
                          const SizedBox(height: 40),

                          // Condition & Usage Type
                          Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Expanded(
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      'CONDITION',
                                      style: GoogleFonts.rajdhani(
                                        color: Colors.grey,
                                        fontSize: 10,
                                        fontWeight: FontWeight.w800,
                                        letterSpacing: 1.0,
                                      ),
                                    ),
                                    const SizedBox(height: 6),
                                    Text(
                                      identification.condition.toUpperCase(),
                                      style: GoogleFonts.rajdhani(
                                        color: Colors.black,
                                        fontSize: 13,
                                        fontWeight: FontWeight.w900,
                                        height: 1.2,
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                              const SizedBox(width: 16),
                              Expanded(
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Text(
                                      'USAGE_TYPE',
                                      style: GoogleFonts.rajdhani(
                                        color: Colors.grey,
                                        fontSize: 10,
                                        fontWeight: FontWeight.w800,
                                        letterSpacing: 1.0,
                                      ),
                                    ),
                                    const SizedBox(height: 6),
                                    Text(
                                      identification.usage.toUpperCase(),
                                      style: GoogleFonts.rajdhani(
                                        color: Colors.black,
                                        fontSize: 13,
                                        fontWeight: FontWeight.w900,
                                        height: 1.2,
                                      ),
                                    ),
                                  ],
                                ),
                              ),
                            ],
                          ),
                          const SizedBox(height: 32),

                          // Confidence Rating
                          Text(
                            'CONFIDENCE_RATING',
                            style: GoogleFonts.rajdhani(
                              color: Colors.grey,
                              fontSize: 10,
                              fontWeight: FontWeight.w800,
                              letterSpacing: 1.0,
                            ),
                          ),
                          const SizedBox(height: 2),
                          Row(
                            crossAxisAlignment: CrossAxisAlignment.center,
                            children: [
                              Text(
                                '${(identification.confidenceScore * 100).toStringAsFixed(2)}%',
                                style: GoogleFonts.bebasNeue(
                                  color: Colors.black,
                                  fontSize: 36,
                                  fontWeight: FontWeight.w900,
                                  letterSpacing: -1.0,
                                ),
                              ),
                              const SizedBox(width: 12),
                              Expanded(
                                child: Container(
                                  height: 2,
                                  color: const Color(0xFFDA292E),
                                ),
                              ),
                            ],
                          ),
                          const SizedBox(height: 24),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
            
            // Bottom Button
            Padding(
              padding: const EdgeInsets.fromLTRB(24, 8, 24, 24),
              child: SizedBox(
                width: double.infinity,
                height: 56,
                child: ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).pop();
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFDA292E),
                    foregroundColor: Colors.white,
                    shape: const RoundedRectangleBorder(
                      borderRadius: BorderRadius.zero,
                    ),
                    elevation: 0,
                  ),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                        'SCAN ANOTHER ASSET',
                        style: GoogleFonts.rajdhani(
                          fontSize: 15,
                          fontWeight: FontWeight.w800,
                          letterSpacing: 1.5,
                        ),
                      ),
                      const SizedBox(width: 12),
                      const Icon(Icons.arrow_forward_rounded, size: 20),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

The result screen is a pure display component. It receives two things from the camera screen, which are the captured File and the typed ItemIdentification object. No API calls happen here and no async work is performed. The screen simply renders the structured data returned from the flow.

The entire UI reads directly from the typed identification object. identification.itemName, identification.condition, identification.usage, and identification.confidenceScore are all strongly typed values. There's no need for casting, manual parsing, or defensive checks around missing fields.

Because the schema was intentionally kept minimal, the UI stays simple as well. Each field maps directly to a visible element on the screen without any transformation or extra logic. The image is shown at the top, followed by the item name, condition, usage, and confidence score.

This is the practical payoff of using schemantic. The data that leaves the AI flow as a structured object arrives in the UI in the same form. There is no gap between the model response and the UI layer. The result is a clean, predictable, and fully type safe rendering pipeline.

Step 8: Wire up the splash screen and main.dart

Update lib/screens/splash_screen.dart:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:permission_handler/permission_handler.dart';

import 'camera_screen.dart';

class SplashScreen extends StatelessWidget {
  const SplashScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF041926),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: Center(
                child: Text(
                  'LENSID',
                  style: GoogleFonts.bebasNeue(
                    color: Colors.white,
                    fontSize: 48,
                    fontWeight: FontWeight.w900,
                    letterSpacing: -2.0,
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
              child: SizedBox(
                width: double.infinity,
                height: 56,
                child: ElevatedButton(
                  onPressed: () async {
                    await Permission.camera.request();
                    if (!context.mounted) return;
                    Navigator.of(context).pushReplacement(
                      MaterialPageRoute(
                        builder: (_) => const CameraScreen(),
                      ),
                    );
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFDA292E),
                    foregroundColor: Colors.white,
                    shape: const RoundedRectangleBorder(
                      borderRadius: BorderRadius.zero,
                    ),
                    elevation: 0,
                  ),
                  child: Text(
                    'START',
                    style: GoogleFonts.rajdhani(
                      fontSize: 16,
                      fontWeight: FontWeight.w800,
                      letterSpacing: 1.2,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

The splash screen is the entry point of the app and is intentionally kept minimal. It serves one purpose: to move the user into the scanning experience as quickly as possible.

The layout is built using a Column with two main sections. The top section centers the app name “LENSID” using a custom font, which gives it a strong visual identity without adding extra UI elements. The bottom section contains a single full-width button labeled “START”.

When the user taps the button, the app requests camera permission using permission_handler. This ensures that by the time the user reaches the next screen, the camera is already accessible. After requesting permission, the app navigates to the camera screen using pushReplacement, which removes the splash screen from the navigation stack so the user can't return to it.

Update lib/main.dart:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'screens/splash_screen.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  // Lock to portrait orientation so the camera UI always looks correct.
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
  ]);

  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      statusBarIconBrightness: Brightness.light,
    ),
  );

  runApp(const LensIDApp());
}

class LensIDApp extends StatelessWidget {
  const LensIDApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LensID',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        scaffoldBackgroundColor: const Color(0xFF041926),
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFDA292E),
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
        snackBarTheme: const SnackBarThemeData(
          behavior: SnackBarBehavior.floating,
        ),
      ),
      home: const SplashScreen(),
    );
  }
}

WidgetsFlutterBinding.ensureInitialized() is required before the first frame when your app's initialization code uses platform channels, which the camera plugin does. Calling runApp() without this on older Flutter versions causes cryptic errors.

Step 9: Run the App

Set your API key if you haven't already:

export GEMINI_API_KEY=your_key_here

For Flutter, pass the key as a dart-define so it's available to the running process:

flutter run --dart-define=GEMINI_API_KEY=$GEMINI_API_KEY

Update identification_service.dart to read the key from the dart-define:

import 'package:flutter/foundation.dart';

// Replace:
_ai = Genkit(plugins: [googleAI()]);

// With:
const apiKey = String.fromEnvironment('GEMINI_API_KEY');
_ai = Genkit(plugins: [googleAI(apiKey: apiKey.isEmpty ? null : apiKey)]);

When apiKey is not provided, googleAI() falls back to the GEMINI_API_KEY environment variable, which works during development. The String.fromEnvironment approach works for both dev and production builds.

Step 10: Test with the Developer UI

While developing, you can test the identification flow without needing the camera at all. Start the Developer UI:

genkit start:flutter -- -d chrome

Open http://localhost:4000. Find identifyItemFlow in the sidebar. In the Run tab, provide a base64-encoded test image and click Run. The flow executes and you see the ItemIdentification result as structured JSON in the output panel. The trace panel shows the exact multimodal prompt sent to the model, the response received, and the token count.

This is how you iterate on the quality of your identifications: adjust the field descriptions in scan_models.dart, re-run the build runner, test in the Developer UI, check the trace. No device needed, no app restart required.

Screenshots

Splash Screen Capture/Scan Screen Result Screen

Github Repo: https://github.com/Atuoha/lens\_id\_genkit\_dart

Architectural Diagram

Architectural Diagram

Data flows from the device camera to a file, is encoded as base64, wrapped in a typed ScanRequest, sent through a Genkit flow to the Gemini model, and returns as a fully typed ItemIdentification that the UI renders directly.

Where Genkit Dart Is Headed

Genkit Dart is currently in preview, which means it's actively being developed and some APIs are subject to change before a stable release. But even in preview, the fundamentals are solid enough to build real applications.

The trajectory points in a few clear directions:

  1. Multi-agent support, already present in the TypeScript version, is coming to Dart. This means flows that spawn sub-agents, delegate tasks to specialized sub-flows, and coordinate multiple model calls toward a single complex goal.

  2. RAG (retrieval-augmented generation) support through vector database plugins like Pinecone, Chroma, and pgvector is already listed in the documentation and will allow Flutter applications to build document-aware AI features with a consistent API.

  3. Model Context Protocol support in Genkit Dart will allow models to connect to external tools and data sources using the emerging MCP standard. This is important because MCP is becoming a common integration layer between AI models and developer tools. Genkit's MCP support means those integrations become accessible in your Dart flows without building custom adapters.

  4. On the Flutter side, the streaming story will become more refined. Patterns for updating Flutter UI in real time as a flow streams its output are emerging in the community. Genkit's native streaming support, combined with Flutter's reactive widget model, creates a genuinely good foundation for typewriter-style AI UI patterns.

The advice at this stage is to build with Genkit Dart now for learning and internal tools. Follow the framework's development through the official Genkit Discord and GitHub repository. By the time a stable release lands, you'll have genuine hands-on experience rather than theoretical knowledge.

Conclusion

Genkit Dart isn't just a client library for calling AI models from Flutter. It's a framework that changes how you think about building AI features into applications.

It gives you a consistent, provider-agnostic model interface so that switching between Gemini, Claude, GPT-4o, Grok, or a local Ollama model is a one-line change. It gives you flows as the structured, observable, deployable unit of AI logic. It gives you schemantic-powered type safety so your AI outputs are real Dart objects, not loosely typed maps. It gives you a visual developer UI so you can test and trace your flows without writing test scaffolding. And it gives you a deployment path from localhost to a production server with minimal ceremony.

For Flutter developers specifically, the dual-runtime nature of Dart makes Genkit uniquely powerful. Your AI logic can live in a Shelf backend or in your Flutter client, and because both sides are Dart, they share schemas, types, and mental models. The complexity that comes from maintaining separate server and client representations of the same data disappears.

There has never been a better time to start building AI-powered applications with Dart and Flutter. The tooling is here. The framework is here. The model ecosystem is richer than it has ever been. Genkit Dart brings all of it together in a way that's idiomatic, type-safe, and genuinely a pleasure to work with.

References

Official Documentation & Core Resources

Packages & Plugins

Framework Integrations

Core Concepts & Guides

AI Providers & Integrations

Developer Tools



Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

Beyond the Hype: Is AI taking the fun out of software development? by Colin Eberhardt

1 Share

In this episode, I’m joined by Dean Kerr (Lead Developer) and Amy Laws (Developer) to discuss ‘The Experiment’ – a four‑week study we ran to explore how AI really affects software development. Instead of synthetic benchmarks, the project team tackled genuine issues in an open‑source project, alternating between AI‑assisted work and going completely ‘cold turkey’.

The contrasts were striking. Amy’s AI‑free period exposed how dependent everyday development has become on instant summaries and contextual answers. Meanwhile, Dean found that adopting a simple agentic loop (Analysis → Implementation → Reflection) helped him make better use of AI rather than blindly accept its output.

Together, their experiences reveal a discipline in flux: developers gain speed and support from AI, but also confront questions about craftsmanship, learning, and where the fun in software now sits as the tools reshape both workflow and mindset.

Subscribe to the podcast

Transcript

Please note: this transcript is provided for convenience and may include minor inaccuracies and typographical or grammatical errors.

Colin Eberhardt

Welcome to Beyond the Hype, a monthly podcast from the Scott Logic team where we cast a practical eye over what’s new and exciting in software development. Everything from Kafka to Kubernetes, AI to APIs, microservices to microfrontends. We look beyond the promises, the buzz and the excitement to guide you towards the genuine value.

I’m Scott Logic’s CTO, Colin Eberhardt, and each month on this podcast, I bring together friends, colleagues, and experts for a demystifying discussion that aims to take you beyond the hype. In this episode, I’m joined by Amy and Dean, who have been undertaking an experiment to quantify the impact of AI on software development.

And yes, they found that AI resulted in an increased velocity. However, through this experiment, we learned much more about the broader impact AI is having on software development. And the impact on the human beings who are still an important part of this process. This impact is leading us to ask questions about the future of this craft, and ultimately, here, we ask whether it’s taking the fun out of software development.

We pick up the conversation by discussing what this experiment was and why we undertook it in the first place.

Introduction to The Experiment

Colin Eberhardt

We sort of came round to this discussion from something the two of you have been doing, uh, within Scott Logic, uh, recently, that internally we, we seem to call it by the shorthand of The Experiment, which makes it sound really quite grand. But Dean, for the benefit of the people who have no idea what The Experiment is, it would be great if you could describe what the two of you have been up to recently and why.

Dean Kerr

Yeah, so we’ve got a small sort of AI Incubator team within Scott Logic, and we were looking at running an experiment, as you say, into the effectiveness of AI when it comes to, uh, development. Both from a sort of a qualitative standpoint and also from a quantitative standpoint. So, how much faster does it make you, but also, and it could be more importantly here, how does it make you feel, and does it improve or sort of reduce job satisfaction?

And we ran this experiment over four weeks. So, we had two phases of The Experiment primarily. We had a two-week upskilling phase where we got to grips with an open source library of our choosing. We preselected 10 issues from that open source library. Uh, making sure that the issues were sort of real-world issues that we’d typically encounter in our role as software consultants.

So, issues that are relevant to us. And then, the second half of The Experiment, the latter phase, was actually going away, pairing up and then individually within those pairs, working through each of those 10 issues using an approach. One approach was actually “ cold turkey” – no AI whatsoever – which was quite interesting; some team members who had used AI previously are now going back to no AI, which has an interesting viewpoint there. And then the other approach was using what we termed basic AI. So, in this case, it was what we get from GitHub Copilot on the sort of bare-bones subscription, which is roughly, I think $20 a month per seat. And we picked a sort of free model. Which was GPT-5 mini.

Colin Eberhardt

Cool. So I guess there are lots of people and organisations trying to measure the impact of AI, but The Experiment, as we call it, does feel a bit different. A lot of the benchmarks that you see published tend to be more clean-room environments. Uh, often they are measuring the effectiveness of just the AI itself, not AI plus a human being.

It feels like this experiment is a little bit more of a better reflection of the real work of a developer in a complicated and potentially unfamiliar environment. From your perspective, Dean, does it, did it feel like a good reflection of real work?

Dean Kerr

I believe so. I think it definitely felt closer to home. Like you said, there are plenty of experiments out there that we could lean back on that are fairly clean room. They’re actually quite wide-scale as well, whereas we kept ours sort of smaller and simpler, and I think more focused.

Importantly, so that we have evidence from these experiments that is actually relevant to us and our domains that we typically work in as well.

Real‑World Workflows: Open Source, Quality and Benchmarks

Colin Eberhardt

So why pick an open source project over just some internal code base? What was the thinking behind that?

Dean Kerr

I think there were two caveats to picking an open source project, one of them being the ability to actually contribute back to open source. So we ended up picking a mock service worker, and the plan there is to contribute back to some of the libraries that we ourselves have used professionally in our day-to-day roles. So it’s nice to contribute back there. But I guess secondly, there was also a sort of proof of a mechanism in play here, so that a lot of the experiments, you know, run an issue for you, the standard software development lifecycle. And we could actually use as proof the fact that the issues we worked on were actually reintegrated into a mock service worker or the open source library as a sort of a gold marker or gold standard to say, “Hey, the work we did, it wasn’t just slop.” I know there was a lot of, uh, talk recently around a lot of open source libraries just getting a load of pull requests put up. But we wanted to ensure that our work was relevant and appropriate.

Colin Eberhardt

Yeah, that’s really interesting because the benchmarks that the organisations that are developing the foundation models, they have a benchmark and some of them are relatively real-world things like SWE-Benches, a bunch of GitHub issues, and their goal is to typically make a suite of tests pass.

So, they’re demonstrating that it’s functionally correct, but there isn’t really anything that assesses the quality. Whereas not only are you doing something of actual value by fixing a real-world issue, the fact that the quality gate is outside of the team means a maintainer has to go, “Yeah, I’m, I’m happy with that.”

That’s a real rubber stamp that the solution that you came up with, whether you used AI or not, was of good quality.

Dean Kerr

There were a couple of issues we encountered as well, where we had maybe two or three solutions that were all equally valid, but it was more a matter of preference which one would be accepted. And it was almost sort of second-guessing what the library owner themselves would prefer. So, each of these three solutions, they all tackled the issue slightly differently, and that meant trade-offs in different ways for the mock service worker.

Colin Eberhardt

So, one of the things that I found interesting about this experiment was not the obvious. The spoiler is, funnily enough, AI makes you faster. And I don’t think anyone’s surprised by that. I think we came up with a factor of 1.9. Could have been any number, really. We knew that it was, or at least we hoped that our experiment was going to demonstrate that you’re faster, because we certainly feel that. What I found more interesting and quite surprising is some of the things that, that you as the team learned along the way. And I think some of the most extreme learnings and the most interesting learnings came from the team that wasn’t allowed to use AI at all. What, what you called the “cold turkey” team.

Cold Turkey Begins: Rediscovering Pre‑AI Development

Colin Eberhardt

Now, Dean, you were lucky, you got to use AI even if it was the old, slightly rubbish version. But Amy, I’d love to hear more about your experience, because you were in cold turkey land, and I know you worked alongside Andy. And what I found really funny was that he, he went, he went deep, he tried to eradicate AI from his daily personal life as well. What, what, what was it like having AI taken away from you?

Amy Laws

Yeah, so I think like coming into this, um, obviously being on the, um, AI Incubator team, we’re kind of encouraged to explore and use it more heavily than other people. So, it really was kind of a contrast having to go from using AI and experimenting with it to not using it at all. So, when we say no AI, Andy and I really tried to do everything we could to avoid AI. So, we disabled it in our IDEs because what I forgot is that it’s so integrated now that, although I wasn’t actively opening the Copilot chat panel, the autocomplete was still on, so I had to fully disable it. The same, even doing kind of Google searches, which is kind of your fallback, the AI search automatically pops up at the top.

So, you are having to scroll straight past it, and it’s one when you’re actively trying to avoid it, you realise just how ingrained it has become in everything that we do. So, that was kind of a bit of a learning curve, I guess, reverting to an older way of working, of picking up, going back through your Stack Overflow and forums and reading docs and those kinds of things that I’ve not had to do for quite a long time.

Colin Eberhardt

What was it like going back to Stack Overflow? Because I’ve seen the statistics that the Stack Overflow traffic has dropped considerably, so we know fewer people are using it, which makes sense, but for the few people that are using it, so you were forced to use it, what was it like going back to it? Is there a feeling that information is now lacking because Stack Overflow only exists because of an ongoing question-and-answer flow? But what’s it like there now?

Amy Laws

Yeah, definitely. Like, I guess when I was using it a couple of years ago, I would kind of automatically discount older answers or take them with a pinch of salt because things in tech move so quickly. Something that was answered five years ago might not necessarily be relevant anymore. But that’s really hard to do these days because, as you said, the response rate and, I guess, people asking questions on it, are dropping so rapidly. We just found that a lot of the things we were Googling or like searching on there, there just weren’t modern responses, and it may not be compatible with the versions of the libraries we had and that kind of thing. So, I definitely found myself having to search a lot harder on it than I would’ve previously.

Colin Eberhardt

Yeah, so it is weird. We are, We are getting to a point where, without AI, it’s fundamentally harder to find the answers.

Amy Laws

Yeah, I definitely agree with that. And I guess, I think I’ve lost the skill a little bit as well. Um, so I’ve not had to search through like documentation for quite a long time, and I think there’s kind of a skill and a nuance to that, that you kind of have without realising it.

Colin Eberhardt

Yeah. So, taking a step back, when you talk about AI-assisted software development, most of the time you think about AI writing the code.

Losing Old Skills, Missing New Tools

Colin Eberhardt

And another thing that I found really interesting in this experiment was that so much of what you missed was not the fact that it wrote the code for you. It was all the other things.

Can you talk about that? I mean, you’ve already talked about searching for answers to questions, but there were a lot more examples than that, weren’t there?

Amy Laws

Yeah, so a big one for me was kind of summarising information. So, it’s already been spoken about, but we worked on open source issues. But what we did with that is we actually went from the oldest ones on the backlog to the newest. So, a lot of the issues that we had were quite old. Some of them had very, very lengthy comment threads; some of them had over 60 comments. And a lot of that was noise, so people saying, “Oh, have you tried this workaround? This might have been fixed in this version.” No, it hasn’t. And all that kind of information that isn’t really that helpful a few years on.

Trying to kind of understand that entire comment thread and retain the important bits of information was quite difficult, especially when our job involves things other than just coding. So I’d kind of get myself up to speed with it and then get broken off to go to a meeting or something, and then coming back and trying to get back into that thread and what was relevant and what wasn’t was quite hard.

And I felt it was frustrating to know that if I had AI, I could have just thrown that issue into AI and got a nice summary. And I think that’s one of the things of having had it removed, you realise what you’re missing more.

Colin Eberhardt

Yeah, absolutely there. And thinking about how I use it and to, to your point, things like through Google search, I mean, often if I’m looking for something, I still use Google pretty much in the same way I’m trying to find a thing. But if I’m asking a question, more often than not, its AI-generated summary will answer that question. And I don’t have to navigate to a different site. It’s changed the way that I work without me intentionally, choosing that change or even acknowledging that’s the change that’s taken place.

Dean Kerr

I guess it’s one of the, sort of, the life cycles of the internet I’ve seen where it started with IRC (Internet Relay Chat) and message forums and things like Stack Overflow might be the next sort of format to sort of be cannibalised, really by the next format, which may well be just an input box where you ask AI.

Colin Eberhardt

Yeah, and that’s a massive rabbit hole that I, I’m not sure we’ll even go down because I, I couldn’t work out, well, I can’t work out what the future might look like because we know that the AI was trained on that data set, that’s what makes it so powerful, and the feedback loop has gone. If we are not asking questions, what does the AI train on? As I said, we’ll not go down that rabbit hole ‘cause I have no idea what’s gonna happen.

But getting back to some of the things that you mentioned about using AI to help you answer questions, to summarise information. What I find really interesting about that is that there’s a lot more tolerance for error in the AI. We, we focus a lot on how much code can it emit? How, how good is that code? And, and if it’s not good, some people sort of reject AI for software development to a certain, to a certain extent because they don’t think it writes quality code.

Whereas when it’s summarising an issue thread or you’re just having a conversation with it, it feels like you can be a lot more tolerant. When you were using it to summarise issues, did you think about what the quality of the AI summarisation is, or was it just that it felt good enough?

Dean Kerr

Yeah, I guess for me it did feel like it did a pretty good job. Like Amy mentioned, there are pretty long comment threads in some of the issues, and there is the case of, yeah, do you trust the summary because things will get cut out, and the AI is making a judgment of what things are relevant and no longer relevant.

But looking, you know, I had a look afterwards, ‘cause the, the raw sort thread is there. And it did seem to do a pretty good job at that. I think what’s interesting is, I guess it’s, in this case, it was a GitHub issue, but it could equally also be a Jira issue or a Trello issue. And one thing I didn’t really get to explore, with the GitHub issue, is using the sort of multimodalness of modern AI now, where in Jira you typically have screenshots and, it could be error messages, or stack traces that you can all feed to the AI to summarise as well. So, I think I did a good job with the textural summary. I’ll be really interested in terms of next, if I feed it more than just one piece of data, how it would actually do in terms of summarising all that as well.

Colin Eberhardt

Yeah, I guess this gets into, we’ve talked a bit about going cold turkey, and how that really made you sort of reflect on how much you use AI, and to a certain extent, we’re all becoming dependent on AI. Another thing I found interesting in the experiment was from the team using AI, and that it wasn’t a case of, oh yeah, roll up our sleeves, we are using AI.

Agentic Loops and Structured AI Workflows

Colin Eberhardt

Dean, you talked about the approach that you are using and, and you, you, you sort of described it as a particular pattern of Analysis → Implementation → Reflection. How did you get to that point? Why? Why did you sort of feel the need to almost formalise your own approach?

Dean Kerr

There’s been a lot of sort of literature recently that talked about closing the feedback loop with agentic AI and the sort of improvements that can be made through that. And, um, yeah, there’s some, for example, some recent posts on porting across things as large as web browsers or parsers with relative ease and swiftness as well.

So, I thought if that could work for sort of a larger-scale project going, I guess from a relatively greenfield, ground zero to a fully working application, I felt like it would be equally valid for just picking up a single issue as well. So yeah, I think putting a little bit of effort in at the very beginning, uh, knowing that I had to tackle 10 issues, to set up a, a lightweight harness that Analysis → Implementation → Reflection, I felt that that would pay dividends.

So, and I think it did in the end, uh, the, the Analysis phase, looking at the issue, summarising the thread for me, giving me the chance to interject, I didn’t agree with what it had summarised or if it was a bit off in, in, in some respects to its understanding of the issue. I gotta remember I was using the free model from around August, so I think the capabilities of models have jumped since then. So, um, I think it didn’t get things as right as the premium models. So having the ability to interject was quite useful. The implementation phase as well, where I could set the model off now that I have sort of double-checked its understanding, let it run implementation against some test cases that were built during the Analysis phase and in a sort of red/green, uh, loop, uh, run into those test cases pass.

Colin Eberhardt

So, was that actually an agentic loop? Did you basically instruct it once you’ve done your analysis and you’ve built up a test suite? Do you then basically say, right, now you can solve the issue.

Dean Kerr

Yeah. And, if it was a feature, implement the feature, or if it was a bug, you know, implement the fixes in it. I think you read in these articles, and it all feels very grand and formal, but it is really just prompting the agent to run tests until they pass. It’s as simple as that, really.

It doesn’t have to be a heavyweight, super complex harness, uh, you can get away with a, with a simple prompt, really just to say, use red/green.

Colin Eberhardt

Yeah. And that, that’s something I find really interesting about AI and agents as a tool, that they’re not in any way opinionated. And by that I mean they don’t have an opinion about how you should use them. And I dunno, Dean, whether you’ve, whether you’ve had the time to think about how you used these tools in the past, was it more because we were running this experiment, you thought, I need to have a think about how I approach this. I’ve heard of agentic loops. I, I believe that to be a productive way of working. I’m gonna spend a lot of time working out how I approach the construction of an agentic loop. I mean, was that quite new to you, to spend that much time considering how you use this tool?

Dean Kerr

I think a lot of it’s fairly new to a lot of people. It moves that quickly. And that was the, I guess, the beauty of the first phase of the experiment, where you get two weeks to do upskilling. A lot of studies, uh, as you’ve seen elsewhere, don’t give anyone any time to get to grips with the approach, which is, you know, what model you may be using, what tooling, be that GitHub Copilot or Claude or others. And then the approach being agentic AI, for example. So, having the time to actually experiment and see what does and doesn’t work for a particular approach gave me the time necessary to fall into that sort of feedback mechanism, which is probably one of the latest styles of approaches that people are currently doing at the minute.

Colin Eberhardt

Yeah, I think this one was inspired, I think, by a study by METR, where they were looking at the impact of AI, I think, on open source maintainers. And what they effectively proved was that people who are experienced with AI are better at using AI. That was sort of paraphrasing it. But Amy, what are your thoughts on Analysis → Implementation → Reflection? To a certain extent, does that potentially reflect the way that you, as a human being, approach the problems?

Amy Laws

Yeah, I think so. Obviously, we didn’t have the AI to help me with it, but I think it is kind of an extension of our natural workflow. And I think from using AI for other things, um, the more and more I kind of treat it in the same way that I would work without it, I have found it is more effective.

I guess, kind of naturally, the Reflection part is the most important. And I would naturally do that before I put something up for PR (Pull Request). I would kind of sit and like almost review my own PR before I put it up. And I think probably with AI, I’ve developed a bit of a tendency not to do that quite as much or not be quite as critical.

Whereas I think going back to not using it, I made sure that I really, really understood what every line of code was doing and that I was kind of happy with it. And I think that level of understanding is something that I didn’t realise that maybe I’ve become a bit more lenient with.

Colin Eberhardt

Yeah, I think you make a great point ‘cause it’s, it can be really quite hard to work out how you should be using AI, and it’s easy to think, oh, I’m using it wrong, but I use a similar approach. I, I think, how would a human being do this particular task, whether it’s software development or something else, and more often than not, that’s a pretty good path forward. On the reflection side of things, when you mentioned that Dean, I realised I don’t think I’ve ever done that. I don’t think I’ve ever, when using AI to write code, asked it, “Why did you do that? What was your reasoning for doing that?” That was really eye-opening for me. I dunno whether that’s a thing that people typically do.

Dean Kerr

I think with a lot of things, I guess it is just drawn from experiences before AI. As a software developer, if a junior team member put up a PR, you’d ask these questions, in your head at first, “Okay, a solution’s being proposed to me, but what are the alternatives and why did they get ruled out”, so to speak?

So, it’s a natural tendency to reflect after you’ve done a bit of work. And I think with AI, there’s the, I guess, there’s also the tendency to just submit whatever generates immediately, and that always feels a little bit wrong to me. I like to sit and sort of study and stew on a particular solution, just for a little bit until I have the confidence and the, I wouldn’t say bravery, but to put it up for review by others.

Colin Eberhardt

I wonder if there’s almost a psychological OB obstacle here, because I don’t think AI genuinely thinks, and part of me is almost reluctant to ask it, “Why did you implement it this way? What other options did you consider? And then asking it, “What other options did you consider?” I don’t think it really considered options. But it sounds like you could ask it those questions and still get a valuable output.

Dean Kerr

Yeah, it’s, it’s treating it like I guess a fellow developer. It’s it is a Large Language Model at the end of the day, but, yeah, talking to it as you would a human can, in a weird way, the closer you bring it to your old or current ways of working, the better you can understand the output half the time.

Colin Eberhardt

And again, this is the weird thing about the tool, I think sometimes understanding that it’s a large language model, understanding the basics, like what a prompt is, context engineering, context length. I think that’s really helpful. But then, sometimes that actually understanding what it is feels counterproductive. Sometimes, you just have to suspend disbelief and pretend it’s a human being. It’s very weird.

The Future of AI Work: Autonomy, Architecture and Senior Skills

Colin Eberhardt

So, just taking a step back, this is a difficult one, but where do you think this technology’s heading in the future? Do you think we are gonna spend a lot more time effectively talking to AI and getting AI to write our code for us? It feels almost inevitable. What are your thoughts?

Dean Kerr

I think for the vast majority of use cases it’s gonna move on that sliding scale towards full autonomy, really. At the minute it’s sort of AI augmented development, but, um, I guess the step next after augmented is more inclined with autonomous. So, moving away from interjecting as much and perhaps just being more of a reviewer rather than a co-creator of software.

Colin Eberhardt

Yeah. And Amy, as an example, outside of this experiment, does that reflect your day-to-day? If you’re writing code for whatever reason, are you typically spending more time thinking about how you can encourage AI to write the correct code or create a harness or an agentic loop around it?

Is that your mindset these days?

Amy Laws

Yeah, I think it is. So, I’d probably describe myself at the minute as using AI to maybe get 90% of the way to a solution. So, kind of using it to do a lot of the boilerplate and then being able to refine it on top. But to get that boilerplate to a good quality, as you said, there are other considerations that you’ve got to put in.

So yeah, setting up feedback mechanisms in a way that the AI can usefully iterate, if that’s something that you want to do. I think I spend more time defining my specs at the start and kind of thinking really carefully about what it is I want to build, because I know that I can take the leaps a lot faster.

So, having that defined upfront is something that I’m having to do a lot more. I guess previously you would kind of build a little bit and then maybe think about what the next steps were, whereas now it feels like you’ve got to think two or three steps ahead because you don’t know how fast you’re gonna be able to jump between them.

Colin Eberhardt

Yeah, and that’s the interesting thing, is that it feels like a very different type of skill, and more often than not, we associate that particular skill with being relatively senior and experienced within software. Dean, I can imagine that, well, you described it yourself, a lot of the experience you’ve used to help you make the most of AI is the experience that you’ve gained from working in software for a while.

And Amy, I know your, your situation’s slightly different in that most of your career has been with AI. For the people who are joining now, where AI is the tool that they will immediately be given, how do they gain those skills that you think are important, and I think we all agree are important to being successful with AI?

Amy Laws

Yeah, I think that’s a really hard one, because it’s something that is quite a new challenge. I think I’ve relied a lot on the skills that I first learned when I was learning to program. Kind of debugging is becoming really important. Often, I found that AI is not great at helping debug things.

So, I think it probably is a case of challenging yourself to maybe not go completely cold turkey as we did, but maybe step back from the tools a little bit and make sure that those fundamentals are there, because I think you need both ends of the spectrum. You need to be able to get into the nitty gritty and, I dunno, debug a hard issue or resolve an issue that AI can’t, but you’ve also got to be able to do the, like other extreme, as Dean said, of working as a more senior team member and I guess almost like treating the AI like a junior developer. And I think it’s gonna be quite a hard one for people to learn both of those ends in parallel.

Colin Eberhardt

Yeah, I agree. I think there’s gonna have to be a really considered change to how people learn because it would be all too easy to give someone inexperienced an AI tool and see them suddenly be very productive and think, “Okay, there’s no need for them to learn anything.” For people who are new to software development, I think we’re gonna have to intentionally slow down and make sure they do learn some of the skills that take a little while to pick up and work out how to do that.

To your point, whether it is to a certain extent, taking the tools away. It feels like we’ve got to, to a certain extent, ignore the obvious productivity benefit and slow it down a little bit.

Amy Laws

Yeah, I guess it’s the same when you train anybody junior in anything. Like, there is always a bit of a slowdown in speed for long-term gain, and I think it’s the same kind of idea, just in a different setting.

Colin Eberhardt

It is – the problem is that in a simple sense, we’re measured on two things: speed and quality. And more often than not, speed is the thing that is more visible than quality. And I think that’s where we have to be very, very careful not to over-index on speed and forget about quality, ‘cause I think that takes you down really down the wrong path.

Amy Laws

Yeah, I definitely agree with that. I think there’s probably a bit of an ongoing question, as you said, quality and what is it gonna be like to maintain these code bases in 5, 10, 15 years’ time. It’s something that we just don’t know at the minute.

Colin Eberhardt

Yeah. So Dean, from, from your perspective where do you think it’s going and do you think perhaps some of the things that AI is less good at, at the moment it will potentially get better at, ‘cause it, it feels like our role has shifted to, to hear how Amy sort of describes it, spending more time, 90% of your time working out how to describe the problem to the AI so that it is successful, and then maybe the last 10% is a little bit old fashioned. It’s a bit more hands-on. How far do you think that’s going to progress? Do you think AI will eventually be good at architecture? What? Where else will it start to consume our day jobs?

Dean Kerr

It’s a million-dollar question, isn’t it? It’s funny, right? Because AI is extremely knowledgeable. It knows everything. It’s been trained on everything, yet it still needs a little bit of a nudge and a bit of curation when it comes to things like architecture, where a lot of the time, perhaps it’s making architectural decisions based on things it doesn’t know that you might know. It could be a future direction of the product, for example, or even just boiling down to a simple preference for how you think a library should scale for the end users. So, I’m not sure if AI would ever get to the point where it could get those sort of, uh, characteristics because the information just wouldn’t be available to them, but I think they’d certainly get better at making a good guess at it.

Colin Eberhardt

You make an interesting point there. When you talked about architecture, you talked about scalability and uh, I think talking about libraries, you talked about the end user. You start moving into concerns that AI just doesn’t have any of the inputs. It’s, it’s good at writing code because you can give it pretty much all the inputs it needs to be successful.

But even when you get to software architecture, that’s not about looking down at the code, that’s looking up at the business problem that, that this technology solves. And whilst AI’s good at helping you sort of summarise Jira tickets and things like that. I haven’t seen any evidence yet that it’s good at discovering what a software product should actually do. I’ve not seen any evidence that it can do that yet. Which is reassuring.

Dean Kerr

Yes, for now. But yeah, that’s, I guess why you went with fuzzy information and, and, uh, preferences. I guess it may have all the access to the code base and the data associated with the code base. It still can make some interesting abstractions sometimes that might make sense to it, but the abstractions might not be human-readable or as human-readable as well, which is a, another interesting characteristic that I could see probably improving in the near future there.

Colin Eberhardt

Yeah, it does feel like the limits are going to be at or close to the point where you need to get humans involved, whatever that might be. That feels like the area where AI will stop being useful. I guess to wrap up, and I think, one of the interesting things about AI is that it’s changing the way that we approach software, but it’s also having an impact on how people feel about software.

And I know Amy, when you went cold turkey, I guess lots of the things that you mentioned were, were generally quite negative, and some of them were just quite funny about how frustrating it was.

Joy, Craft, and What AI Changes About Coding

Colin Eberhardt

But one of the things that you mentioned that I thought was really notable was that you had a sense of pride, or a bit more joy in actually handcrafting the solution.

It’d be great if you could speak more about that. Was it a bit more fun? I know reading issues was drudgery, but the code part. How did that feel?

Amy Laws

Yeah. So, one of the things that first attracted me, I guess, to coding was problem solving, and that kind of high you get when you’ve had a really hard challenge, and you spend quite a long time kind of in the code, and you finally get that test passing or get that feature working. And I think I forgot that that feeling is actually really great.

And when you’ve used AI to write your code, I guess I feel less ownership of it, even though it is still kind of my work, because I’ve not kind of handcrafted and carefully gone through like every line of code. That high just wasn’t quite the same, and that was something that I didn’t realise I missed.

So, a lot of the bugs that we had in this project were often quite hard to pin down, and it was a real challenge to kind of work out what was causing the issue. I think a lot of the time, when you worked it out, the solution was quite simple. But yeah, that really great feeling when you’ve finally fixed a really hard problem is something that first attracted me to this job, and I didn’t realise that I’d kind of missed.

And as you said, like knowing that you have built something or you have solved the problem is something that I guess is going away a little bit as we’re kind of more hands-off with the code.

Colin Eberhardt

Yeah, it’s a really interesting sort of connection we have with the code that we write. I think a suitable analogy for people who have not written software is potentially carpentry. You can imagine that there’s a great difference between, I don’t know, building a chair or something like that and crafting it with your own hands, versus if you just programmed it into a computer and a CNC machine built it. I think that the end result is exactly the same, and it could be exactly the same human being that has achieved that end result.

But I think a lot of people would understand and relate to there being a greater sense of achievement through actually crafting it with a hand chisel than programming a machine to do it.

Amy Laws

Yeah, I think that’s exactly right. And I think as software developers we do have a pride in the things we’ve built, and actually seeing people use them or knowing that people are using them is a really great feeling, and it’s something that maybe is gonna change a little bit in the future.

Colin Eberhardt

Yeah, I agree. I mean, Dean, you’ve been doing software for a long time, and I’m assuming that a lot of the time you’ve been working on software, you’ve experienced that joy of creating an elegant solution or refactoring some code just because you felt it looked better, and that gave you joy.

What do you think about this shift in software engineering?

Dean Kerr

Yeah, it’s interesting really ‘cause as you say, it’s, I guess, I’ve been in the business of the industry for over a decade. And I think things have moved slowly between that decade, I guess, as I gradually got more senior, moving slowly and more away from the code and getting my problem-solving thrills, maybe at a higher level than low-level code. But, I still got immense satisfaction out of fixing a bug that might have taken a long time or took a lot of analysis to get to the bottom of, or like you say, spending a good amount of time with maybe an initial solution that I refactored into a quite elegant one.

So, yeah, there’s still the satisfaction there for that, but there’s equally, you know, you get satisfaction in building applications and products as well. Um, I think I’ve gradually shifted to be sort of in the middle, from doing purely development work to being, you know, a product owner only.

So, I think that’s a nice balance to have. But, I think, AI if it keeps progressing at this rate, I think you’re gonna have less of that sort of lower-level working satisfaction and more of getting satisfaction and morale from solving higher-level or maybe even product-oriented problems going forward.

Colin Eberhardt

I guess taking the conversation full circle and coming back to The Experiment, do you feel any more satisfaction from The Experiment knowing that your pull request eventually got merged and the, and the software that was shipped?

Dean Kerr

I’d say that satisfaction is equal, whether I was handwriting every line or I reviewed it and it was effectively mine, I guess, to the same extent anyway. There’s still that responsibility and ownership of a pull request, regardless of whether it was AI that generated the code for you in one prompt and it “one-shot” it, versus you handcrafting it over a week.

Colin Eberhardt

Yeah, so I guess it’s time to put the ultimate question to each of you and see whether you are, you are happy to answer yes, no, or sit on the fence and go with a maybe. So Amy, do you think AI has taken the fun out of software development?

Amy Laws

I think I’m gonna have to go “maybe” on this one. I think I’ve definitely lost some of the areas that I used to have a lot of joy in, but as Dean said, you kind of find them in other areas, if that makes sense. So actually, I find AI absolutely fascinating and if I can get it to solve a really hard problem for me.

That brings me joy in the same way that fixing a really hard bug would. So yes, sorry to avoid the question, but I think it’s just shifted where we find it.

Colin Eberhardt

Oh, on the fence then. Come on, Dean, can we get you to come off the fence, or are you gonna do the same?

Dean Kerr

Well, you know, rather than no, which is a bit of an absolute, I could say not yet if that’s a good, yeah, second one, anyway. I think for me it’s been really interesting to see all the knowledge I’ve built up in industry so far as a developer, how quickly I thought it’s almost becoming obsolete.

I thought, you know, you’d learn a couple of languages, and maybe one or two of those languages would, you know, fall out of fashion, go out of date and not be as relevant any more. But for the whole development bit itself to potentially be obsolete is a bit sad. But, at the same time, I think I still get the same satisfaction, like you say, to getting a pull request merged or a particular tricky feature deployed, and seeing it used by users. So, I’ve put not yet rather than, no, because, uh, like I said, AI’s changing from week to week. So, it might be no at the minute, uh, but it could, uh, yes later on as it progresses.

Colin Eberhardt

Yeah, I totally get what you mean about this; it does genuinely feel like we’ve reached quite a surprising point. I mean, I think a lot of us could see that this was going to be a big thing, you know, as far back as a couple of years ago, but I didn’t think we would be reaching this particular point. So I guess it’s, it’s my turn and I, I’m gonna fall off the fence. I’m gonna say no, AI is not taking the fun outta software development, but I’m gonna come in with a caveat, that I think the place that you find fun has moved completely. It’s not where it used to be. And I think that’s the sort of critical thing.

And I sort of almost discovered this myself from looking at some of the hobby projects, ‘cause you know, I don’t write much code in the day job, but I still love writing code and building things. And I looked back, and four or five years ago, a lot of the stuff I was doing was with WebAssembly, and I was writing an emulator for an Atari 2600.

And the funny thing is. I never played a game on it. That wasn’t the intention. I, I just wanted to build the thing, and that was it. Whereas now, the hobby projects I work on tend to be ones where it’s a thing I actually want to use, and the apps themselves are really boring. You know, Crud style, form-based applications where the code itself would bore me stupid.

But, I’m having more fun actually building things that I actually use. So, for me personally, I still find it fun, but the things that I find fun have changed completely in the space of five years. Oh, I get the final say. Cool. Well, ‘cause I’m the one that, that came off the fence.

Episode outro

Colin Eberhardt

And that brings this episode to a close. While AI hasn’t necessarily taken the fun out of software development, the change in our role and focus means that this fun is something we have to find somewhere else, and it may not be derived directly from the act of writing code. This is going to be an uncomfortable realisation for some people.

If you’ve enjoyed this episode, we’d really appreciate it if you could rate and review Beyond the Hype. It’ll help other people tune out the noise and find something of value, just like our podcast aims to do. If you want to tap into more of our thinking on technology and design, head over to our blog at scottlogic.com forward slash blog.

So only remains for me to thank Amy and Dean for taking part, and you for listening. Join us again the next time as we go Beyond the Hype.

Read the whole story
alvinashcraft
28 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

AI data centers may heat nearby land by up to 9.1°C, study finds

1 Share
Artificial intelligence data centers generate enough heat to raise temperatures in surrounding areas, according to new research examining how these facilities influence land surface temperatures over time.  Scientists found that land near AI data centers can warm by an average of 2°C (3.6°F) within months after operations begin, with extreme increases reaching 9.1°C (16.4°F). The warming effect can extend as far as 10 kilometers (6.2 miles) from the facilities and may already affect more than 340 million people who live within that distance. The research was led by Andrea Marinoni of the Earth Observation group at the University of Cambridge… [Continue Reading]
Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

Oracle cuts 491 jobs in Washington state as it embraces AI-led engineering

1 Share
Oracle’s Cloud Experience Center in downtown Seattle. (GeekWire File Photo / Todd Bishop)

Oracle is laying off 491 employees in Washington state, according to a filing Tuesday from the state Employment Security Department.

The cuts impact workers at two Seattle offices as well as remote employees and take effect June 1. The cloud and database giant stated in its WARN letter that the offices will not be closing.

Earlier this month, Bloomberg and others reported that Oracle was planning to cut thousands of jobs across the company as it tries to fund the high-cost deployment of new data centers. The reductions are also the result of AI-driven efficiencies within the organization, according to comments by Mike Sicilia, Oracle’s co-chief executive, in an earnings call March 10.

“The use of AI coding tools inside Oracle is enabling smaller engineering teams to deliver more complete solutions to our customers more quickly,” Sicilia said, according to the publication CIO.

GeekWire has reached out to Oracle for comment on the layoffs.

The Washington layoffs affect more than 230 software developers across multiple seniority levels and an additional 48 employees with the title of software development. The cuts include workers in senior director and vice president roles, as well as managers, product developers, product managers, program managers, site reliability developers, technical analysts, user experience developers and others.

The layoffs are the latest in a series of Oracle reductions. In August the company laid off 161 workers, followed by 101 employees in October. By last fall, Oracle had approximately 3,800 employees in the Seattle area, according to LinkedIn.

Oracle has grown its presence in the region over the past decade, tapping into the area’s engineering talent pool as it battled Amazon and Microsoft in the cloud. In recent years, the company has established partnerships with both Seattle-area giants.

Now all three, plus other tech companies, have been undergoing multiple rounds of job reductions, with recent Meta cuts impacting 168 Washington workers and T-Mobile confirming new layoffs last Friday.

Related:

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

Agents League: Meet the Winners

1 Share

Agents League brought together developers from around the world to build AI agents using Microsoft's developer tools. With 100+ submissions across three tracks, choosing winners was genuinely difficult. Today, we're proud to announce the category champions.

🎨 Creative Apps Winner: CodeSonify

View project

CodeSonify turns source code into music. As a genuinely thoughtful system, its functions become ascending melodies, loops create rhythmic patterns, conditionals trigger chord changes, and bugs produce dissonant sounds. It supports 7 programming languages and 5 musical styles, with each language mapped to its own key signature and code complexity directly driving the tempo.

What makes CodeSonify stand out is the depth of execution. CodeSonify team delivered three integrated experiences: a web app with real-time visualization and one-click MIDI export, an MCP server exposing 5 tools inside GitHub Copilot in VS Code Agent Mode, and a diff sonification engine that lets you hear a code review. A clean refactor sounds harmonious. A messy one sounds chaotic. The team even built the MIDI generator from scratch in pure TypeScript with zero external dependencies. Built entirely with GitHub Copilot assistance, this is one of those projects that makes you think about code differently.

🧠 Reasoning Agents Winner: CertPrep Multi-Agent System

View project

CertPrep Multi-Agent System team built a production-grade 8-agent system for personalized Microsoft certification exam preparation, supporting 9 exam families including AI-102, AZ-204, AZ-305, and more. Each agent has a distinct responsibility: profiling the learner, generating a week-by-week study schedule, curating learning paths, tracking readiness, running mock assessments, and issuing a GO / CONDITIONAL GO / NOT YET booking recommendation.

The engineering behind the scene here is impressive. A 3-tier LLM fallback chain ensures the system runs reliably even without Azure credentials, with the full pipeline completing in under 1 second in mock mode. A 17-rule guardrail pipeline validates every agent boundary. Study time allocation uses the Largest Remainder algorithm to guarantee no domain is silently zeroed out. 342 automated tests back it all up. This is what thoughtful multi-agent architecture looks like in practice.

💼 Enterprise Agents Winner: Whatever AI Assistant (WAIA)

View project

WAIA is a production-ready multi-agent system for Microsoft 365 Copilot Chat and Microsoft Teams. A workflow agent routes queries to specialized HR, IT, or Fallback agents, transparently to the user, handling both RAG-pattern Q&A and action automation — including IT ticket submission via a SharePoint list.

Technically, it's a showcase of what serious enterprise agent development looks like: a custom MCP server secured with OAuth Identity Passthrough, streaming responses via the OpenAI Responses API, Adaptive Cards for human-in-the-loop approval flows, a debug mode accessible directly from Teams or Copilot, and full OpenTelemetry integration visible in the Foundry portal. Franck also shipped end-to-end automated Bicep deployment so the solution can land in any Azure environment. It's polished, thoroughly documented, and built to be replicated.

Thank you

To every developer who submitted and shipped projects during Agents League: thank you 💜 Your creativity and innovation brought Agents League to life!

👉 Browse all submissions on GitHub

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

YouTrack Introduces Whiteboards

1 Share

YouTrack 2026.1 introduces Whiteboards, a new way for teams to plan, brainstorm, and collaborate. Connect your current projects to get a better overview and organize work, or add notes and turn them into tasks and articles when you’re ready. This allows project managers and teams to plan from scratch, collaborate on ongoing projects, and ensure every activity is linked to your work in YouTrack.

We’ve also streamlined project access management for administrators and improved the notification center experience for all users. For teams that rely on AI tools in everyday workflows, there are new ways to use YouTrack within your existing stack, including an n8n integration and new remote Model Context Protocol (MCP) server actions for the Knowledge Base.

The YouTrack app ecosystem continues to grow, with 50 apps now available on JetBrains Marketplace. We’ve highlighted some of the latest apps for project managers, QA and support teams, and more.

Turn planning into action with Whiteboards

We’re excited to introduce Whiteboards – a new, flexible space where you can visualize your projects as you work on them. Whiteboards extend YouTrack’s built-in functionality and are available to every user and agent.

Whether you’re a project manager structuring a plan, a team brainstorming together, or an individual organizing your own work, Whiteboards support your approach. You can restructure existing projects, plan what comes next, and capture everything along the way – all in one place.

How Whiteboards work in a nutshell

Turn ideas into tasks and articles

Work on Whiteboards on your own or together with your team, using cards and text blocks to shape your ideas and turning them into tasks or documentation with a single click.

You can also connect any notes with links, so that linked cards reflect real relationships between tasks in your projects.

Import and work on ongoing projects

When you need to reorganize your current work, you can bring existing tasks, tickets, and articles onto your Whiteboard and update them directly as your plans evolve. Every change you make is instantly reflected across your projects – for both imported items and those created on the Whiteboard. Task dependencies are synced automatically as well.

Navigate and track your progress

You can return to any Whiteboard at any point to see how your plans have evolved. Zoom in to focus on specific areas, switch to full-screen mode for a bird’s-eye view, or use search to quickly find and navigate to relevant content.

You can also control who can view or edit your work, with visibility on shared Whiteboards following your YouTrack project permissions.

Adapt Whiteboards to your work

Since Whiteboards start from a blank canvas, you can shape them to fit any work scenario – from creating a cross-project overview to focusing on specific topics in detail. Here are a few ways you can use them.

Plan projects 

Planning often starts with ideas rather than structured tasks. Project managers can outline team roadmaps, restructure ongoing work, or visualize new projects. As cards are converted into YouTrack tasks and articles, your team can continue working in projects without interruption.

Brainstorm with the team

Any team member can add and update cards, text blocks, and their connections in real time or asynchronously. Whether you’re running a retrospective, building a mind map, or sketching out ideas together, Whiteboards adapt to your team’s distinct workflows. For example, product teams can shape new features, while support teams can map customer journeys.

Share knowledge with your users

Administrators can create shared Whiteboards to explain workflows, processes, and project structures to users or guests. By combining guidance notes with direct links to relevant tasks and articles, you make it easier to access everything in one place.

Organize personal work

Individual users can also use Whiteboards for their own planning – capturing ideas, organizing a week, or taking notes that are visible only to them. When you’re ready, you can invite others to collaborate and turn your private whiteboard into shared work.

Design enhancements for administrators and teams

Streamlined access management for projects

We’ve added further YouTrack design updates to make working on your projects easier. The new People tab on the project overview page simplifies how administrators manage project teams. Administrators can add or remove users and groups, assign roles, and filter team members by roles and permissions – all without having to jump between pages. The previous Team and Access tabs are now combined into a single, streamlined view. You can learn more about all changes in our documentation.

Here’s how it works in practice. When new team members join, you can add them to the project and assign their role right away. To manage existing project members, you can filter users and groups, then update or revoke their roles as needed. The People tab also lets you control access for people outside the project team, giving you a clearer overview of who can access your project.

Full-page notification center

Every user can now expand the notification center to a full-page view and reply to comments directly from there. This makes it easier to quickly respond to feedback or discussions without switching contexts.

Use YouTrack inside your existing stack with AI-powered integrations

For many teams, AI tools are already part of their everyday workflows. In addition to our built-in free AI assistance, we’re introducing new AI-powered integrations so you can use YouTrack from the tools you already rely on.

n8n integration

n8n is a workflow automation platform that connects your tools and services without code. YouTrack now has a dedicated node in n8n, so you can automate workflows and connect YouTrack with hundreds of apps – sync data, trigger issue updates, execute YouTrack commands, and build cross-platform workflows with ease.

You can build your own workflows or use existing templates. For example, you can configure workflows to collect data from third-party systems into YouTrack, update tasks based on actions from your AI agents, share YouTrack content with other systems, and much more. This means YouTrack can be integrated into every step of your automation.

New remote MCP server actions for the Knowledge Base

For teams working from their existing LLM, IDE, or agent platform, we’ve expanded the number of predefined actions available via YouTrack’s remote MCP server. You can now use AI-powered tools to find, create, and update Knowledge Base articles and create tasks with pre-configured visibility.

If you want to set up the remote MCP server for your coding agents, such as Claude Code or Cursor, or for integration platforms like Zapier and Make or other automation tools, you can find detailed instructions in our documentation.

YouTrack Helpdesk experience for standard users and agents 

We’ve enhanced the experience for standard users and agents participating as internal reporters in helpdesk projects, making it easier for them to submit and track their own requests. They can now seamlessly access reporter functionality while submitting tickets and receive reporter email notifications.

When the product team needs to join the conversation, standard users now have a similar experience to agents when replying to reporters via public comments or email CC.

New apps on JetBrains Marketplace

Check out some recent apps from our certified consulting partners and third-party providers, now available to enhance your YouTrack experience.

Apps for project managers and teams

Planning Widget by CARL von CHIARI helps managers plan team activities in a calendar view. Review the tasks and tickets your team members work on each day, track time spent, and filter the view by employee.

Risk Manager by Rixter AB helps project managers assess project risk levels by building risk-matrix widgets based on the probability and impact of specific outcomes for selected tasks.

Article Approval by twenty20 is a paid app that allows teams to manage approval workflows directly in the Knowledge Base. You can invite approvers and acknowledgers for each article, set due dates, and track approval statuses.

Apps for QA and DevOps teams

Test Case Generator by Depa Panjie Purnama helps users create test cases or generate them using AI models while working on other development tasks.

TestOps Plugin by bodm helps DevOps teams to stay on top of testing while developing features. It brings recent test cases from Allure TestOps directly into related tasks.

Bug Report Constructor by Evgenii Venediktov allows QA teams to collect bug reports using a single template and enables users to quickly create task drafts with pre-saved blocks.

Gerrit Integration by Phoenix Systems helps developers display related Gerrit Code Review changes directly in tasks, including status, approvals, and links.

Apps for support teams

Custom Ticket Views by ​​Appfero is a paid app created to help support teams enhance reporters’ experience when working in YouTrack Helpdesk. It adds a new menu section that shows a reporter their submitted tickets in a customizable view.

Customer Satisfaction by Appfero is another paid app that enables teams to collect feedback on task execution through automated customer satisfaction surveys (CSAT). Configure your custom survey flow and review response analytics directly in YouTrack.

Apps for working with task, ticket, and article content

Clever Checklists by TEKDynamics lets everyone manage daily work with to-do lists by adding a custom checklist to every task in a selected project.

Ticket Templates by Marcus Christensson automatically updates tasks and article content using your saved templates, which can be created based on ticket fields, tags, and various other conditions.

Article Templates by Maksim Fedorov makes it easier to draft articles based on existing Knowledge Base content. You can turn articles into templates and manage them all from a handy Article templates dashboard widget.

Text Replacer by Marcus Christensson allows you to update content across your project on either a one-time or a recurring basis. You can use it for tasks and articles to turn external system IDs into links, replace text with ready-made content, and more.

Copy link and context as Markdown by Maksim Fedorov makes working with content easier by copying selected tasks or article contexts, and their links, as Markdown.

App for administrators

Admin Tools by msp AG is created for administrators and adds a separate page that lets you add custom fields to multiple projects at once, get a clear overview of all projects, and view license information for your YouTrack.

Other enhancements 

Knowledge Base articles now rank more accurately in search results thanks to AI enhancements, helping you find the right information faster. We’ve also introduced other small design updates to further improve your overall experience.

 

Check out the release notes for the full technical details and a comprehensive list of this release’s bug fixes and improvements. For more details on configuring the latest features, see the documentation.

If you use YouTrack Cloud, you’ll automatically be upgraded to YouTrack 2026.1 in accordance with our Maintenance Calendar.

If you have an active YouTrack Server subscription, you can upgrade to YouTrack 2026.1 today.

If you don’t have an active YouTrack subscription, you can use the free YouTrack for up to 10 users to test out the new version before you commit to buying!

For more information about the licensing options available for YouTrack, please visit our Buy page.

Your YouTrack team

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories