home / skills / windmill-labs / windmill / raw-app

This skill helps you scaffold and manage Windmill raw apps, enabling frontend backends and datatables with best practices.

npx playbooks add skill windmill-labs/windmill --skill raw-app

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.7 KB
---
name: raw-app
description: MUST use when creating raw apps.
---

# Windmill Raw Apps

Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.

## Creating a Raw App

```bash
wmill app new
```

This interactive command creates a complete app structure with your choice of frontend framework (React, Svelte, or Vue).

## App Structure

```
my_app.raw_app/
├── AGENTS.md              # AI agent instructions (auto-generated)
├── DATATABLES.md          # Database schemas (run 'wmill app generate-agents' to refresh)
├── raw_app.yaml           # App configuration (summary, path, data settings)
├── index.tsx              # Frontend entry point
├── App.tsx                # Main React/Svelte/Vue component
├── index.css              # Styles
├── package.json           # Frontend dependencies
├── wmill.ts               # Auto-generated backend type definitions (DO NOT EDIT)
├── backend/               # Backend runnables (server-side scripts)
│   ├── <id>.<ext>         # Code file (e.g., get_user.ts)
│   ├── <id>.yaml          # Optional: config for fields, or to reference existing scripts
│   └── <id>.lock          # Lock file (run 'wmill app generate-locks' to create)
└── sql_to_apply/          # SQL migrations (dev only, not synced)
    └── *.sql              # SQL files to apply via dev server
```

## Backend Runnables

Backend runnables are server-side scripts that your frontend can call. They live in the `backend/` folder.

### Creating a Backend Runnable

Add a code file to the `backend/` folder:

```
backend/<id>.<ext>
```

The runnable ID is the filename without extension. For example, `get_user.ts` creates a runnable with ID `get_user`.

### Supported Languages

| Language         | Extension    | Example          |
|------------------|--------------|------------------|
| TypeScript       | `.ts`        | `myFunc.ts`      |
| TypeScript (Bun) | `.bun.ts`    | `myFunc.bun.ts`  |
| TypeScript (Deno)| `.deno.ts`   | `myFunc.deno.ts` |
| Python           | `.py`        | `myFunc.py`      |
| Go               | `.go`        | `myFunc.go`      |
| Bash             | `.sh`        | `myFunc.sh`      |
| PowerShell       | `.ps1`       | `myFunc.ps1`     |
| PostgreSQL       | `.pg.sql`    | `myFunc.pg.sql`  |
| MySQL            | `.my.sql`    | `myFunc.my.sql`  |
| BigQuery         | `.bq.sql`    | `myFunc.bq.sql`  |
| Snowflake        | `.sf.sql`    | `myFunc.sf.sql`  |
| MS SQL           | `.ms.sql`    | `myFunc.ms.sql`  |
| GraphQL          | `.gql`       | `myFunc.gql`     |
| PHP              | `.php`       | `myFunc.php`     |
| Rust             | `.rs`        | `myFunc.rs`      |
| C#               | `.cs`        | `myFunc.cs`      |
| Java             | `.java`      | `myFunc.java`    |

### Example Backend Runnable

**backend/get_user.ts:**
```typescript
import * as wmill from 'windmill-client';

export async function main(user_id: string) {
  const sql = wmill.datatable();
  const user = await sql`SELECT * FROM users WHERE id = ${user_id}`.fetchOne();
  return user;
}
```

After creating, generate lock files:
```bash
wmill app generate-locks
```

### Optional YAML Configuration

Add a `<id>.yaml` file to configure fields or static values:

**backend/get_user.yaml:**
```yaml
type: inline
fields:
  user_id:
    type: static
    value: "default_user"
```

### Referencing Existing Scripts

To use an existing Windmill script instead of inline code:

**backend/existing_script.yaml:**
```yaml
type: script
path: f/my_folder/existing_script
```

For flows:
```yaml
type: flow
path: f/my_folder/my_flow
```

### Calling Backend from Frontend

Import from the auto-generated `wmill.ts`:

```typescript
import { backend } from './wmill';

// Call a backend runnable
const user = await backend.get_user({ user_id: '123' });
```

The `wmill.ts` file provides type-safe access to all backend runnables.

## Data Tables

Raw apps can query Windmill datatables (PostgreSQL databases managed by Windmill).

### Critical Rules

1. **ONLY USE WHITELISTED TABLES**: You can ONLY query tables listed in `raw_app.yaml` → `data.tables`. Tables not in this list are NOT accessible.

2. **ADD TABLES BEFORE USING**: To use a new table, first add it to `data.tables` in `raw_app.yaml`.

3. **USE CONFIGURED DATATABLE/SCHEMA**: Check the app's `raw_app.yaml` for the default datatable and schema.

### Configuration in raw_app.yaml

```yaml
data:
  datatable: main           # Default datatable
  schema: app_schema        # Default schema (optional)
  tables:
    - main/users            # Table in public schema
    - main/app_schema:items # Table in specific schema
```

**Table reference formats:**
- `<datatable>` - All tables in the datatable
- `<datatable>/<table>` - Specific table in public schema
- `<datatable>/<schema>:<table>` - Table in specific schema

### Querying in TypeScript (Bun/Deno)

```typescript
import * as wmill from 'windmill-client';

export async function main(user_id: string) {
  const sql = wmill.datatable();  // Or: wmill.datatable('other_datatable')

  // Parameterized queries (safe from SQL injection)
  const user = await sql`SELECT * FROM users WHERE id = ${user_id}`.fetchOne();
  const users = await sql`SELECT * FROM users WHERE active = ${true}`.fetch();

  // Insert/Update
  await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`;
  await sql`UPDATE users SET name = ${newName} WHERE id = ${user_id}`;

  return user;
}
```

### Querying in Python

```python
import wmill

def main(user_id: str):
    db = wmill.datatable()  # Or: wmill.datatable('other_datatable')

    # Use $1, $2, etc. for parameters
    user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
    users = db.query('SELECT * FROM users WHERE active = $1', True).fetch()

    # Insert/Update
    db.query('INSERT INTO users (name, email) VALUES ($1, $2)', name, email)
    db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id)

    return user
```

## SQL Migrations (sql_to_apply/)

The `sql_to_apply/` folder is for creating/modifying database tables during development.

### Workflow

1. Create `.sql` files in `sql_to_apply/`
2. Run `wmill app dev` - the dev server watches this folder
3. When SQL files change, a modal appears in the browser to confirm execution
4. After creating tables, **add them to `data.tables`** in `raw_app.yaml`

### Example Migration

**sql_to_apply/001_create_users.sql:**
```sql
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    name TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);
```

After applying, add to `raw_app.yaml`:
```yaml
data:
  tables:
    - main/users
```

### Migration Best Practices

- **Use idempotent SQL**: `CREATE TABLE IF NOT EXISTS`, etc.
- **Number files**: `001_`, `002_` for ordering
- **Always whitelist tables** after creation
- This folder is NOT synced - it's for local development only

## CLI Commands

| Command | Description |
|---------|-------------|
| `wmill app new` | Create a new raw app interactively |
| `wmill app dev` | Start dev server with live reload |
| `wmill app generate-agents` | Refresh AGENTS.md and DATATABLES.md |
| `wmill app generate-locks` | Generate lock files for backend runnables |
| `wmill sync push` | Deploy app to Windmill |
| `wmill sync pull` | Pull latest from Windmill |

## Best Practices

1. **Check DATATABLES.md** for existing tables before creating new ones
2. **Use parameterized queries** - never concatenate user input into SQL
3. **Keep runnables focused** - one function per file
4. **Use descriptive IDs** - `get_user.ts` not `a.ts`
5. **Always whitelist tables** - add to `data.tables` before querying
6. **Generate locks** - run `wmill app generate-locks` after adding/modifying backend runnables

Overview

This skill provides a template and tooling for building raw apps: custom frontends (React, Svelte, Vue) that call typed Windmill backend runnables and query whitelisted datatables. It scaffolds an app structure, manages backend runnables and SQL migrations, and generates type-safe bindings for frontend calls. Use it to turn scripts and queries into webhooks, workflows, and UIs with an efficient developer experience.

How this skill works

The skill scaffolds a raw_app folder with frontend entry points, backend runnables, a raw_app.yaml config, and an auto-generated wmill.ts type binding. Backend runnables live under backend/ and can be authored in many languages; the frontend calls them via the generated backend client. Datatables are accessed only if listed in raw_app.yaml, and local SQL migrations live in sql_to_apply/ for dev-time application.

When to use it

  • You need a custom UI that calls server-side logic with typed bindings.
  • You want to author backend runnables in TypeScript, Python, SQL, or other supported languages.
  • You must enforce a whitelist of database tables for frontend-accessible queries.
  • You want a fast workflow engine and easy local dev with SQL migrations and live reload.
  • You need to convert scripts or flows into callable webhooks or UI actions.

Best practices

  • Use the interactive wmill app new command to scaffold apps and pick a frontend framework.
  • Keep backend runnables focused: one runnable per file with descriptive IDs (e.g., get_user.ts).
  • Always add new tables to raw_app.yaml data.tables before querying them from runnables or the frontend.
  • Use parameterized queries to avoid SQL injection; prefer the provided query interpolation or $1-style parameters in Python.
  • Run wmill app generate-locks after adding or changing backend runnables to create/update lock files.

Example use cases

  • Build a React admin dashboard that calls backend runnables for CRUD operations on whitelisted Postgres tables.
  • Author a Svelte frontend that triggers Python runnables for data processing and returns results to the UI.
  • Create a Vue-powered form that calls TypeScript runnables which perform DB queries using the configured datatable.
  • Develop local SQL migrations in sql_to_apply/ during development and confirm execution via the dev server modal.
  • Expose existing Windmill scripts or flows by adding YAML references so the frontend calls them like inline runnables.

FAQ

How do I call a backend runnable from the frontend?

Import the generated backend client (wmill.ts) and call backend.<id> with the expected parameters, e.g., await backend.get_user({ user_id: '123' }).

Which tables can I query from a raw app?

Only tables explicitly listed in raw_app.yaml under data.tables are accessible; add new tables there before using them.

Where should I put SQL migrations?

Place development SQL files in sql_to_apply/ and run wmill app dev; confirm execution in the browser and then whitelist created tables in raw_app.yaml.