How this skill is triggered — by the user, by Claude, or both
Slash command
/hallow-windmill:raw-appThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
You — the AI agent — create the app yourself by running wmill app new with the right flags. Do NOT tell the user to "run wmill app new and follow the prompts" or wait for them to do it. The bare wmill app new is an interactive wizard that hangs waiting for stdin in any non-TTY context (which includes you). Always pass flags.
You need three things to run the command:
f/folder/my_app or u/username/my_appreact19 (recommended), react18, svelte5, vueIf the user's request did not supply every one of these explicitly, ask. Do not guess values, do not invent paths, do not pick a framework on the user's behalf, do not "just use react19 because it's the default".
Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group all missing fields into a single round-trip so the user answers them at once:
framework — multiple-choice with the four allowed values; mark react19 as (Recommended) and put it first.summary and path — provide one or two example values as multiple-choice options (the user can pick "Other" to type a free-form answer).Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.
Once you have summary + path + framework, run it:
wmill app new \
--summary "Customer dashboard" \
--path f/sales/dashboard \
--framework react19
That's the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of --summary/--path/--framework puts the command in non-interactive mode.
Layer these in only when the user asked for them:
| Flag | When to add it |
|---|---|
--datatable <name> | The user wants this app wired to a specific Windmill datatable. Without it, the app is created with no datatable. |
--schema <name> | Together with --datatable. Creates the schema with CREATE SCHEMA IF NOT EXISTS if it doesn't already exist. |
--overwrite | The target directory already exists and the user said it's OK to replace. Without it, non-interactive mode aborts with an error so you don't clobber existing work. |
--no-open-in-desktop | Already implied in non-interactive mode; only needed if you're somehow running interactively. |
After wmill app new and any initial edits to App.tsx / index.tsx, offer to open the visual preview as a one-sentence next step (e.g. "Want me to open the visual preview?"). Don't auto-open — opening the dev page has side effects (browser window, possibly a launch.json entry when an embedded preview tool is in play) the user should consent to.
For apps the preview command runs from the app folder (cd <app_path>__raw_app && wmill app dev …); the preview skill picks the proxy vs direct branch based on whether the runtime exposes a tool that can embed a localhost URL. If the user already asked to see/preview/visualize the app in their original request, skip the offer and just invoke the skill.
wmill app new with no flags (the prompt will hang).wmill app new and follow the prompts" — that's a step backwards from what you can do directly.react19 because the user didn't say — even sensible defaults must be confirmed.--overwrite automatically when the directory exists — confirm with the user first.wmill app new
This is the wizard. It only works when run by a human in a real terminal. Don't call it this way from an agent.
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 are server-side scripts that your frontend can call. They live in the backend/ folder.
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.
| 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 |
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
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"
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
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.
Raw apps can query Windmill datatables (PostgreSQL databases managed by Windmill).
ONLY USE WHITELISTED TABLES: You can ONLY query tables listed in raw_app.yaml → data.tables. Tables not in this list are NOT accessible.
ADD TABLES BEFORE USING: To use a new table, first add it to data.tables in raw_app.yaml.
USE CONFIGURED DATATABLE/SCHEMA: Check the app's raw_app.yaml for the default datatable and schema.
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 schemaimport * 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;
}
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
The sql_to_apply/ folder is for creating/modifying database tables during development.
.sql files in sql_to_apply/wmill app dev - the dev server watches this folderdata.tables in raw_app.yamlsql_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
CREATE TABLE IF NOT EXISTS, etc.001_, 002_ for orderingwmill app new is the exception: you run it yourself, with flags, per the "Creating a Raw App" section above.
For everything else, tell the user which command fits their intent and let them run it — these touch the workspace or local lock files, and the user should consent each time:
| Command | Description |
|---|---|
wmill app dev | Start dev server with live reload (see the preview skill for the full open-the-app-in-the-IDE-pane procedure). |
wmill app generate-agents | Refresh AGENTS.md and DATATABLES.md |
wmill generate-metadata | Generate lock files for backend runnables |
Deploying to Windmill: wmill sync push and wmill sync pull are banned at Hallow. Use the MCP windmill tools (e.g. mcp__windmill__createApp, mcp__windmill__updateApp) or the Windmill UI to mirror local changes to the server.
get_user.ts not a.tsdata.tables before queryingwmill generate-metadata after adding/modifying backend runnablesnpx claudepluginhub hallow-inc/claude-plugins --plugin hallow-windmillGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.