Skip to content

Code-Based Apps

Build applications with TypeScript/TSX files

Bifrost applications are React + Tailwind applications that run inside the Bifrost platform. Apps are built with TypeScript/TSX files organized into pages, layouts, and components.

Everything your app needs is available via a single import:

import { Button, Card, useState, useWorkflowQuery } from "bifrost";

External npm packages (declared in app.yaml) use standard imports:

import dayjs from "dayjs";
import { LineChart, Line } from "recharts";
app.yaml # Metadata (name, description, dependencies)
_layout.tsx # Root layout wrapper (required)
_providers.tsx # Optional context providers
styles.css # Custom CSS (dark mode via .dark selector)
pages/
index.tsx # Home page (/)
about.tsx # About page (/about)
clients/
index.tsx # Clients list (/clients)
[id].tsx # Client detail (/clients/:id)
components/
MyComponent.tsx # Shared components
modules/
utils.ts # Utility modules
  • Root files: _layout.tsx (required), _providers.tsx (optional), styles.css (optional), app.yaml
  • Pages: pages/*.tsx - automatically become routes
  • Components: components/*.tsx - reusable UI
  • Modules: modules/*.ts - utilities and helpers
  • Dynamic routes: [param].tsx syntax (e.g., [id].tsx)

Everything comes from a single "bifrost" import — React hooks, UI components, Bifrost hooks, icons, and utilities:

import {
useState, useEffect, useMemo, useCallback, useRef, useContext,
Button, Card, CardHeader, CardTitle, CardContent,
useWorkflowQuery, useUser, useNavigate,
Settings, ChevronRight,
cn, toast, format
} from "bifrost";

The primary way to interact with backend workflows.

useWorkflowQuery(workflowId, params?, options?)

Section titled “useWorkflowQuery(workflowId, params?, options?)”

Auto-executes on mount. Use for loading data.

// Load data on mount
const { data, isLoading } = useWorkflowQuery("workflow-uuid", { limit: 10 });
// Conditional loading -- only runs when id is truthy
const { data } = useWorkflowQuery("workflow-uuid", { id }, { enabled: !!id });
PropertyTypeDescription
dataT | nullResult data
isLoadingbooleanTrue while executing
isErrorbooleanTrue if failed
errorstring | nullError message
refetch() => Promise<T>Re-execute
logsStreamingLog[]Real-time logs

Options: { enabled?: boolean } — set false to defer execution.

Manual execution via execute(). Use for user-triggered actions.

const { execute, isLoading } = useWorkflowMutation("workflow-uuid");
const result = await execute({ name: "New Item" });
PropertyTypeDescription
execute(params?) => Promise<T>Run workflow
isLoadingbooleanTrue while executing
dataT | nullLast result
errorstring | nullError message
reset() => voidReset state

HookDescription
useUser()Current authenticated user { id, email, name }
useAppState(key, initialValue)Persistent cross-page state
useParams()URL path parameters
useSearchParams()Query string parameters
useNavigate()Programmatic navigation: navigate("/path")
useLocation()Current location object

Standard React hooks are available from the "bifrost" import:

useState, useEffect, useMemo, useCallback, useRef, useContext

  • Outlet - Renders child routes in layouts
  • Link - Navigation links

These are available from "bifrost" without installation. They are standard shadcn/ui components — use them exactly as documented in the shadcn/ui docs.

Layout: Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent

Forms: Button, Input, Label, Textarea, Checkbox, Switch, Select (+ SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectValue, SelectSeparator), RadioGroup, RadioGroupItem, Combobox, MultiCombobox, TagsInput

Display: Badge, Avatar (+ AvatarImage, AvatarFallback), Alert (+ AlertTitle, AlertDescription), Skeleton, Progress

Navigation: Tabs (+ TabsList, TabsTrigger, TabsContent), Pagination (+ PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious)

Feedback: Dialog (+ DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger), AlertDialog (+ sub-components), Tooltip (+ TooltipContent, TooltipProvider, TooltipTrigger), Popover (+ PopoverContent, PopoverTrigger, PopoverAnchor)

Data: Table (+ TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption)

Date: CalendarPicker, DateRangePicker

Icons: All lucide-react icons (e.g., Settings, ChevronRight, Search, Plus, Trash2, Users, Mail)

Utilities: cn(...) (Tailwind class merging), toast(message) (Sonner notifications), format(date, pattern) (date-fns)

Need a component not listed above? Build it in components/. shadcn/ui components are just TSX files — recreate any shadcn component, customize it, or build entirely new ones from scratch with React and Tailwind.

For example, to add a Sheet component, create components/Sheet.tsx using Radix primitives and Tailwind — the same pattern shadcn/ui uses. Or build a rich text editor, a kanban board, a color picker — anything you can build in React.


Add a styles.css file to your app root for custom styles:

/* CSS variables for theming */
:root {
--app-primary: oklch(0.5 0.18 260);
--app-surface: #fffef9;
}
/* Dark mode -- inherits from platform toggle */
.dark {
--app-primary: oklch(0.7 0.15 260);
--app-surface: #1e1e22;
}
/* Custom classes */
.paper-bg {
background-color: var(--app-surface);
background-image: repeating-linear-gradient(
transparent, transparent 1.7rem,
rgba(0,0,0,0.06) 1.7rem, rgba(0,0,0,0.06) 1.75rem
);
}

Use in components: <div className="paper-bg rounded-lg">. Tailwind classes and custom CSS classes can be mixed freely.


Declare npm packages in app.yaml:

name: My Dashboard
description: Analytics dashboard
dependencies:
recharts: "2.12"
dayjs: "1.11"

Max 20 packages. Loaded at runtime from esm.sh CDN.


Apps run inside the Bifrost shell (not in an iframe). Browser globals (window, document, fetch, ResizeObserver, MutationObserver, etc.) are accessible — use them directly when needed. External npm packages that depend on DOM APIs (rich text editors, drag-and-drop libraries, charting with DOM measurement) work normally.

Cannot use:

  • ES dynamic import() — all dependencies must be declared in app.yaml
  • Node.js APIs (fs, path, process, etc.)

Use useWorkflowQuery/useWorkflowMutation for calling backend workflows. Use fetch directly for external HTTP calls that don’t need backend logic.


The root _layout.tsx is required and must use <Outlet /> for routing.

Your app renders in a fixed-height container. The platform does not scroll the page for you — if a page needs scrolling, add overflow-auto to the element that should scroll.

import { Outlet } from "bifrost";
// _layout.tsx - CORRECT
export default function RootLayout() {
return (
<div className="h-full bg-background overflow-hidden">
<Outlet />
</div>
);
}
import {
useLocation, useNavigate, cn,
LayoutDashboard, Building2, Outlet
} from "bifrost";
const navItems = [
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
{ path: "/customers", label: "Customers", icon: Building2 },
];
export default function Layout() {
const location = useLocation();
const nav = useNavigate();
return (
<div className="flex h-full bg-background overflow-hidden">
<aside className="w-64 border-r bg-card flex flex-col shrink-0">
<nav className="flex-1 p-2">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<button
key={item.path}
onClick={() => nav(item.path)}
className={cn(
"w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm",
isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"
)}
>
<Icon className="h-4 w-4" />
{item.label}
</button>
);
})}
</nav>
</aside>
<main className="flex-1 min-w-0 min-h-0 flex flex-col p-6 overflow-hidden">
<Outlet />
</main>
</div>
);
}

A full page with workflow integration, loading states, and actions.

pages/index.tsx
import {
useState, useEffect,
useWorkflowQuery, useWorkflowMutation,
Card, CardHeader, CardTitle, CardContent,
Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
Button, Badge, Skeleton,
Building2, Loader2, RefreshCw, X,
cn
} from "bifrost";
interface Customer {
id: string;
name: string;
email: string;
status: "active" | "inactive";
}
export default function CustomersPage() {
const { data, isLoading, refetch } = useWorkflowQuery("ef8cf1f2-b451-47f4-aee8-336f7cb21d33");
const deleteMutation = useWorkflowMutation("abc12345-delete-workflow-id");
const [deleting, setDeleting] = useState<string | null>(null);
const customers: Customer[] = data?.customers || [];
const handleDelete = async (id: string) => {
setDeleting(id);
try {
await deleteMutation.execute({ customer_id: id });
refetch();
} finally {
setDeleting(null);
}
};
if (isLoading && customers.length === 0) {
return (
<div className="p-6">
<Skeleton className="h-9 w-48 mb-4" />
<Skeleton className="h-64 w-full" />
</div>
);
}
return (
<div className="flex flex-col h-full p-6 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between mb-6 shrink-0">
<div>
<h1 className="text-3xl font-bold">Customers</h1>
<p className="text-muted-foreground">Manage your customer list</p>
</div>
<Button
variant="outline"
onClick={() => refetch()}
disabled={isLoading}
>
{isLoading ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
)}
Refresh
</Button>
</div>
{/* Table */}
<Card className="flex flex-col min-h-0 flex-1">
<CardHeader className="shrink-0">
<CardTitle className="flex items-center gap-2">
<Building2 className="w-5 h-5" />
Customer List
</CardTitle>
</CardHeader>
<CardContent className="flex-1 min-h-0 overflow-auto">
{customers.length === 0 ? (
<div className="flex flex-col items-center py-12">
<Building2 className="w-12 h-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">No customers found</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{customers.map((customer) => (
<TableRow key={customer.id}>
<TableCell className="font-medium">{customer.name}</TableCell>
<TableCell>{customer.email}</TableCell>
<TableCell>
<Badge
variant={customer.status === "active" ? "default" : "secondary"}
>
{customer.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(customer.id)}
disabled={deleting === customer.id}
>
{deleting === customer.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<X className="w-4 h-4" />
)}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}

Use these MCP tools to build apps:

  1. create_app - Create app metadata (name, slug, description)
  2. list_workflows - Get workflow IDs you’ll need
  3. replace_content - Create/update files (entity_type: app_file)
  4. get_content - Read existing files
  5. publish_app - Publish when ready (only when user requests)
1. create_app (name, slug)
2. list_workflows → get IDs
3. replace_content → _layout.tsx
4. replace_content → pages/index.tsx
5. Preview at /apps/{slug}
6. publish_app (when user requests)