Skip to main content
Data apps are single-file HTML applications that run inside a Doc. They go beyond standard YAML dashboards by giving you full control over the UI: client-side filtering, interactive pivot tables, custom visualizations, and AI-powered natural language querying. A data app is stored as an HTML file in Drive and rendered in a sandboxed iframe within your Doc. Definite auto-injects a window.Definite bridge object so your HTML can query data, run Cube models, and call the AI assistant without any setup.

When to use data apps vs YAML dashboards

YAML DashboardData App
Standard charts, tables, KPIsCustom interactive UI (expandable rows, conditional formatting)
Multiple tiles in a grid layoutSingle full-screen experience
Cube/SQL datasets with built-in vizCustom JavaScript visualizations (D3, Perspective.js, ECharts)
Quick to build with FiRequires writing HTML/JavaScript
Use data apps when you need: client-side filtering and drill-down, multi-tab layouts, brush-selectable date histograms, AI-powered Q&A, or fully custom canvas visualizations. Use YAML dashboards when: standard charts, tables, and KPIs are sufficient. They’re faster to build, easier to maintain, and Fi can create them from natural language.

Creating a data app

Step 1: Write your HTML file

Create a single HTML file with your app logic. Use the Data Bridge API to query data from Definite. Include Tailwind CSS and Inter font for visual consistency.
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<body class="bg-gray-50 p-6 font-[Inter]">
  <div id="app"><div id="loading">Loading...</div></div>
  <script>
    async function init() {
      try {
        const rows = await Definite.query(
          "SELECT region, SUM(revenue) as rev FROM LAKE.PUBLIC.sales GROUP BY 1 LIMIT 1000"
        )
        document.getElementById("loading").remove()
        // Build your UI with rows
      } catch (err) {
        document.getElementById("loading").textContent = "Error: " + err.message
      }
    }
    init()
  </script>
</body>
Always include a loading state and error handling. Bridge queries are async and may take a moment to return.

Step 2: Upload to Drive

Upload your HTML file to the apps folder in Drive. You can do this via the MCP server:
  1. Call get_drive_upload_url(file_name="my-app.html", folder="apps") to get a signed upload URL
  2. Upload the file: curl -X PUT -T ./my-app.html "SIGNED_UPLOAD_URL"
The file is now available at apps/my-app.html in Drive.

Step 3: Create a Doc with an HTML tile

Create a Doc with a full-screen HTML tile pointing to your uploaded file. No datasets block is needed since the app fetches its own data through the bridge.
version: 1
schemaVersion: "2025-01"
kind: dashboard
metadata:
  name: "Sales Dashboard"
datasets: {}
layout:
  columns: 36
  tiles:
    - id: app
      x: 0
      y: 0
      w: 36
      h: 20
      type: html
      fullScreen: true
      driveFile: "apps/my-app.html"
Setting fullScreen: true removes all padding, borders, and tile headers so your HTML fills the entire page edge-to-edge.

Data Bridge API

Definite auto-injects a window.Definite object into every data app iframe. You do not need to define it yourself.
MethodReturnsUse for
Definite.query(sql)Promise<Array<object>>SQL queries returning JSON rows
Definite.queryArrow(sql)Promise<ArrayBuffer>SQL queries returning Arrow IPC (best performance for large datasets)
Definite.cubeQuery(query)Promise<Array<object>>Cube semantic model queries
Definite.fiFast(options)AI response objectNatural language to SQL, summarization

SQL queries

const rows = await Definite.query(
  "SELECT region, SUM(revenue) as rev FROM LAKE.PUBLIC.sales GROUP BY 1 LIMIT 1000"
)
// [{ region: "North America", rev: 50000 }, { region: "Europe", rev: 32000 }, ...]

Cube queries

const rows = await Definite.cubeQuery({
  measures: ["sales.revenue"],
  dimensions: ["sales.region"],
  timeDimensions: [{ dimension: "sales.order_date", granularity: "month", dateRange: "last 90 days" }],
  limit: 1000
})

Arrow queries (high performance)

For large datasets, use queryArrow to get data in Apache Arrow IPC format. This is significantly faster than JSON for datasets with 100K+ rows.
const buffer = await Definite.queryArrow(
  "SELECT * FROM LAKE.PUBLIC.transactions LIMIT 200000"
)
// Use with DuckDB WASM or Apache Arrow libraries

AI queries with fiFast

if (window.Definite?.fiFast) {
  const result = await Definite.fiFast({
    prompt: "What were our top 5 products by revenue last month?",
    model: "gemini-3.1-flash-lite-preview",
    system: "You are a DuckDB SQL expert. Always use the execute_sql tool.",
    temperature: 0.0,
    max_output_tokens: 1200,
  })
}
fiFast may not be available in all contexts (e.g., public embeds). Always check window.Definite?.fiFast before calling.

Client-side analytics with DuckDB WASM

For interactive filtering, pivoting, and drill-down without round-trips to the server, load your data into DuckDB WASM running in the browser. The pattern:
  1. Fetch data via Definite.queryArrow() (Arrow format for performance)
  2. Load it into a local DuckDB WASM instance
  3. Run fast client-side SQL for filters, KPIs, and aggregations
import * as arrow from 'https://storage.googleapis.com/definite-public/libs/apache-arrow@17.0.0/apache-arrow.esm.js';
import * as duckdb from 'https://storage.googleapis.com/definite-public/libs/duckdb-wasm@1.29.0/duckdb-wasm.esm.js';

// Fetch data from Definite
const buffer = await Definite.queryArrow("SELECT * FROM LAKE.PUBLIC.sales LIMIT 200000");

// Load into DuckDB WASM
const arrowTable = arrow.tableFromIPC(new Uint8Array(buffer));
await conn.insertArrowFromIPCStream(arrow.tableToIPC(arrowTable, 'stream'), { name: 'sales', create: true });

// Now query locally for instant results
const result = await conn.query("SELECT region, SUM(revenue) as rev FROM sales GROUP BY 1");
Version compatibility is critical. Use Apache Arrow 17.0.0 with DuckDB WASM 1.29.0. Mismatched versions cause silent failures.

Column naming

When creating tables in DuckDB WASM for use with Perspective.js, rename columns to camelCase and cast types explicitly:
CREATE TABLE txns AS SELECT
  transaction_id AS txnId,
  STRFTIME(created_at, '%Y-%m-%d') AS createdDate,
  amount::DOUBLE AS amount,
  ABS(amount)::DOUBLE AS absAmount
FROM raw_data

Visualization with Perspective.js

Perspective.js provides interactive pivot tables, charts, and data grids that work with DuckDB WASM. It’s ideal for data apps that need user-configurable visualizations. Supported chart types: Datagrid, Y Line, Y Area, Y Bar, X Bar, Y Scatter, Heatmap, Treemap, Sunburst.
import perspective from "https://cdn.jsdelivr.net/npm/@perspective-dev/client@4.3.0/dist/cdn/perspective.js";
import { DuckDBHandler } from "https://cdn.jsdelivr.net/npm/@perspective-dev/client@4.3.0/dist/esm/virtual_servers/duckdb.js";

// Bridge DuckDB WASM to Perspective
const handler = new DuckDBHandler(conn);
const pspClient = await perspective.worker(perspective.createMessageHandler(handler));
<perspective-viewer
  id="viewer"
  table="memory.txns"
  plugin="Datagrid"
  theme="Pro Dark">
</perspective-viewer>
Call viewer.notifyResize() after tab switches or layout changes. Perspective viewers go blank without it.

Theme support

Data apps support dark and light modes. Use CSS custom properties to match the Definite UI:
:root {
  --bg-primary: #09090b; --bg-card: #111113; --bg-elevated: #18181b;
  --border: #27272a; --border-hover: #3f3f46;
  --text-primary: #fafafa; --text-secondary: #a1a1aa; --text-muted: #71717a;
}
html.light {
  --bg-primary: #ffffff; --bg-card: #f9fafb; --bg-elevated: #f3f4f6;
  --border: #e5e7eb; --border-hover: #d1d5db;
  --text-primary: #111827; --text-secondary: #4b5563; --text-muted: #9ca3af;
}
Detect the theme from the URL parameter (Definite passes ?theme=light or ?theme=dark):
var theme = new URLSearchParams(location.search).get('theme');
if (theme === 'light') document.documentElement.classList.add('light');

Caching with IndexedDB

Cache Arrow data in IndexedDB for fast reloads. Users see cached data instantly while fresh data loads in the background.
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours

async function fetchData(sql) {
  // Check IndexedDB cache first
  const cached = await getCachedResult(sql);
  if (cached) return { buffer: cached, fromCache: true };

  // Fetch from Definite
  const buffer = await Definite.queryArrow(sql);

  // Store in cache for next time
  await setCachedResult(sql, buffer);
  return { buffer, fromCache: false };
}
Show a cache indicator so users know when they’re seeing cached vs live data. Include a “Clear cache and reload” button for transparency.

Full-screen mode

Set fullScreen: true on your HTML tile to remove all Doc chrome (padding, borders, tile headers, sidebar). The HTML fills the entire viewport edge-to-edge.
layout:
  columns: 36
  tiles:
    - id: app
      x: 0
      y: 0
      w: 36
      h: 20
      type: html
      fullScreen: true
      driveFile: "apps/my-app.html"
Full-screen tiles must be the only tile in the layout. The left sidebar auto-collapses when a full-screen tile is active.

Embedding and sharing

Data apps in Docs can be shared publicly using embed tokens. This allows anonymous access without authentication.
  1. Create a public embed token for your Doc
  2. Share the public URL: viewers see the data app without logging in
  3. Public mode uses cached query results only (no live query execution)
In public/embedded mode, only Cube queries are allowed. Direct SQL execution is disabled for security.

Next steps

Agent Reference: Data Apps

Programmatically create data apps via the MCP server or AI agents

Tile Types Reference

Configuration options for all tile types including HTML tiles