소스 정보
- 저장소
- windmill-labs/windmill
- 최근 소스 활동
- 2026년 9월 29일 12:47
- 감지된 SKILL.md 언어
- 영어
- 스타
- 18,107
- 포크
- 1,111
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
SKILL.md 표시 중
SKILL.md
소스 지침 · 읽기 전용 미리보기- name
- raw-app
- description
- MUST use when creating raw apps.
# Windmill Raw Apps — CLI workflow
This guide covers raw apps from the terminal: scaffolding via `wmill app new`, the on-disk layout, and the file-based conventions the CLI uses to represent backend runnables and data table configuration. The platform shape (how a raw app behaves at runtime — frontend bundling, runnable types, datatable SDK calls) is covered in the companion authoring guide.
## Creating a Raw App
**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.
### Step 1 — Gather the three required values by asking the user
You need three things to run the command:
1. **summary** — a short description of the app
2. **path** — the windmill path, e.g. `f/folder/my_app` or `u/username/my_app`
3. **framework** — one of `react19` (recommended), `react18`, `svelte5`, `vue`
If 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:
- For `framework` — multiple-choice with the four allowed values; mark `react19` as `(Recommended)` and put it first.
- For `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.
### Step 2 — Run the command yourself
Once you have summary + path + framework, run it:
```bash
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.
### Optional flags
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. |
### Step 3 — Offer the visual preview
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.
### Anti-patterns to avoid
- ❌ Running `wmill app new` with no flags (the prompt will hang).
- ❌ Telling the user to "run `wmill app new` and follow the prompts" — that's a step backwards from what you can do directly.
- ❌ Inventing a path/summary/framework instead of asking the user.
- ❌ Defaulting to `react19` because the user didn't say — even sensible defaults must be confirmed.
- ❌ Passing `--overwrite` automatically when the directory exists — confirm with the user first.
### Interactive (only when a human is at the terminal)
```bash
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.
## On-disk app layout
```
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 on disk
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 (extension-driven)
| 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` |
After creating or editing a backend runnable — especially when its imports or arguments changed — its local lock and `wmill-lock.yaml` go stale. Offer to run `wmill generate-metadata` and run it once the user agrees (or automatically if the project's `AGENTS.md` opts into that) — YOU run it, don't just name it and wait. It writes local files only (not a deploy), and keeping the lock current avoids noise in git-sync/CI:
```bash
wmill generate-metadata
```
After it runs, check the regenerated `.lock` diff and tell the user which dependency versions changed (e.g. `requests 2.31.0 → 2.32.0`), so they can catch an unwanted bump before deploying.
### Optional YAML configuration
Add a `<id>.yaml` file alongside the code 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
```
## Data tables — `raw_app.yaml` config
The `data` block in `raw_app.yaml` controls which tables the app can query.
```yaml
data:
datatable: main # Default datatable
schema: app_schema # Schema the app's tables go in (optional); still write them as app_schema.<table>
tables:
- main/users # Table in public schema
- main/app_schema:items # Table in specific schema
roles: # Optional: the role the app uses each datatable through
main: analyst
```
**Table reference formats:**
- `<datatable>` — All tables in the datatable
- `<datatable>/<table>` — Specific table in public schema
- `<datatable>/<schema>:<table>` — Table in specific schema
**Roles:** when a datatable is under roles, its queries run as a role, which only reaches what it was granted. `roles` records the role the app uses each datatable through; the app's code must pass the same role: `wmill.datatable('main', { role: 'analyst' })` in TypeScript, `wmill.datatable('main', role='analyst')` in Python. A datatable without an entry is used as its default role.
## 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
```
A migration runs with no default schema, so a table outside `public` is created with its schema, `CREATE TABLE IF NOT EXISTS app_schema.items (...)`, listed as `main/app_schema:items`, and queried as `app_schema.items`.
### 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
Commands you run yourself, not the user:
- `wmill app new` — run it with flags, per the "Creating a Raw App" section above.
- `wmill app lint <app_folder>` — checks the app's structure and that it builds. Run it after editing, before offering a preview or a deploy; a bundle that compiles still says nothing about behavior, so a preview is what checks that.
- `wmill generate-metadata` — (re)generates local lock files and refreshes `wmill-lock.yaml` content hashes; writes local files only (not a deploy). After adding or editing a runnable, offer it and run it on agreement — or automatically if the project's `AGENTS.md` opts into that (see "After creating a runnable" above).
For the rest, tell the user which command fits their intent and let them run it — these deploy to the workspace, overwrite local files, or launch a long-running server, so 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 sync push` | Deploy app to Windmill |
| `wmill sync pull` | Pull latest from Windmill |
# Windmill Raw Apps
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
## App shape
A raw app has three logical parts:
- **Frontend** — bundled with esbuild from `index.tsx` as the entrypoint. Files include the entrypoint, components (`App.tsx`), styles, etc.
- **Backend runnables** — server-side scripts the frontend calls, each addressed by a unique key.
- **Data** — optional whitelisted datatables (managed PostgreSQL) that the backend runnables can query. The frontend never queries the database directly; backend runnables are the only bridge.
## Frontend
### Entrypoint
The entrypoint is `index.tsx` for React and `index.ts` for Svelte and Vue. It is both the bundling entrypoint (the bundler is esbuild) and the **mount** entrypoint: the preview executes the bundle against an empty `<div id="root">` and auto-renders nothing, so the entrypoint must mount a top-level `App` itself. Keep the UI in `App.tsx` / `App.svelte` / `App.vue` and keep the entrypoint as the mount shim.
React (`index.tsx`):
```tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte (`index.ts`): `mount(App, { target: document.getElementById('root')! })`. Vue (`index.ts`): `createApp(App).mount('#root')`.
**Never replace the entrypoint with a bare component** (`export default function App() { ... }` and no mount call). A component that is defined but never mounted renders a blank screen with **no error thrown** — it never executes, so nothing reaches the console or the error overlay. If an app renders blank, check that the entrypoint still mounts `App` into `#root`.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
### Generated bindings (`wmill.d.ts` / `wmill.ts`)
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.
### Calling backend runnables
Import the generated bindings and call the runnable like a function. `./wmill` is the **only** way the frontend reaches anything server-side — datatables, workspace items, external services. Never `fetch` the Windmill API from frontend code: the bundle holds no token and builds no API URL.
| Export | Resolves to | Use it for |
|---|---|---|
| `backend.<key>(args)` | the runnable's result | the default — run and wait |
| `backendAsync.<key>(args)` | the **job id** (a string) | long-running work you want to track |
| `waitJob(jobId)` | the job's **result** (rejects if the job failed) | awaiting a `backendAsync` job |
| `getJob(jobId)` | a `Job` (`{ type, success, result, duration_ms, ... }`) | polling status without blocking |
| `streamJob(jobId, onUpdate?)` | the final result, calling `onUpdate` per chunk | showing output as it is produced |
A runnable is always called with **one object** whose keys are its `main` parameters — `main(user_id: string, limit: number)` is called as `backend.get_users({ user_id, limit })`, never with positional arguments. A runnable without parameters is called with no argument. Resource and variable ids handed to the `wmill` client are paths (`u/<user>/<name>` or `f/<folder>/<name>`).
Run and wait — the common case:
```tsx
import { backend } from './wmill';
const user = await backend.get_user({ user_id: '123' });
```
Start a long job, then await it:
```tsx
import { backendAsync, waitJob } from './wmill';
const jobId = await backendAsync.run_report({ month: '2026-08' }); // a string
const report = await waitJob(jobId); // the result itself
```
Or poll it without blocking, to render progress:
```tsx
import { getJob } from './wmill';
GitHub에서 보기이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기