* feat(cli): improve agent prompts/skills and workspace fork workflow
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): refuse fork --from-branch rename of a base branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): auto-detect fork branch workflow, drop rt.d.ts refresh and legacy-name warning
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): reconcile raw-app generate-metadata stance (agent offers+runs)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): agent runs all CLI commands, gated on intent not on user typing them
Extends #9467's safe-vs-destructive model: the agent runs consequential commands (sync push, generate-metadata) itself too, gated on explicit user intent rather than handed to the user to type. The explicit-intent rule is the safeguard; an approval prompt is treated as a possible backstop, not assumed (auto-approve/headless runs have none).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Revert "docs(skills): agent runs all CLI commands, gated on intent not on user typing them"
Reverts 9225e1759b. That commit over-reached: #9467 already established the safe-vs-destructive split, and the targeted item-6 fix already removed the passive "tell the user they can run <safe next step>" phrasing. The blanket "agent runs everything" principle pushed deploys to be more eager and carried a wrong "permission layer prompts for approval" claim (untrue in auto-approve/headless mode). Keep deploys conservative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): default fork workspace name/id to the current branch when renaming it
When 'wmill workspace fork' converts the current working branch into the fork branch, default the fork's name and id to that branch (sanitized to a slug, since branch names can contain '/'). Interactive: the prompt is pre-filled (enter to accept); non-interactive (--yes): used automatically. Adds a unit test for the slug derivation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): address fork review — guard fork-branch rename, cap+validate fork id
Two P2s from review:
- --from-branch refused when the current branch is already a fork branch (would detach the existing fork by renaming its branch).
- fork id slug capped to 42 chars (backend max 50 incl. wm-fork- prefix); auto-derived id is slugged; full id validated client-side before existsWorkspace/datatable cloning so an invalid id fails fast instead of leaving cloned Postgres databases behind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.9 KiB
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:
- summary — a short description of the app
- path — the windmill path, e.g.
f/folder/my_apporu/username/my_app - 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; markreact19as(Recommended)and put it first. - For
summaryandpath— 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:
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 newwith no flags (the prompt will hang). - ❌ Telling the user to "run
wmill app newand 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
react19because the user didn't say — even sensible defaults must be confirmed. - ❌ Passing
--overwriteautomatically when the directory exists — confirm with the user first.
Interactive (only when a human is at the terminal)
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 a runnable, offer to generate its lock files as a one-sentence next step (e.g. "Want me to generate the lock files?") and run it yourself once they agree — don't just name the command and wait. If the user already asked you to finish/lock the app, run it directly. It writes local lock files (not a deploy), so offer rather than running silently:
wmill generate-metadata
Optional YAML configuration
Add a <id>.yaml file alongside the code 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
Data tables — raw_app.yaml config
The data block in raw_app.yaml controls which tables the app can query.
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
SQL Migrations (sql_to_apply/)
The sql_to_apply/ folder is for creating/modifying database tables during development.
Workflow
- Create
.sqlfiles insql_to_apply/ - Run
wmill app dev— the dev server watches this folder - When SQL files change, a modal appears in the browser to confirm execution
- After creating tables, add them to
data.tablesinraw_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
Two commands you run yourself, not the user:
wmill app new— run it with flags, per the "Creating a Raw App" section above.wmill generate-metadata— generates local lock files; offer it and run it on consent, per "After creating a runnable" above (it writes local lock files, not a deploy).
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 |