Skip to content

Building Internal Tools

Flow-Like’s A2UI (Agent-to-UI) system lets you build rich internal tools—dashboards, admin panels, forms, and data viewers—without writing frontend code. Design visually, connect to your workflows, and deploy instantly.

Tool TypeUse Cases
DashboardsKPI displays, real-time metrics, system status
Admin PanelsUser management, content moderation, settings
Data ViewersSearch interfaces, record browsers, log viewers
FormsData entry, surveys, approval workflows
ReportsScheduled reports, export tools, analytics
Control CentersTrigger workflows, manage processes, monitor jobs

Every app can have multiple Pages, each with a unique route:

App: Customer Portal
├── /dashboard → Overview Page
├── /customers → Customer List
├── /customers/:id → Customer Detail
├── /reports → Reports Page
└── /settings → Settings Page

Navigate between pages programmatically or via Link components.

A2UI provides 50+ components for building interfaces:

ComponentPurpose
RowHorizontal flex container
ColumnVertical flex container
GridCSS Grid layout
CardContent container with borders
TabsTabbed navigation
AccordionCollapsible sections
ModalPopup dialogs
DrawerSide panels
ComponentPurpose
TableFull-featured data tables with sorting, filtering, pagination
NivoChart25+ chart types (bar, line, pie, heatmap, etc.)
PlotlyChartAdvanced scientific charts
TextTypography (headings, body, labels)
BadgeStatus indicators
ProgressProgress bars
AvatarUser images
MarkdownRich text display
ComponentPurpose
TextFieldText input (text, email, password, number)
SelectDropdown selection
CheckboxBoolean toggle
SwitchToggle switch
RadioGroupSingle selection from options
SliderRange selection
DateTimeInputDate/time picker
FileInputFile upload
ComponentPurpose
ButtonClickable actions
LinkNavigation links
TooltipHover information
PopoverClick-triggered info

Components connect to data through bindings:

Table Component
├── data ◀── Variable: customers
├── columns ◀── [name, email, status, actions]
└── onRowClick ──▶ Navigate to /customers/{id}

Binding Types:

  • Literal – Static values: "Hello World"
  • Variable – Dynamic: $customers
  • Path – Nested access: $customer.orders[0].total
  • Template – Interpolation: "Welcome, {$user.name}!"

Components trigger workflows through Actions:

Action TypePurpose
invokeRun a workflow (Quick Action)
navigateGo to another page
updateDataUpdate a variable
openModalShow a dialog
closeModalHide a dialog
Button: "Submit Order"
├── onClick: invoke → ProcessOrder workflow
│ └── payload: { customer_id, items, total }
└── Loading state while workflow runs
  1. Open your App in the Studio
  2. Navigate to Pages
  3. Click Add Page
  4. Set route: /dashboard
  5. Choose layout: Grid (2 columns)

Drag Card components for each metric:

┌─────────────────┐ ┌─────────────────┐
│ Total Revenue │ │ Active Users │
│ $45,230 │ │ 1,234 │
│ ↑ 12% MTD │ │ ↑ 5% today │
└─────────────────┘ └─────────────────┘

Card Configuration:

Card: Revenue
├── Text (h2): "Total Revenue"
├── Text (h1): {$metrics.revenue}
├── Text (caption): "↑ {$metrics.revenue_change}% MTD"
└── Style: bg-green-50

Add a NivoChart for trends:

NivoChart
├── type: "line"
├── data: {$salesTrend}
├── colors: ["#3b82f6", "#10b981"]
├── enableGridX: false
└── legends: bottom

Add a Table for recent activity:

Table: Recent Orders
├── data: {$recentOrders}
├── columns:
│ ├── id (sortable)
│ ├── customer
│ ├── amount (format: currency)
│ ├── status (badge)
│ └── actions (buttons)
├── pagination: true
├── pageSize: 10
└── onRowClick: navigate → /orders/{id}

Create a workflow to fetch dashboard data:

Board: DashboardData
└── Init Event (runs on page load)
├──▶ SQL Query: Get Metrics
│ │
│ ▼
│ Set Variable: metrics
├──▶ SQL Query: Get Sales Trend
│ │
│ ▼
│ Set Variable: salesTrend
└──▶ SQL Query: Get Recent Orders
Set Variable: recentOrders
Column (gap: 16px)
├── Text (h2): "Create Customer"
├── TextField: name (required)
├── TextField: email (type: email, required)
├── Select: tier (options: Free, Pro, Enterprise)
├── Checkbox: newsletter
├── Row
│ ├── Button: "Cancel" (variant: outline)
│ └── Button: "Create" (variant: default)
└── Text: {$error} (color: red, hidden if empty)

Button: Create

onClick: invoke → CreateCustomer
├── payload:
│ ├── name: {$form.name}
│ ├── email: {$form.email}
│ ├── tier: {$form.tier}
│ └── newsletter: {$form.newsletter}
└── onSuccess: navigate → /customers/{result.id}

Workflow: CreateCustomer

Quick Action Event (name, email, tier, newsletter)
Validate Email Format
├── Invalid ──▶ Set error variable ──▶ Return
SQL Insert: customers table
Return: { id, success: true }

Client-side validation via component properties:

TextField: email
├── type: "email"
├── required: true
├── placeholder: "[email protected]"
├── error: {$emailError}
└── onChange: validate email format

Server-side validation in the workflow before database insert.

The Table component is powerful for data-heavy tools:

Table
├── columns:
│ ├── name (sortable: true)
│ ├── created (sortable: true, default: desc)
│ └── status
└── onSort: refetch with new order
Row (above table)
├── TextField: search (onDebounce: filter)
├── Select: status filter
└── DateTimeInput: date range
Table
├── data: {$filteredData}
└── columns: ...
Column: Actions
└── Row
├── Button (icon: edit) → openModal: EditDialog
├── Button (icon: trash) → invoke: DeleteRecord
└── Button (icon: eye) → navigate: /records/{id}
Button: "Export CSV"
├── onClick: invoke → ExportData
└── Downloads CSV file

A2UI includes 25+ chart types via Nivo:

ChartBest For
barCategorical comparisons
lineTrends over time
piePart-to-whole
radarMulti-variable comparison
heatmap2D data density
scatterCorrelation
funnelConversion flows
treemapHierarchical data
sankeyFlow diagrams
calendarDate-based heatmaps
bulletProgress vs targets
radialBarCircular progress
NivoChart
├── type: "bar"
├── data: [
│ { region: "North", sales: 45000 },
│ { region: "South", sales: 32000 },
│ { region: "East", sales: 28000 },
│ { region: "West", sales: 51000 }
│ ]
├── keys: ["sales"]
├── indexBy: "region"
├── colors: { scheme: "blues" }
└── legends: [{ position: "bottom" }]

Local to the current page, resets on navigation:

Set Page State (key: "filterValue", value: "active")
Get Page State (key: "filterValue") ──▶ "active"

Persists across pages (stored in IndexedDB):

Set Global State (key: "user", value: { id, name, role })
Get Global State (key: "user") ──▶ { id, name, role }

Board-level state for workflow data:

Variables:
├── customers: Array<Customer>
├── selectedCustomer: Customer | null
├── isLoading: Boolean
└── error: String | null

A2UI uses Tailwind CSS classes for responsive layouts:

Grid
├── columns: 1 (mobile)
├── md:columns: 2 (tablet)
├── lg:columns: 3 (desktop)
└── gap: 16px

Breakpoints:

  • sm: 640px
  • md: 768px
  • lg: 1024px
  • xl: 1280px
  • 2xl: 1536px

Create reusable UI components as Widgets:

Widget: CustomerCard
├── Props:
│ ├── customer: Customer
│ └── onEdit: Action
├── Content:
│ └── Card with customer info
└── Actions:
└── Edit button triggers onEdit

Use in pages:

WidgetInstance
├── widgetId: "CustomerCard"
├── props: { customer: {$selectedCustomer} }
└── onEdit: openModal → EditCustomerModal

Complete admin panel structure:

App: Admin Panel
├── /
│ └── Dashboard (metrics, charts, recent activity)
├── /users
│ ├── Table of users
│ ├── Search/filter bar
│ └── Actions: Edit, Suspend, Delete
├── /users/:id
│ ├── User details
│ ├── Activity log
│ └── Edit form
├── /content
│ ├── Content list
│ └── Moderation queue
├── /settings
│ ├── App settings form
│ └── API keys management
└── Layout:
├── Sidebar (navigation)
├── Header (user menu, notifications)
└── Main content area

Always show loading indicators:

{$isLoading ? Spinner : Table}

Display errors clearly:

{$error && Alert (variant: destructive): $error}

Handle empty data gracefully:

{$data.length === 0 ? EmptyState : Table}

Confirm destructive actions:

Button: Delete
├── onClick: openModal → ConfirmDelete
└── Modal confirms then invokes DeleteRecord

Use proper tab order and focus management for accessibility.