Files
windmill/system_prompts/base/raw-app.md
centdix 110384580e refactor: add global ai chat mode with workspace-item draft tools (#9056)
* docs: add global ai mode plan

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add global ai draft mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: scope global ai mode to scripts and flows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify global ai workspace item shape

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: split global ai write tool into per-type tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add global ai schedule and trigger workspace item tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add dev-only /global_drafts route to inspect ai draft store

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add edit_script and patch_flow_json global ai tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add deploy_workspace_item global ai tool with confirmation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: emit open-resource action card after deploy_workspace_item

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add delete_workspace_item global ai tool with confirmation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(system_prompts): emit RESOURCES_BASE and resource/variable zod schemas

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add global ai resource and variable workspace item tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: search_resource_types uses listResourceType to avoid embedding feature dep

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "fix: search_resource_types uses listResourceType to avoid embedding feature dep"

This reverts commit 6d1d19514a.

* feat: emit open-resource action card for variable and resource deploys

* feat: add global ai raw app workspace item tools

* feat: split raw-app prompt into chat-only authoring and cli prefix

* feat: add init_app global ai tool to scaffold raw apps from templates

* fix: pass write_flow value as JSON string for gemini compat

* refactor: hoist countExactMatches and applyExactReplace to chat/shared

* refactor: extract editableFlowJson module shared with global mode

* fix(global): preserve flow schema and groups across draft and deploy

* feat: extract inline scripts from flow reads and patches in global mode

* refactor: add findAndReplace helper for match-validated text patches

* refactor: extract getInlineRunnableContent helper for app file tools

* refactor: extract assertNotGeneratedAppFile guard for /wmill.d.ts

* feat: gate global ai mode behind localStorage flag for dev rollout

* chore: bump svelte to ^5.55.5 in raw app template (sync with main)

* fix: isolate global ai draft rollout

* fix: preserve global ai deploy metadata

* fix: harden global ai draft tools

* chore: remove global ai plan doc

* fix: align raw app prompt guidance

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-12 09:42:07 +00:00

5.2 KiB

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

index.tsx is the bundling entrypoint. It typically renders a top-level App component. The bundler is esbuild.

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:

import { backend } from './wmill';

// Call a backend runnable
const user = await backend.get_user({ user_id: '123' });

The frontend cannot reach datatables, workspace items, or external services on its own — it goes through backend.<key>(args) for everything server-side.

Backend runnables

Each runnable has a unique key (used to call it from the frontend) and one of four types:

Type What it is
inline Custom code stored on the app itself. Most common for app-specific logic.
script Reference to an existing workspace script by path.
flow Reference to an existing workspace flow by path.
hubscript Reference to a hub script by path.

Inline runnables

Inline runnables carry their own source code. For file-based raw apps, the runnable language is determined by the backend file extension. The script must expose a main function as its entrypoint.

TypeScript example (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;
}

Python example (backend/get_user.py):

import wmill

def main(user_id: str):
    db = wmill.datatable()
    user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
    return user

Path runnables (script / flow / hubscript)

When type is script, flow, or hubscript, the runnable just stores a path to an existing workspace or hub item — no inline code. The referenced item's input/output schema becomes the runnable's surface.

Static inputs

staticInputs is an optional Record<string, any> for arguments not overridable from the frontend. Useful with path runnables to pre-fill some args while leaving the rest to the frontend caller.

Data Tables

Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the wmill client; the frontend never queries them directly.

Critical rules

  1. Whitelisted tables only: a runnable can only query tables listed in the app's data.tables config. Tables not in this list are not accessible.
  2. Add tables before using: queries against unlisted tables fail at runtime. When you introduce a new table, register it in data.tables first.
  3. Use the configured datatable/schema: the app's data config sets the default datatable and schema; reference them consistently across runnables.

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

Best Practices

  1. Check existing tables before creating new ones — reuse beats schema growth.
  2. Use parameterized queries — never concatenate user input into SQL.
  3. Keep runnables focused — one function per runnable; small surface area.
  4. Use descriptive keysget_user, not a.
  5. Always whitelist tables — adding a runnable that queries a new table requires the table to be in data.tables first.