🥞PancakeJS

Views, Actions, and Data

Pancake apps are built from three types of definitions: Views, Actions, and Data. Understanding when to use each is key to building effective AI-native applications.

Overview

TypePurposeHas UITypical Use
ViewsDisplay interactive UIsYesDashboards, search results, forms
ActionsWrite operationsNoSubmit, update, delete
DataRead operationsNoFetch, search, query

Views

Views combine a server-side handler with a client-side UI. When the AI invokes a view, the handler runs first to fetch data, then the UI renders with that data.

import { defineView } from '@pancake-apps/server';
import { z } from 'zod';

views: {
  dashboard: defineView({
    description: 'Show user dashboard with stats',
    input: z.object({
      userId: z.string(),
    }),
    data: z.object({
      name: z.string(),
      stats: z.object({
        views: z.number(),
        clicks: z.number(),
      }),
    }),
    handler: async ({ userId }) => {
      const user = await getUser(userId);
      const stats = await getStats(userId);
      return { name: user.name, stats };
    },
    ui: { html: './src/views/dashboard.html' },
  }),
}

View Options

OptionTypeDescription
descriptionstringWhat the AI sees when listing tools
inputZodSchemaParameters the view accepts
dataZodSchemaData returned by the handler
handlerfunctionServer-side data fetching
uiobjectPath to HTML or React component
visibilitystringWho can invoke: 'both', 'model', or 'app'

Visibility Options

  • both (default): AI can invoke, users can navigate directly
  • model: Only AI can invoke (hidden from app navigation)
  • app: Only visible in app navigation (AI cannot invoke)

Actions

Actions are write operations. They handle mutations, submissions, and side effects. Unlike views, they don't render UI.

import { defineAction } from '@pancake-apps/server';
import { z } from 'zod';

actions: {
  updateProfile: defineAction({
    description: 'Update user profile information',
    input: z.object({
      name: z.string(),
      email: z.string().email(),
    }),
    output: z.object({
      success: z.boolean(),
      updatedAt: z.string(),
    }),
    handler: async ({ name, email }) => {
      const result = await updateUser({ name, email });
      return { success: true, updatedAt: result.updatedAt };
    },
  }),
}

Action Options

OptionTypeDescription
descriptionstringWhat the AI sees
inputZodSchemaRequired input parameters
outputZodSchemaWhat the handler returns
handlerfunctionThe mutation logic

Calling Actions from Views

In React:

import { useAction } from '@pancake-apps/web';

function ProfileForm() {
  const { dispatch } = useAction();

  const handleSubmit = async (data) => {
    const result = await dispatch('updateProfile', data);
    if (result.success) {
      // Show success message
    }
  };
}

In vanilla HTML:

const result = await pancake.unified.action('updateProfile', {
  name: 'John',
  email: 'john@example.com',
});

Data

Data endpoints are read operations. They query, search, and fetch information without modifying anything.

import { defineData } from '@pancake-apps/server';
import { z } from 'zod';

data: {
  searchProducts: defineData({
    description: 'Search products by name or category',
    input: z.object({
      query: z.string(),
      category: z.string().optional(),
      limit: z.number().default(10),
    }),
    output: z.array(z.object({
      id: z.string(),
      name: z.string(),
      price: z.number(),
    })),
    handler: async ({ query, category, limit }) => {
      return searchProducts({ query, category, limit });
    },
  }),
}

Data Options

OptionTypeDescription
descriptionstringWhat the AI sees
inputZodSchemaQuery parameters
outputZodSchemaShape of returned data
handlerfunctionThe query logic

Fetching Data from Views

In React:

import { useData } from '@pancake-apps/web';

function ProductSearch() {
  const { getData } = useData();
  const [products, setProducts] = useState([]);

  const search = async (query) => {
    const results = await getData('searchProducts', { query });
    setProducts(results);
  };
}

In vanilla HTML:

const products = await pancake.unified.data('searchProducts', {
  query: 'laptop',
  limit: 20,
});

When to Use Each

ScenarioUse
Display data with interactive UIView
Form submissionAction
Delete or update operationAction
Fetch data for displayData
Search or filterData
Background mutationAction

Combining Them

A typical app uses all three together. Here's an example e-commerce flow:

const app = createApp({
  name: 'shop',
  version: '1.0.0',

  // Views for displaying UI
  views: {
    productList: defineView({
      description: 'Browse products',
      handler: async () => fetchFeaturedProducts(),
      ui: { html: './src/views/products.html' },
    }),

    cart: defineView({
      description: 'View shopping cart',
      handler: async ({ userId }) => getCart(userId),
      ui: { html: './src/views/cart.html' },
    }),
  },

  // Actions for mutations
  actions: {
    addToCart: defineAction({
      description: 'Add product to cart',
      input: z.object({ productId: z.string(), quantity: z.number() }),
      output: z.object({ cartTotal: z.number() }),
      handler: async ({ productId, quantity }) => {
        return addToCart(productId, quantity);
      },
    }),

    checkout: defineAction({
      description: 'Complete purchase',
      input: z.object({ paymentMethod: z.string() }),
      output: z.object({ orderId: z.string() }),
      handler: async ({ paymentMethod }) => {
        return processCheckout(paymentMethod);
      },
    }),
  },

  // Data for queries
  data: {
    searchProducts: defineData({
      description: 'Search products',
      input: z.object({ query: z.string() }),
      output: z.array(ProductSchema),
      handler: async ({ query }) => searchProducts(query),
    }),
  },
});

The view calls the data endpoint for search results, and calls actions when the user adds items or checks out.

Best Practices

Views

  • Keep handlers focused on data fetching
  • Let the UI handle interaction logic
  • Use descriptive names that make sense to AI

Actions

  • One action per mutation
  • Return meaningful confirmation
  • Handle errors gracefully

Data

  • Keep queries simple and composable
  • Use pagination for large result sets
  • Cache when appropriate

Next Steps

On this page