mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
110384580e
* 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>
138 lines
5.2 KiB
Markdown
138 lines
5.2 KiB
Markdown
# 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:
|
|
|
|
```typescript
|
|
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`):
|
|
|
|
```typescript
|
|
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`):
|
|
|
|
```python
|
|
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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```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 keys** — `get_user`, not `a`.
|
|
5. **Always whitelist tables** — adding a runnable that queries a new table requires the table to be in `data.tables` first.
|