Agent Skills: Windmill Raw Apps

MUST use when creating raw apps.

UncategorizedID: windmill-labs/windmill/raw-app

Repository

windmill-labsLicense: NOASSERTION
16,266927

Install this agent skill to your local

pnpm dlx add-skill https://github.com/windmill-labs/windmill/tree/HEAD/system_prompts/auto-generated/skills/raw-app

Skill Files

Browse the full folder contents for raw-app.

Download Skill

Loading file tree…

system_prompts/auto-generated/skills/raw-app/SKILL.md

Skill Metadata

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

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 generate-metadata' to create/update)
└── 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:

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, tell the user they can generate lock files by running:

wmill generate-metadata

Optional YAML Configuration

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

backend/get_user.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:

type: script
path: f/my_folder/existing_script

For flows:

type: flow
path: f/my_folder/my_flow

Calling Backend from Frontend

Import from the auto-generated wmill.ts:

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.yamldata.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

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)

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

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:

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:

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

Tell the user they can run these commands (do NOT run them yourself):

| 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 generate-metadata | 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 - tell the user to run wmill generate-metadata after adding/modifying backend runnables