Skip to content
ArticlesPower Apps

Power Apps

Building Power Apps Generative Pages with Claude Code

A hands-on guide to building production-ready model-driven app pages using React, TypeScript, and Fluent UI V9 - powered by AI code generation and the PAC CLI.

Model-driven apps in Power Apps have long been praised for their rapid development story: define tables, configure forms, and a working application materializes. But that convenience came with a ceiling. When business requirements demanded a bespoke dashboard, a non-standard layout, or pixel-perfect UX, makers often hit the limits of the declarative surface.

Generative pages change the equation. Introduced as a preview feature in early 2026, generative pages allow developers to write full React + TypeScript components that render natively inside a model-driven app shell. The component runs in a sandboxed iframe, has typed access to Dataverse through a provided Data API, and ships with Fluent UI V9: the same design system that powers Microsoft 365. Combined with an AI-powered code generation workflow through the model-apps plugin, the entire cycle from idea to deployed page shrinks from days to minutes.

This guide walks through the end-to-end process of building a Project Tracker Dashboard as a generative page, covering schema discovery, component authoring, deployment, and verification.

Prerequisites

Before starting, ensure the following are installed and configured:

  • Node.js 18+ (LTS recommended)
  • PAC CLI at the latest version (pac install latest or via the VS Code Power Platform Tools extension)
  • Claude Code with the model-apps plugin installed
  • A Dataverse environment with at least one custom table (this guide uses a Project table with columns like Name, Status, Budget, Progress, and Owner)
  • A model-driven app already created in the target environment

The PAC CLI must be authenticated against the target environment before starting:

Code
pac auth create --environment https://yourorg.crm.dynamics.com

Replace the URL with the actual organization endpoint. Run pac org who to confirm the connection.

Step 1: Setup and Plugin Configuration

With the PAC CLI authenticated, the model-apps plugin handles the integration between the AI code generation workflow and the Dataverse environment. Once installed, it exposes commands for schema generation, code scaffolding, and one-command deployment.

The plugin automatically detects the active PAC CLI auth profile and the selected environment. Developers can verify readiness by listing available model-driven apps:

Code
pac model list

This returns the app IDs needed later for deployment. Note the App ID of the target model-driven app: it will be required during the upload step.

Step 2: Generate the Schema (The Most Critical Step)

Dataverse column names are notoriously unintuitive. A column labeled "Project Name" in the UI might be cr4a2_name or pblab_projectname at the API level, depending on the publisher prefix and the exact moment it was created. Guessing column names, or letting any tool hallucinate them, is the single fastest path to a broken page.

The generate-types command solves this by querying the Dataverse metadata API and producing a TypeScript file with every column name, type, and relationship for the specified tables:

Code
pac model genpage generate-types \
  --data-sources "pblab_project,pblab_task,pblab_deliverable" \
  --output-file RuntimeTypes.ts

This produces a RuntimeTypes.ts file containing typed interfaces for each table. The file includes the exact logical names, option set values, and relationship navigation properties. Every column reference in the page component should come from this file, never from memory or documentation alone.

Schema generation flow: from Dataverse tables to typed RuntimeTypes.ts

Open the generated file and review the column names. Confirm they match what appears in the Dataverse table designer. This five-minute verification step prevents hours of debugging later.

Step 3: Write the Page Component

A generative page is a single TypeScript file that exports a React component. The framework injects props containing the Data API, navigation helpers, and formatting utilities. Here is the skeleton every page starts from:

Code
import React, { useState, useEffect } from "react";
import {
  makeStyles,
  tokens,
  Card,
  Text,
  Badge,
  ProgressBar,
  Spinner,
  Input,
} from "@fluentui/react-components";
import { SearchRegular } from "@fluentui/react-icons";
import type { GeneratedComponentProps } from "./RuntimeTypes";

Note the imports: React 17 (not 18: the generative pages runtime ships React 17), Fluent UI V9 components, and the generated types. The GeneratedComponentProps type provides full IntelliSense for the Data API.

Querying Dataverse

The Data API exposes a queryTable method that mirrors OData query syntax:

Code
const ProjectDashboard: React.FC<GeneratedComponentProps> = ({ dataApi }) => {
  const [projects, setProjects] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const loadProjects = async () => {
      try {
        const result = await dataApi.queryTable("pblab_project", {
          select: [
            "pblab_name",
            "pblab_status",
            "pblab_budget",
            "pblab_progress",
            "pblab_startdate",
            "pblab_enddate",
          ],
          filter: "statecode eq 0",
          orderBy: "pblab_name asc",
          pageSize: 50,
        });
        setProjects(result.rows);
      } catch (err) {
        console.error("Failed to load projects:", err);
      } finally {
        setLoading(false);
      }
    };
    loadProjects();
  }, [dataApi]);

  // render logic follows
};

Every column name in the select array comes directly from RuntimeTypes.ts. The filter uses standard OData syntax. The orderBy accepts a string with the column name and direction.

Accessing Formatted Values

Dataverse returns raw values by default: option sets come back as integers, dates as ISO strings, and currency as plain numbers. For display-friendly values, use the OData formatted value annotation:

Code
const statusLabel =
  row["pblab_status@OData.Community.Display.V1.FormattedValue"] ?? "Unknown";
const budgetFormatted =
  row["pblab_budget@OData.Community.Display.V1.FormattedValue"] ?? "$0";

This pattern retrieves the localized, human-readable label that Dataverse computes server-side. It respects the user's language settings and currency format. Always prefer formatted values for any column that is an option set, currency, date, or lookup.

Styling with Fluent UI Tokens

Generative pages run inside a Fluent UI V9 provider, so makeStyles and design tokens work out of the box:

Code
const useStyles = makeStyles({
  container: {
    display: "grid",
    gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))",
    gap: tokens.spacingHorizontalL,
    padding: tokens.spacingHorizontalXL,
  },
  card: {
    padding: tokens.spacingHorizontalL,
    borderRadius: tokens.borderRadiusLarge,
    backgroundColor: tokens.colorNeutralBackground1,
    boxShadow: tokens.shadow4,
    transition: "box-shadow 0.2s ease",
    ":hover": {
      boxShadow: tokens.shadow8,
    },
  },
  statValue: {
    fontSize: tokens.fontSizeHero900,
    fontWeight: tokens.fontWeightSemibold,
    color: tokens.colorBrandForeground1,
  },
  progressSection: {
    display: "flex",
    flexDirection: "column",
    gap: tokens.spacingVerticalS,
  },
});

Using tokens rather than hard-coded values ensures the page adapts to theme changes (dark mode, high contrast) automatically. This is not optional polish: it is a requirement for any page that ships inside a model-driven app, since administrators can apply custom themes at the environment level.

Putting It Together

A complete Project Tracker Dashboard might include summary statistics at the top (total projects, total budget, average progress), a search/filter bar, and a card grid showing each project with its status badge, budget, date range, and progress bar:

Code
return (
  <div className={styles.container}>
    {/* Summary stats row */}
    <Card className={styles.card}>
      <Text className={styles.statValue}>{projects.length}</Text>
      <Text>Total Projects</Text>
    </Card>

    {/* Project cards */}
    {filteredProjects.map((project) => (
      <Card key={project.pblab_projectid} className={styles.card}>
        <Text weight="semibold" size={500}>
          {project.pblab_name}
        </Text>
        <Badge appearance="filled" color={getStatusColor(project.pblab_status)}>
          {project["pblab_status@OData.Community.Display.V1.FormattedValue"]}
        </Badge>
        <div className={styles.progressSection}>
          <Text size={200}>Progress</Text>
          <ProgressBar
            value={project.pblab_progress / 100}
            thickness="large"
            color="brand"
          />
          <Text size={200}>{project.pblab_progress}%</Text>
        </div>
      </Card>
    ))}
  </div>
);

Project Tracker Dashboard rendered inside a model-driven app

The result is a fully interactive dashboard that inherits the model-driven app chrome, navigation, sitemap, header, and theme, while rendering a completely custom React surface inside the content area.

Step 4: Deploy with a Single Command

Deployment is where generative pages truly shine compared to traditional PCF controls or embedded canvas apps. A single PAC CLI command handles transpilation, bundling, publishing, and sitemap registration:

Code
pac model genpage upload \
  --app-id 00000000-0000-0000-0000-000000000000 \
  --code-file ./project-tracker-dashboard.tsx \
  --data-sources "pblab_project,pblab_task,pblab_deliverable" \
  --add-to-sitemap

Replace the --app-id with the actual model-driven app GUID. The --data-sources flag must list every table the component queries: this sets up the security trimming so the page only loads for users who have read access to those tables.

The --add-to-sitemap flag automatically creates a sitemap entry so the page appears in the app navigation. Without it, the page is uploaded but not visible until manually added through the app designer.

The upload command typically completes in under 30 seconds. Behind the scenes, the PAC CLI transpiles the TypeScript, bundles it with the React and Fluent UI runtime, uploads the package to the environment, publishes all customizations, and updates the sitemap XML.

Step 5: Verify in the Browser

Open the model-driven app in a browser. The new page should appear in the left navigation. Click it and verify:

  1. Data loads correctly: the cards should populate with live Dataverse records
  2. Formatted values display properly: status badges should show labels (not integer codes), budgets should show currency formatting
  3. Search and filter work: if implemented, test the interactive elements
  4. Theme compliance: switch to dark mode in the Power Apps settings and confirm the page adapts
Deploy pipeline: from .tsx to live app in ~30 seconds

If columns show undefined or blank values, the most common cause is a mismatch between the column names in the code and the actual Dataverse logical names. Return to RuntimeTypes.ts and verify every column reference.

Key Takeaways

  • Schema-first development prevents errors. The generate-types command eliminates guesswork around Dataverse column names. Run it before writing any component code, and re-run it whenever the table schema changes.
  • Single-file architecture keeps things simple. A generative page is one TSX file. There is no project scaffolding, no webpack configuration, no package.json to maintain. The PAC CLI handles the build pipeline.
  • Fluent UI V9 tokens ensure theme compliance. Hard-coded colors and spacing will break under theme changes. Tokens adapt automatically.
  • Formatted values are non-negotiable for display. Raw Dataverse values (integers for option sets, ISO dates, unformatted currency) are unsuitable for end users. Always use the @OData.Community.Display.V1.FormattedValue annotation.
  • One-command deployment removes friction. The upload command handles transpilation, bundling, publishing, and sitemap in a single step. Iterate fast.

What Comes Next

Generative pages are currently in public preview as of March 2026. The feature is expected to reach general availability later in 2026, with additional capabilities including multi-file components, shared libraries, and deeper integration with model-driven app forms.

For the latest documentation, visit the official Microsoft Learn page: Create generative pages for model-driven apps.

Developers exploring this feature should keep a close eye on PAC CLI release notes: the model genpage command group is evolving rapidly, and new flags and capabilities are being added with each release.

The combination of a code-first approach, typed schema generation, and single-command deployment makes generative pages one of the most significant additions to the Power Apps developer experience in years. For teams already comfortable with React and TypeScript, the barrier to building rich, custom experiences inside model-driven apps has never been lower.