feat: default new raw apps to react 19 and ask for the data setup (#11586)

* feat: default new raw apps to react 19 and ask for the data setup

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: say what app new --schema writes and soften the already-set-up wording

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: resolve bare init_app table names in the app schema, fix --schema help

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-07 12:52:01 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent f6fbf75916
commit 818ddd5d31
18 changed files with 407 additions and 60 deletions
+6
View File
@@ -164,6 +164,12 @@ the decrypted value, exactly as against a real backend. The chat's read path pas
Seed a recognizable secret (the existing fixture uses `sk_live_do_not_leak_me`) and
assert it via `valueExcludes` to catch a leak.
`runtime.userAnswers` scripts the user's side of `askUserQuestion` in a global case: one
answer per question, in the order they are asked, and a question past the last answer is
dismissed, so `userAnswers: []` dismisses every question. Without it the tool reports that
it cannot ask, and the model usually carries on with its own guess, which makes "asks
before creating anything" impossible to assert.
`toolExpect.toolCallArgs` entries support `sharedByAtLeast: <n>`: at least `n` recorded
calls to that tool must carry the same non-blank string in the field. Use it for calls that
have to share an identifier, like two test runs of one chat conversation.
@@ -123,6 +123,7 @@ export interface GlobalEvalOptions {
planMode?: boolean;
model?: string;
maxIterations?: number;
userAnswers?: string[];
provider?: AIProvider;
backend: WindmillBackendSettings;
workspaceRoot?: string;
@@ -220,6 +221,7 @@ export async function runGlobalEval(
onToolCall: options.runContext?.onToolCall,
options: {
maxIterations: options.maxIterations,
userAnswers: options.userAnswers,
model,
workspace: workspaceRoot,
provider: options.provider,
@@ -85,6 +85,7 @@ export async function runEval<THelpers, TOutput>(
const model = options.model ?? "gpt-4o";
const maxIterations = options.maxIterations ?? 20;
const pendingAnswers = options.userAnswers && [...options.userAnswers];
const workspace = options.workspace ?? "test-workspace";
const provider = toFrontendEvalProvider(options.provider);
@@ -138,6 +139,12 @@ export async function runEval<THelpers, TOutput>(
// edit the arguments, so a case can assert what the model proposed but never how
// it reacts to the user changing something.
requestRunArgs: async (_toolId, form) => form.args,
...(pendingAnswers && {
requestUserQuestion: async () => {
const answer = pendingAnswers.shift();
return answer === undefined ? undefined : [answer];
},
}),
onNewToken: (token: string) => {
if (shouldEmitMessageStart) {
onAssistantMessageStart?.();
@@ -16,6 +16,9 @@ export interface ToolCallDetail {
export interface EvalRunnerOptions {
backend: WindmillBackendSettings;
maxIterations?: number;
// Scripted answers to the model's questions, one per question in order; a question past
// the last one is dismissed. Unset, the run cannot be asked anything.
userAnswers?: string[];
model?: string;
workspace?: string;
provider?: AIProvider;
+106 -1
View File
@@ -198,22 +198,127 @@
- uses the requested placeholder value and description
- does not create a resource or deploy anything
# A new app that has to store data, with nothing said about where: the data setup is the
# user's call, so it is asked before anything is created. The question is dismissed here,
# which leaves nothing the model may build on.
- id: global-test7-ambiguous-app-asks-question
prompt: |-
Create a new raw app for triaging support tickets.
initial: ai_evals/fixtures/frontend/global/initial/datatable_orders_seed.json
runtime:
maxTurns: 4
maxTurns: 6
userAnswers: []
validate:
draftCountExactly: 0
toolExpect:
requiredToolsUsed:
- list_datatables
- askUserQuestion
forbiddenToolsUsed:
- init_app
- exec_datatable_sql
- write_app_file
- write_app_runnable
- deploy_workspace_item
- delete_workspace_item
toolCallArgs:
- tool: askUserQuestion
field: question
stringIncludesAnyOf:
- data table
- datatable
- schema
- tables
- store
skipJudge: true
# The same request with the question answered: the app is built on what the user chose.
- id: global-new-app-data-setup-answer-applied
prompt: |-
Create a new raw app for triaging support tickets.
initial: ai_evals/fixtures/frontend/global/initial/datatable_orders_seed.json
runtime:
maxTurns: 32
userAnswers:
- A new schema named helpdesk
- Create new tables
validate:
draftCountExactly: 1
requiredDrafts:
- type: app
valueIncludes:
- helpdesk
- react-dom/client
toolExpect:
requiredToolsUsed:
- askUserQuestion
- init_app
forbiddenToolsUsed:
- deploy_workspace_item
toolCallArgs:
- tool: init_app
field: framework
stringEqualsAnyOf:
- react19
- tool: exec_datatable_sql
field: sql
stringIncludesAnyOf:
- helpdesk
skipJudge: true
# No framework named, no app to follow, nothing to store: the modal's recommended
# framework is used without a question.
- id: global-new-app-defaults-to-react19
prompt: |-
Build me a little app that converts temperatures between Celsius and Fahrenheit.
runtime:
maxTurns: 12
validate:
draftCountExactly: 1
requiredDrafts:
- type: app
valueIncludes:
- react-dom/client
toolExpect:
requiredToolsUsed:
- init_app
forbiddenToolsUsed:
- askUserQuestion
- exec_datatable_sql
- deploy_workspace_item
toolCallArgs:
- tool: init_app
field: framework
stringEqualsAnyOf:
- react19
skipJudge: true
# The user said where the data goes, so there is nothing left to ask.
- id: global-new-app-storage-specified-no-question
prompt: |-
Build a todo list app in React. Keep the todos in the main data table, in a new
schema called todo_app.
initial: ai_evals/fixtures/frontend/global/initial/datatable_orders_seed.json
runtime:
maxTurns: 20
validate:
draftCountExactly: 1
requiredDrafts:
- type: app
valueIncludes:
- todo_app
toolExpect:
requiredToolsUsed:
- init_app
- write_app_runnable
forbiddenToolsUsed:
- askUserQuestion
- deploy_workspace_item
toolCallArgs:
- tool: exec_datatable_sql
field: sql
stringIncludesAnyOf:
- todo_app
skipJudge: true
- id: global-test8-human-script-infer-path-language
+4
View File
@@ -29,6 +29,10 @@ export interface EvalCaseRuntimeAppContextSpec {
export interface EvalCaseRuntimeSpec {
maxTurns?: number;
// Global mode: scripted answers to askUserQuestion, one per question in order; a question
// past the last one is dismissed, so `[]` dismisses them all. Unset, the question tool
// reports that it cannot ask.
userAnswers?: string[];
backendPreview?: EvalCaseRuntimeBackendPreview;
appContext?: EvalCaseRuntimeAppContextSpec;
// Global mode: run as a session chat (preview tools + session prompt) vs the standalone chat.
+1
View File
@@ -54,6 +54,7 @@ export function createGlobalModeRunner(
sessionChat: context.evalCase?.runtime?.sessionChat,
planMode: context.evalCase?.runtime?.planMode,
maxIterations: context.evalCase?.runtime?.maxTurns,
userAnswers: context.evalCase?.runtime?.userAnswers,
provider: modelConfig.provider,
model: modelConfig.model,
backend: backendSettings,
+1 -1
View File
@@ -908,7 +908,7 @@ const command = new Command()
)
.option(
"--schema <schema:string>",
"Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist."
"Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it."
)
.option(
"--overwrite",
+1 -1
View File
@@ -90,7 +90,7 @@ A data pipeline is NOT a flow: it is a set of independent scripts in one folder,
## Raw App Development
You MUST use the \`raw-app\` skill to create or modify raw apps.
When a new app needs to be created, YOU run \`wmill app new\` yourself with \`--summary\`, \`--path\`, and \`--framework\` flags (and any other relevant flags). Do NOT ask the user to run it. If you don't have the values for those flags, ask the user via \`AskUserQuestion\` (one call, all missing fields) — never invent them. See the \`raw-app\` skill for the full procedure.
When a new app needs to be created, YOU run \`wmill app new\` yourself with \`--summary\`, \`--path\`, and \`--framework\` flags (and any other relevant flags). Do NOT ask the user to run it. If you don't have a summary or a path, ask the user via \`AskUserQuestion\` (one call, all missing fields) — never invent them. The framework is not asked: \`react19\` unless the user or the project's existing apps say otherwise. An app that stores data also needs its data table and schema settled with the user first. See the \`raw-app\` skill for the full procedure.
## Triggers
+55 -16
View File
@@ -6287,47 +6287,53 @@ This guide covers raw apps from the terminal: scaffolding via \`wmill app new\`,
**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
### Step 1 — Settle the setup
You need three things to run the command:
You need three values 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".
**summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
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**: never ask. Use the one the user named, otherwise the one the project's existing apps use, otherwise \`react19\` (see "Starting a new app" in the authoring guide).
**data setup**: when the app has to store data and the user did not say where, ask which data table, which schema and which tables, as "Data setup" in the authoring guide lays out. \`wmill datatable list\` gives the data tables to offer.
Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group everything missing into a single round-trip so the user answers 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).
- For the data setup — the choices the authoring guide lists, the default first.
Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.
Only proceed once every value is concrete. 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:
Once the setup is settled, run it:
\`\`\`bash
wmill app new \\
--summary "Customer dashboard" \\
--path f/sales/dashboard \\
--framework react19
--framework react19 \\
--datatable main \\
--schema app1
\`\`\`
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.
\`--summary\`, \`--path\` and \`--framework\` are the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of them puts the command in non-interactive mode, so the data setup only reaches the app through the flags below.
### 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. |
| \`--datatable <name>\` | The app stores data: the data table settled in step 1. Without it, the app is created with no datatable. |
| \`--schema <name>\` | Together with \`--datatable\`, when the app's tables go in a schema, new or existing. If the schema doesn't exist yet, writes a \`sql_to_apply/\` migration with \`CREATE SCHEMA IF NOT EXISTS\`, applied like any other migration (see "SQL Migrations" below). Leave it out for \`public\`. |
| \`--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. |
Existing tables the app reuses have no flag: list them under \`data.tables\` in \`raw_app.yaml\` (see "Data tables" below).
### 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.
@@ -6338,8 +6344,9 @@ For apps the preview command runs from the app folder (\`cd <app_path>__raw_app
- ❌ 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.
- ❌ Inventing a path or summary instead of asking the user.
- ❌ Asking which framework to use — \`react19\` is the default when nothing says otherwise.
- ❌ Creating a schema or tables for a new app before the user has said where its data goes.
- ❌ Passing \`--overwrite\` automatically when the directory exists — confirm with the user first.
### Interactive (only when a human is at the terminal)
@@ -6526,6 +6533,38 @@ A raw app has three logical parts:
- **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.
## Starting a new app
A new app gets the setup Windmill's own new-app dialog gives one: a framework, then where its data lives.
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
### Data setup
When a new app has to store data and the user did not say where, **ask before creating anything**: no app, no schema, no table. Where the data lives is the user's decision, and tables created in the wrong place stay there. Look up the workspace's data tables first so every choice you offer exists, then ask, as proposed answers rather than open questions:
1. **Data table**: which one to use, the default first (\`main\` when it exists, otherwise the first listed). Skip this one when the workspace has a single data table.
2. **Schema**: where the app's new tables go.
- A **new schema**, the usual choice, offered first. Suggest the first unused name among \`app1\`, \`app2\`, …
- An **existing schema** of that data table, by name.
- **None**: the tables go in \`public\`.
3. **Tables**: only when that data table already holds tables the app could use, whether to **reuse** them (name them) or **create new ones**.
Do not ask when:
- the user already said where the data goes. A part they left out takes its default above: the default data table, a new \`appN\` schema, new tables;
- the app, or its saved draft, already carries a data config (\`data.datatable\`, \`data.schema\` or \`data.tables\`). That config is the answer;
- the app stores nothing.
With no data table in the workspace there is nothing to choose: say that one has to be configured in the workspace settings before the app can store data.
A question left unanswered or dismissed is not a go-ahead for the defaults: stop there and say what is still needed.
Then build what was chosen: the data table and schema become the app's \`data.datatable\` and \`data.schema\`, the tables it uses, reused or new, go in \`data.tables\`, and a new schema is created (\`CREATE SCHEMA IF NOT EXISTS\`) before the tables that go in it.
\`wmill app new --datatable <name> --schema <name>\` records both and, for a schema that does not exist yet, writes the migration in \`sql_to_apply/\` that creates it: nothing exists in the database until \`wmill app dev\` applies that migration, so the app's table migrations come after it.
## Frontend
### Entrypoint
@@ -7980,7 +8019,7 @@ app related commands
- \`--path <path:string>\` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- \`--datatable <datatable:string>\` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- \`--schema <schema:string>\` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
- \`--schema <schema:string>\` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- \`--overwrite\` - Overwrite the target directory if it already exists, without prompting.
- \`--no-open-in-desktop\` - Do not prompt to open the new app in Claude Desktop.
- \`app generate-agents [app_folder:string]\` - regenerate AGENTS.md and DATATABLES.md from remote workspace
@@ -2607,6 +2607,25 @@ describe('global AI tools', () => {
expect(raw).not.toContain('function New')
})
it('records the data setup a new app is initialized with', async () => {
await callGlobalTool('init_app', {
path: 'u/admin/tickets',
framework: 'react19',
data: {
datatable: 'main',
schema: 'helpdesk',
tables: ['helpdesk:tickets', 'notes', 'main/customers']
}
})
const draft = getBackendDraft('raw_app', 'u/admin/tickets', { workspace: WORKSPACE })
expect(draft?.data).toEqual({
datatable: 'main',
schema: 'helpdesk',
tables: ['main/helpdesk:tickets', 'main/helpdesk:notes', 'main/customers']
})
})
it('discards a draft without deleting the workspace item', async () => {
await callGlobalTool('write_script', {
path: 'f/scripts/discard-me',
@@ -68,7 +68,10 @@ import {
} from '$lib/components/job_args'
import { processSecretArgs } from '$lib/components/secretArgUtils'
import { PLAN_MODE_MESSAGES } from '../planModeMessages'
import { DEFAULT_DATA as DEFAULT_RAW_APP_DATA } from '$lib/components/raw_apps/dataTableRefUtils'
import {
DEFAULT_DATA as DEFAULT_RAW_APP_DATA,
formatDataTableRef
} from '$lib/components/raw_apps/dataTableRefUtils'
import { appSourceToDraftValue } from '$lib/components/raw_apps/rawAppDraftValue'
import type { RawAppDomQuery } from '$lib/components/raw_apps/rawAppDom'
import { dataUrlToImagePart, normalizeImageDataUrl, type AttachedImage } from '../imageUtils'
@@ -1294,7 +1297,22 @@ const initAppSchema = z.object({
framework: z
.enum(FRAMEWORK_KEYS)
.describe(
'Frontend framework template. Confirm with the user before calling — never default silently. react19 is recommended for new apps.'
'Frontend framework template: the one the user named or an app they point at uses, otherwise react19. Do not ask.'
),
data: z
.object({
datatable: z.string().describe('Data table the app stores its data in.'),
schema: z.string().optional().describe("Schema the app's new tables go in. Omit for public."),
tables: z
.array(z.string())
.optional()
.describe(
'Tables the app queries, reused or about to be created, each as `<datatable>/<table>` (public) or `<datatable>/<schema>:<table>`.'
)
})
.optional()
.describe(
'Data setup of an app that stores data, as the user gave or chose it. Omit for an app that stores nothing.'
)
})
@@ -1546,7 +1564,7 @@ Raw apps:
- A draft app is reachable by nobody; deploying is what exposes its backend runnables. deploy_workspace_item says so when the deploy widens who may open the app: anonymous means anyone with the URL, without logging in; guest means anyone the instance's identity provider authenticates, member of this workspace or not. Relay that in plain words and carry on. This is disclosure, not a gate: do not stop and ask for permission, and do not refuse the deploy. You cannot change who may open an app from chat; it is set on the app's deploy settings.
- Use write_app_file, patch_app_file, and delete_app_file for frontend files.
- Use write_app_runnable and delete_app_runnable for backend runnables.
- Use init_app only after confirming framework, path, and summary with the user.
- Use init_app for a new app, with framework react19 unless the user named one or points at an app to follow: do not ask about the framework, and write the summary and pick the path (path conventions above) yourself. When the app has to store data and the user did not say where, settle the data setup with askUserQuestion BEFORE init_app or any SQL (get_instructions subject "app" lists the questions, one askUserQuestion each), then pass the outcome as init_app's data.
- Use deploy_workspace_item after explicit user deploy intent; raw app deploy bundles JS/CSS before saving.`
)}
@@ -2416,7 +2434,7 @@ function getAppInstructions(language?: ScriptLang): string {
- Global mode edits raw app drafts only; it does not save or deploy unless the user explicitly asks to deploy.
- App drafts are addressed by workspace path. Follow the path conventions in the system prompt: default to \`u/<current-user>/<name>\` for bare names; only use \`f/<folder>/<name>\` when the folder is known to exist. The first write tool snapshots the workspace app onto the draft, and subsequent writes accumulate.
- To create a new app, use \`init_app\` with a path, optional summary, and a framework (\`react19\` / \`react18\` / \`svelte5\` / \`vue\`). Confirm framework + path + summary with the user before calling — do not silently default to \`react19\` even though it is the recommended choice. \`init_app\` errors if an app already exists at the path or a draft is already in flight; in that case, edit the existing one rather than re-initializing.
- To create a new app, use \`init_app\` with a path, optional summary, a framework (\`react19\` / \`react18\` / \`svelte5\` / \`vue\`) and, for an app that stores data, its \`data\` setup. "Starting a new app" below decides the framework and when the data setup is asked. Ask each of its questions with \`askUserQuestion\`, one question per call with the suggested answer first, before \`init_app\` and before any SQL. \`init_app\` only records \`data\`: a new schema is yours to create with the datatable SQL tool. \`init_app\` errors if an app already exists at the path or a draft is already in flight; in that case, edit the existing one rather than re-initializing.
- \`init_app\` seeds a starter inline runnable named \`a\` (bun, \`main(x: string) => string\`) so the React/Svelte demo button works on first render. Replace or remove it once you start building real backend runnables.
- Frontend file paths start with \`/\` (e.g. \`/index.tsx\`, \`/App.tsx\`, \`/styles.css\`). Use \`write_app_file\` / \`patch_app_file\` / \`delete_app_file\`.
- Backend inline runnables are addressed as \`backend/<key>/main.{ts|py}\` from the file tools, but you create or update them via \`write_app_runnable\` / \`delete_app_runnable\` (which take the runnable shape directly: \`{ name, type, inlineScript?, path?, staticInputs? }\`).
@@ -6511,16 +6529,31 @@ async function testRunFlowStepByPath(
)
}
/** The `data.tables` entry for a table named without its data table (`schema:table`,
* `schema.table` or `table`), which models write for a table of the app's own data table.
* A bare name is a table of the app's own schema. */
function appTableRef(datatable: string, schema: string | undefined, ref: string): string {
if (ref.includes('/')) return ref
const sep = ref.includes(':') ? ':' : '.'
const at = ref.indexOf(sep)
return formatDataTableRef(
at === -1
? { datatable, schema, table: ref }
: { datatable, schema: ref.slice(0, at), table: ref.slice(at + 1) }
)
}
async function initApp(
args: {
path: string
summary?: string
framework: FrameworkKey
data?: { datatable: string; schema?: string; tables?: string[] }
},
ctx: WriteDraftCtx
): Promise<string> {
const { workspace, toolId, toolCallbacks } = ctx
const { path, summary, framework } = args
const { path, summary, framework, data } = args
if (await getGlobalDraft(workspace, 'app', path)) {
throw new Error(
@@ -6541,13 +6574,20 @@ async function initApp(
const value: AppDraftValue = {
summary,
files: { ...template },
runnables: structuredClone(STARTER_RUNNABLES)
runnables: structuredClone(STARTER_RUNNABLES),
...(data && {
data: {
tables: (data.tables ?? []).map((ref) => appTableRef(data.datatable, data.schema, ref)),
datatable: data.datatable,
schema: data.schema
}
})
}
await recomputeAppPolicy(value)
const result = await saveAppDraft(workspace, path, value)
return finishAppDraftWrite(result, ctx, () => ({
content: `Saved app "${path}" draft (${framework})`,
message: `Initialized a per-user draft app "${path}" from the ${framework} template (saved server-side, not a deployed workspace item). The template is a demo tour with starter runnables ${Object.keys(STARTER_RUNNABLES).join(', ')}: replace its UI and delete the runnables the app does not need (delete_app_runnable). Use write_app_file / write_app_runnable to evolve it.`
message: `Initialized a per-user draft app "${path}" from the ${framework} template (saved server-side, not a deployed workspace item). The template is a demo tour with starter runnables ${Object.keys(STARTER_RUNNABLES).join(', ')}: replace its UI and delete the runnables the app does not need (delete_app_runnable). Use write_app_file / write_app_runnable to evolve it.${data?.schema ? ` Its data config names schema "${data.schema}" of data table "${data.datatable}" without creating it: if it does not exist yet, create it before the tables that go in it.` : ''}`
}))
}
@@ -8950,7 +8990,7 @@ export function prepareGlobalUserMessage(
content += `isLiveDraft: true\n`
if (activeEditor.isNew) {
content +=
'isNew: true — the user just started this item from the new-item builder; it holds the starter template and is the item to build. Edit it in place; do not create another one.\n'
'isNew: true — the user just started this item from the new-item builder; it holds the starter template and is the item to build. Edit it in place; do not create another one. For an app, its files are the framework it was started with and the `data` config read_workspace_item shows is the data setup it was started with: ask about neither.\n'
}
content += '\n'
}
@@ -43,7 +43,7 @@ app related commands
- `--path <path:string>` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- `--datatable <datatable:string>` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- `--schema <schema:string>` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
- `--schema <schema:string>` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- `--overwrite` - Overwrite the target directory if it already exists, without prompting.
- `--no-open-in-desktop` - Do not prompt to open the new app in Claude Desktop.
- `app generate-agents [app_folder:string]` - regenerate AGENTS.md and DATATABLES.md from remote workspace
+36 -1
View File
@@ -888,6 +888,41 @@ A raw app has three logical parts:
- **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.
## Starting a new app
A new app gets the setup Windmill's own new-app dialog gives one: a framework, then where its data lives.
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
### Data setup
When a new app has to store data and the user did not say where, **ask before creating anything**: no app, no schema, no table. Where the data lives is the user's decision, and tables created in the wrong place stay there. Look up the workspace's data tables first so every choice you offer exists, then ask, as proposed answers rather than open questions:
1. **Data table**: which one to use, the default first (\`main\` when it exists, otherwise the first listed). Skip this one when the workspace has a single data table.
2. **Schema**: where the app's new tables go.
- A **new schema**, the usual choice, offered first. Suggest the first unused name among \`app1\`, \`app2\`, …
- An **existing schema** of that data table, by name.
- **None**: the tables go in \`public\`.
3. **Tables**: only when that data table already holds tables the app could use, whether to **reuse** them (name them) or **create new ones**.
Do not ask when:
- the user already said where the data goes. A part they left out takes its default above: the default data table, a new \`appN\` schema, new tables;
- the app, or its saved draft, already carries a data config (\`data.datatable\`, \`data.schema\` or \`data.tables\`). That config is the answer;
- the app stores nothing.
With no data table in the workspace there is nothing to choose: say that one has to be configured in the workspace settings before the app can store data.
A question left unanswered or dismissed is not a go-ahead for the defaults: stop there and say what is still needed.
Then build what was chosen: the data table and schema become the app's \`data.datatable\` and \`data.schema\`, the tables it uses, reused or new, go in \`data.tables\`, and a new schema is created (\`CREATE SCHEMA IF NOT EXISTS\`) before the tables that go in it.
### An app that arrives already set up
An app the user just started from the new-app dialog is already set up: its files are the framework template it was started with, and a \`data\` config it carries (\`data.datatable\`, \`data.schema\` or \`data.tables\`) is the data setup it was started with. Build that app in place, with that data config, and in that framework unless the user now names another. Do not ask for either, and do not create a second app. Only when it carries no data config and has to store data is the data setup still to ask.
## Frontend
### Entrypoint
@@ -3479,7 +3514,7 @@ app related commands
- \`--path <path:string>\` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- \`--framework <framework:string>\` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- \`--datatable <datatable:string>\` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- \`--schema <schema:string>\` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
- \`--schema <schema:string>\` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- \`--overwrite\` - Overwrite the target directory if it already exists, without prompting.
- \`--no-open-in-desktop\` - Do not prompt to open the new app in Claude Desktop.
- \`app generate-agents [app_folder:string]\` - regenerate AGENTS.md and DATATABLES.md from remote workspace
@@ -48,7 +48,7 @@ app related commands
- `--path <path:string>` - App path (e.g., f/folder/my_app or u/username/my_app). Skips the prompt when provided. Triggers non-interactive mode.
- `--framework <framework:string>` - Framework template: react19 | react18 | svelte5 | vue. Skips the prompt when provided. Triggers non-interactive mode.
- `--datatable <datatable:string>` - Datatable to wire up. Without this flag in non-interactive mode, no datatable is configured.
- `--schema <schema:string>` - Schema to use with --datatable. Created (CREATE SCHEMA IF NOT EXISTS) if it doesn't already exist.
- `--schema <schema:string>` - Schema to use with --datatable. If it doesn't exist yet, a sql_to_apply/ migration (CREATE SCHEMA IF NOT EXISTS) is written for it.
- `--overwrite` - Overwrite the target directory if it already exists, without prompting.
- `--no-open-in-desktop` - Do not prompt to open the new app in Claude Desktop.
- `app generate-agents [app_folder:string]` - regenerate AGENTS.md and DATATABLES.md from remote workspace
@@ -11,47 +11,53 @@ This guide covers raw apps from the terminal: scaffolding via `wmill app new`, t
**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
### Step 1 — Settle the setup
You need three things to run the command:
You need three values 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".
**summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
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**: never ask. Use the one the user named, otherwise the one the project's existing apps use, otherwise `react19` (see "Starting a new app" in the authoring guide).
**data setup**: when the app has to store data and the user did not say where, ask which data table, which schema and which tables, as "Data setup" in the authoring guide lays out. `wmill datatable list` gives the data tables to offer.
Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group everything missing into a single round-trip so the user answers 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).
- For the data setup — the choices the authoring guide lists, the default first.
Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.
Only proceed once every value is concrete. 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:
Once the setup is settled, run it:
```bash
wmill app new \
--summary "Customer dashboard" \
--path f/sales/dashboard \
--framework react19
--framework react19 \
--datatable main \
--schema app1
```
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.
`--summary`, `--path` and `--framework` are the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of them puts the command in non-interactive mode, so the data setup only reaches the app through the flags below.
### 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. |
| `--datatable <name>` | The app stores data: the data table settled in step 1. Without it, the app is created with no datatable. |
| `--schema <name>` | Together with `--datatable`, when the app's tables go in a schema, new or existing. If the schema doesn't exist yet, writes a `sql_to_apply/` migration with `CREATE SCHEMA IF NOT EXISTS`, applied like any other migration (see "SQL Migrations" below). Leave it out for `public`. |
| `--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. |
Existing tables the app reuses have no flag: list them under `data.tables` in `raw_app.yaml` (see "Data tables" below).
### 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.
@@ -62,8 +68,9 @@ For apps the preview command runs from the app folder (`cd <app_path>__raw_app &
- ❌ 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.
- ❌ Inventing a path or summary instead of asking the user.
- ❌ Asking which framework to use — `react19` is the default when nothing says otherwise.
- ❌ Creating a schema or tables for a new app before the user has said where its data goes.
- ❌ Passing `--overwrite` automatically when the directory exists — confirm with the user first.
### Interactive (only when a human is at the terminal)
@@ -250,6 +257,38 @@ A raw app has three logical parts:
- **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.
## Starting a new app
A new app gets the setup Windmill's own new-app dialog gives one: a framework, then where its data lives.
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
### Data setup
When a new app has to store data and the user did not say where, **ask before creating anything**: no app, no schema, no table. Where the data lives is the user's decision, and tables created in the wrong place stay there. Look up the workspace's data tables first so every choice you offer exists, then ask, as proposed answers rather than open questions:
1. **Data table**: which one to use, the default first (`main` when it exists, otherwise the first listed). Skip this one when the workspace has a single data table.
2. **Schema**: where the app's new tables go.
- A **new schema**, the usual choice, offered first. Suggest the first unused name among `app1`, `app2`, …
- An **existing schema** of that data table, by name.
- **None**: the tables go in `public`.
3. **Tables**: only when that data table already holds tables the app could use, whether to **reuse** them (name them) or **create new ones**.
Do not ask when:
- the user already said where the data goes. A part they left out takes its default above: the default data table, a new `appN` schema, new tables;
- the app, or its saved draft, already carries a data config (`data.datatable`, `data.schema` or `data.tables`). That config is the answer;
- the app stores nothing.
With no data table in the workspace there is nothing to choose: say that one has to be configured in the workspace settings before the app can store data.
A question left unanswered or dismissed is not a go-ahead for the defaults: stop there and say what is still needed.
Then build what was chosen: the data table and schema become the app's `data.datatable` and `data.schema`, the tables it uses, reused or new, go in `data.tables`, and a new schema is created (`CREATE SCHEMA IF NOT EXISTS`) before the tables that go in it.
`wmill app new --datatable <name> --schema <name>` records both and, for a schema that does not exist yet, writes the migration in `sql_to_apply/` that creates it: nothing exists in the database until `wmill app dev` applies that migration, so the app's table migrations come after it.
## Frontend
### Entrypoint
+22 -15
View File
@@ -6,47 +6,53 @@ This guide covers raw apps from the terminal: scaffolding via `wmill app new`, t
**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
### Step 1 — Settle the setup
You need three things to run the command:
You need three values 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".
**summary and path**: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
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**: never ask. Use the one the user named, otherwise the one the project's existing apps use, otherwise `react19` (see "Starting a new app" in the authoring guide).
**data setup**: when the app has to store data and the user did not say where, ask which data table, which schema and which tables, as "Data setup" in the authoring guide lays out. `wmill datatable list` gives the data tables to offer.
Use whichever interactive question facility your runtime provides — a structured multi-choice tool if available, otherwise plain chat — and group everything missing into a single round-trip so the user answers 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).
- For the data setup — the choices the authoring guide lists, the default first.
Only proceed once you have concrete values for all three. If the user replies with something ambiguous, ask again rather than guessing.
Only proceed once every value is concrete. 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:
Once the setup is settled, run it:
```bash
wmill app new \
--summary "Customer dashboard" \
--path f/sales/dashboard \
--framework react19
--framework react19 \
--datatable main \
--schema app1
```
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.
`--summary`, `--path` and `--framework` are the minimum. The datatable wizard and the "Open in Claude Desktop?" prompt are skipped silently because passing any of them puts the command in non-interactive mode, so the data setup only reaches the app through the flags below.
### 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. |
| `--datatable <name>` | The app stores data: the data table settled in step 1. Without it, the app is created with no datatable. |
| `--schema <name>` | Together with `--datatable`, when the app's tables go in a schema, new or existing. If the schema doesn't exist yet, writes a `sql_to_apply/` migration with `CREATE SCHEMA IF NOT EXISTS`, applied like any other migration (see "SQL Migrations" below). Leave it out for `public`. |
| `--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. |
Existing tables the app reuses have no flag: list them under `data.tables` in `raw_app.yaml` (see "Data tables" below).
### 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.
@@ -57,8 +63,9 @@ For apps the preview command runs from the app folder (`cd <app_path>__raw_app &
- ❌ 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.
- ❌ Inventing a path or summary instead of asking the user.
- ❌ Asking which framework to use — `react19` is the default when nothing says otherwise.
- ❌ Creating a schema or tables for a new app before the user has said where its data goes.
- ❌ Passing `--overwrite` automatically when the directory exists — confirm with the user first.
### Interactive (only when a human is at the terminal)
+40
View File
@@ -10,6 +10,46 @@ A raw app has three logical parts:
- **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.
## Starting a new app
A new app gets the setup Windmill's own new-app dialog gives one: a framework, then where its data lives.
### Framework
Use the framework the user named. When they named none, follow what there is to follow: the app being edited, or the apps the request points at. With nothing to follow, use **React 19**, the recommended default, **without asking**. React 18, Svelte 5 and Vue are for when they are asked for.
### Data setup
When a new app has to store data and the user did not say where, **ask before creating anything**: no app, no schema, no table. Where the data lives is the user's decision, and tables created in the wrong place stay there. Look up the workspace's data tables first so every choice you offer exists, then ask, as proposed answers rather than open questions:
1. **Data table**: which one to use, the default first (`main` when it exists, otherwise the first listed). Skip this one when the workspace has a single data table.
2. **Schema**: where the app's new tables go.
- A **new schema**, the usual choice, offered first. Suggest the first unused name among `app1`, `app2`, …
- An **existing schema** of that data table, by name.
- **None**: the tables go in `public`.
3. **Tables**: only when that data table already holds tables the app could use, whether to **reuse** them (name them) or **create new ones**.
Do not ask when:
- the user already said where the data goes. A part they left out takes its default above: the default data table, a new `appN` schema, new tables;
- the app, or its saved draft, already carries a data config (`data.datatable`, `data.schema` or `data.tables`). That config is the answer;
- the app stores nothing.
With no data table in the workspace there is nothing to choose: say that one has to be configured in the workspace settings before the app can store data.
A question left unanswered or dismissed is not a go-ahead for the defaults: stop there and say what is still needed.
Then build what was chosen: the data table and schema become the app's `data.datatable` and `data.schema`, the tables it uses, reused or new, go in `data.tables`, and a new schema is created (`CREATE SCHEMA IF NOT EXISTS`) before the tables that go in it.
<!-- cli-only -->
`wmill app new --datatable <name> --schema <name>` records both and, for a schema that does not exist yet, writes the migration in `sql_to_apply/` that creates it: nothing exists in the database until `wmill app dev` applies that migration, so the app's table migrations come after it.
<!-- /cli-only -->
<!-- chat-only -->
### An app that arrives already set up
An app the user just started from the new-app dialog is already set up: its files are the framework template it was started with, and a `data` config it carries (`data.datatable`, `data.schema` or `data.tables`) is the data setup it was started with. Build that app in place, with that data config, and in that framework unless the user now names another. Do not ask for either, and do not create a second app. Only when it carries no data config and has to store data is the data setup still to ask.
<!-- /chat-only -->
## Frontend
### Entrypoint