Windmillに新しいTriggerCrud型のトリガーを追加する際、バックエンドからCLI、画面、同期やキャプチャ機能まで必要な変更箇所を確認できる開発チェックリストです。
- 新しい外部サービスのトリガーを追加したいとき
- 実装箇所の抜け漏れを点検したいとき
- トリガー追加のPR前に検証したいとき
WindmillのRaw AppをCLIで作成し、フロントエンド、バックエンド処理、データテーブルの設定や開発用SQLマイグレーションを組み立てる手順を案内します。
MUST use when creating raw apps.
実行スコア 未検証
インストール方法を見るWindmillのRaw AppをCLIから作成し、ファイル構成や設定を整えるためのガイドです。wmill app newに必要な概要、Windmill上のパス、フレームワークを整理し、React 19、React 18、Svelte 5を使ったアプリのひな型を作ります。フロントエンドのエントリーポイントとマウント方法、生成されたwmillバインディングを通じたバックエンドRunnableの呼び出し、データテーブルやスキーマの設定を扱います。バックエンド処理のファイル配置、既存スクリプトやフローの参照、開発用SQLマイグレーションの作成と設定方法も説明されています。資料の範囲では、開発サーバー、Lint、メタデータ生成などのCLIコマンドの使い分けも確認できます。
Windmill上で独自の画面を作りたいときや、既存のRunnableやデータテーブルを画面から利用したいときに向いています。アプリの雛形から設定を始める場合、フロントエンドとサーバー側処理のつなぎ方を確認したい場合、開発中にテーブルを追加する場合にも役立ちます。
資料は途中までです。利用にはWindmillのwmill CLIが前提です。対応フレームワークとしてReact 19、React 18、Svelte 5が記載されています。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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.
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.
You need three values to run the command:
f/folder/my_app or u/username/my_appreact19 (recommended), react18, svelte5summary and path: if the user's request did not supply them, ask. Do not guess values and do not invent paths.
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:
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 every value is concrete. If the user replies with something ambiguous, ask again rather than guessing.
Once the setup is settled, run it:
wmill app new \
--summary "Customer dashboard" \
--path f/sales/dashboard \
--framework react19 \
--datatable main \
--schema app1
--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.
| Flag | When to add it |
|---|---|
--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).
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.
wmill app new with no flags (the prompt will hang).wmill app new and follow the prompts" — that's a step backwards from what you can do directly.react19 is the default when nothing says otherwise.--overwrite automatically when the directory exists — confirm with the user first.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.
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 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
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.
| 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:
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.
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"
viewer_data_access: true makes an inline runnable read data tables and object storage as the person using the app instead of as the app's identity (see "Reading data as the viewer").
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
raw_app.yaml configThe data block in raw_app.yaml controls which tables the app can query.
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 schemaRoles: 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, if it has one, plus the roles of the app's identity, or of whoever is using the app for a runnable with viewer_data_access: true, unless that default is admin.
The sql_to_apply/ folder is for creating/modifying database tables during development.
.sql files in sql_to_apply/wmill app dev — the dev server watches this folderdata.tables in raw_app.yamlsql_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
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.
CREATE TABLE IF NOT EXISTS, etc.001_, 002_ for orderingCommands 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 |
Raw apps let you build custom frontends with React or Svelte that connect to Windmill backend runnables and datatables.
A raw app has three logical parts:
index.tsx as the entrypoint. Files include the entrypoint, components (App.tsx), styles, etc.A new app gets the setup Windmill's own new-app dialog gives one: a framework, then where its data lives.
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 and Svelte 5 are for when they are asked for. These three are the only frameworks the raw app bundler builds. When the user asks to build or convert an app in another framework, write no code in it: say it is not available and offer these three instead.
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:
main when it exists, otherwise the first listed). Skip this one when the workspace has a single data table.app1, app2, …public.Do not ask when:
appN schema, new tables;data.datatable, data.schema or data.tables). That config is the answer;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.
The entrypoint is index.tsx for React and index.ts for Svelte. 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 and keep the entrypoint as the mount shim.
React (index.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')! }).
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.
Set the browser tab title with document.title = '...' (or a <title> the framework renders into <head>). The app has no index.html of its own, so a <title> written there is never served; the deployed app's page shows whatever document.title holds, and follows it when it changes.
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.
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.
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:
import { backend } from './wmill';
const user = await backend.get_user({ user_id: '123' });
Start a long job, then await it:
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:
import { getJob } from './wmill';
const job = await getJob(jobId);
if (job.type === 'CompletedJob') setReport(job.result);
backendAsync resolves a job id and nothing else — guard on it before storing or polling. A poll loop started on an undefined id never completes and shows as a row stuck "running" forever:
const jobId = await backendAsync.run_report(args);
if (!jobId) throw new Error('run_report did not start a job');
Never hand-write a job-polling runnable. A backend runnable that calls jobs/list, or that returns getResultMaybe(...) for the frontend to poll, reimplements backendAsync + waitJob / getJob / streamJob — and it is what leads to guessing at base URLs and tokens.
An app can be demoed by recording a session: every interaction becomes a step carrying a snapshot of the page, replayed publicly or on the Hub. Password inputs are masked automatically. Mark anything else that must not appear with data-wm-no-record — the whole marked subtree is dropped from every snapshot, along with its values and the step's own metadata:
<label data-wm-no-record>
Customer SSN <input value={ssn} onChange={onSsn} />
</label>
Apply it to customer data, internal notes and anything else a viewer of the demo should not see. It costs nothing when the app is never recorded.
A flow deployed with chat mode on is a chat backend (streaming answer, tool calls, memory, conversation history). Do not drive it through a runnable: add @windmill-labs/chat-sdk to package.json and use it directly, it detects the app's Windmill and credential.
import { useWindmillChat } from '@windmill-labs/chat-sdk/react'
const chat = useWindmillChat({ flowPath: 'f/support/assistant' })
// chat.messages ({ role, content, pending, success, tool? }), chat.status, chat.sendMessage(text), chat.stop()
@windmill-labs/chat-sdk/ai-sdk gives a ChatTransport for the Vercel AI SDK's useChat, @windmill-labs/chat-sdk/assistant-ui a runtime for assistant-ui. The flow must be deployed, not a draft. A sandboxed app needs jobs:run in its frontend SDK scopes, plus flow_conversations:write for the conversation sidebar; without them the chat keeps history in the browser.
agent runnableChat with a saved AI agent (an ai_agent resource) through an agent runnable, a path runnable whose path is the agent's. Each call runs one turn of the agent's chat as the app, so a sandboxed app declares no frontend SDK scope, viewers get no consent prompt and need no access to the agent. Prefer it in every app, sandboxed or not:
In raw_app.yaml:
runnables:
chat:
type: agent
path: f/support/agent
import { useWindmillChat } from '@windmill-labs/chat-sdk/react'
import * as wmill from './wmill'
const chat = useWindmillChat({ wmill, runnable: 'chat' })
runnable is the runnable's key; each runnable keeps its own conversations. The runnable takes user_message and memory_id, the conversation id, which the chat fills in: the agent remembers earlier turns of a conversation, and a signed-in viewer's conversations are theirs alone. Called directly as backend.<key>({ user_message }) without memory_id, it is a single run with no conversation, for a one-off question. The chat needs an agent with managed memory. An agent whose memory is off keeps none: its runnable takes previous_messages (chat messages) instead of memory_id, and the app keeps the history itself. The chat keeps its transcript in the app's own storage. chat.stop() stops following without cancelling the turn, and a message sent while it still runs is refused with TurnRunningError. Attachments are not supported this way.
Each runnable has a unique key (used to call it from the frontend) and one of five 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. |
agent | Reference to a saved AI agent by path. Each call is one turn of its chat (see the chat section above). |
Inline runnables carry their own source code, and must expose a main function as their entrypoint.
On disk, a runnable's language is determined by its backend file extension.
TypeScript example (runnable get_user):
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 (runnable get_user):
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
An inline runnable can import another inline runnable of the same app by its id, as a relative import: import { getDb } from './shared' (TypeScript, Bun) or from .shared import get_db (Python). This works across TypeScript (Bun) runnables and across Python runnables, not between the two. The imported runnable's own dependencies are picked up, and it can import further runnables in turn. A runnable that only holds helpers needs no main; the frontend just never calls it. A TypeScript runnable whose id is main cannot be imported, because that name is taken by the entrypoint file of every TypeScript job: give shared code any other id.
wmill client is already authenticatedAn inline runnable runs as an ordinary Windmill job. import * as wmill from 'windmill-client' (TypeScript) and import wmill (Python) are already pointed at this instance and this workspace — there is nothing to configure.
Don't read WM_TOKEN or BASE_INTERNAL_URL and build an API URL to fetch. The client's own setClient already reads exactly those, and it also sets the credentials mode a raw app needs (WM_RAW_APP suppresses credentials, because a sandboxed bundle calls the API from an opaque origin that can never pair with Access-Control-Allow-Origin: *). Rebuilding that by hand drops the parts you can't see. Use wmill.* for everything Windmill, and fetch only for third-party APIs.
Use only wmill functions the SDK actually exports; for an endpoint none of them covers, the generated service classes (JobService, ScriptService, ...) are importable from windmill-client. What is not available is a name you guessed at: getBaseUrl and getWorkspaceToken are inventions, not API.
When type is script, flow, hubscript or agent, 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; an agent's is user_message and user_attachments, plus memory_id when the agent keeps managed memory or previous_messages when its memory is off.
Before writing an inline runnable, look for a workspace script or flow that already does the job, or a Hub script (a prebuilt integration at hub/<version>/<app>/<name>) for a third-party service, and reference it instead of copying its logic.
This decides whether an app works before anything is deployed:
script / flow / hubscript / agent) run the DEPLOYED item at that path. So do wmill.runFlow, wmill.runFlowAsync and wmill.runScriptByPath called from inside a runnable. A draft — including a draft you just created — does not exist for them.So an app wired to a flow you just wrote does nothing until that flow is deployed. The app itself does NOT have to be deployed for this: the preview runs the app's draft, so the referenced flow is the only thing that has to exist deployed.
That makes the fix a one-item deploy, not a release. Offer to deploy exactly the referenced flow or script and leave the app a draft the user keeps testing in the preview — do not push the whole change set through the review-and-deploy page, and do not ask the user to deploy the app, unless they said they want to ship it.
Do NOT quietly reimplement the flow inside an inline runnable to dodge the deployment: that leaves the user with two copies of the same logic and an app that ignores the flow they asked for. Inline the logic only when the user actually wants it inline.
Prefer a path runnable of type flow over an inline runnable that calls wmill.runFlowAsync. The path runnable gives the frontend the flow's real input schema and works with backend / backendAsync / waitJob like any other runnable; a hand-written wrapper gives up all of that.
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.
A draft app is reachable by nobody; deploying is what exposes it, and its backend runnables with it. Otherwise only users with access to the app can open it, unless the app is opened to:
Either one lets those people run the app's backend runnables, so never open an app up unless the user asks.
public: true in raw_app.yaml deploys the app for anonymous users, and guests: true for guests. Remove the line and the next push closes the app again.
An app's custom URL is not in raw_app.yaml: each workspace has its own file next to the app folder, <app path>.<workspace>.custom_path.yaml (for example f/sales/dashboard.prod.custom_path.yaml holding custom_path: dashboard), where <workspace> is the workspace's name in wmill.yaml, or its id. A push applies only the target workspace's file and leaves the deployed custom URL alone when there is none, so deleting the file does not remove the URL: custom_path: '' does. Setting or clearing one takes a workspace admin.
New apps are sandboxed: the frontend is isolated from the viewer's own Windmill session and reaches Windmill only through its backend runnables, plus whatever frontend API scopes the app declares, which each viewer approves before the app runs. With isolation turned off, the frontend runs with the viewer's full session. The scopes are jobs:run, jobs:read, resources:read, variables:read, flow_conversations:read and flow_conversations:write; grant only the ones the frontend calls. The viewer's identity needs no scope: it is in window.ctx, and whoami works with any of them.
Inside the sandbox, IndexedDB is unavailable, third-party sign-in and OAuth redirects fail, and requests to other services carry Origin: null, which an API that checks the origin rejects. Work around these first: call the service from a backend runnable, keep state in a data table or localStorage, or grant the frontend API scope the code needs. Suggest turning isolation off only when none of that works, and say plainly that it is a security risk: the app's code, and any XSS bug in it, then runs with the full Windmill permissions of everyone who opens the app.
In raw_app.yaml:
sandbox: true
frontend_api: [jobs:run]
A file with a sandbox key states both settings: sandbox: false turns isolation off, and a missing frontend_api means no scopes. A file without the key leaves the deployed app's isolation and scopes as they are, so removing sandbox: true does not unsandbox it. frontend_api needs sandbox: true; the push fails otherwise.
A backend runnable reads data as the app's identity (the publisher in publisher mode), the same for every viewer. An inline runnable can instead read data as the person using the app: its data table connections that name no role hold that viewer's roles, and object storage permission rules (which paths may be listed, read, written) are checked against the viewer. Resources, variables and everything else stay the app's, and an anonymous viewer is refused. Turn it on only when what the runnable reads should differ per viewer: row-level security on a data table, or per-user paths in object storage. Both are Enterprise Edition features.
Set it with viewer_data_access: true in the runnable's backend/<id>.yaml.
Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the wmill client; the frontend never queries them directly. When the app needs to store or persist data (user data, settings, application state, records, logs), use a data table.
main data table exists.data.tables config. Queries against unlisted tables fail at runtime, so register a new table there before using it.public schema, so write every other table as schema.table, in table creation and queries alike. The app's data config sets the default datatable and schema its tables go in; use them consistently across runnables.data.roles gives a data table a role, every wmill.datatable call on it passes that role (wmill.datatable('main', { role: 'analyst' }) in TypeScript, wmill.datatable('main', role='analyst') in Python). The role only reaches what it was granted, so a query outside it fails with permission denied.data.roles is called with no role. Unless its default role is admin, the connection then holds the default role, if it may use it, plus every role it is a tenant of, and is refused when that is none. Whose roles those are follows the runnable's identity (see "Reading data as the viewer"). Row-level security policies written as pg_has_role(current_user, '<role>', 'member') then return the right rows with nothing added to the code. A role pins the connection to that one role either way, so a data table whose rows should differ per viewer gets no data.roles entry. Data table roles are an Enterprise Edition feature, self-hosted only.wmill datatable list lists the workspace's data tables. Create or change tables with a migration in sql_to_apply/ (see "SQL Migrations" above), then add them to data.tables in raw_app.yaml.
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})`.execute();
await sql`UPDATE users SET name = ${newName} WHERE id = ${user_id}`.execute();
return user;
}
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).execute()
db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id).execute()
return user
db.query(...) only build a statement. It runs when you call fetch / fetchOne / fetchOneScalar / execute (fetch / fetch_one / fetch_one_scalar / execute in Python). An INSERT or UPDATE without one writes nothing and raises nothing. Awaiting the statement itself is a no-op — it is not a promise.get_user, not a.data.tables first.data-wm-no-record — it is what keeps that data out of a recorded demo; passwords are handled for you.backendAsync + waitJob for long work — never a hand-written job-polling runnable.@windmill-labs/chat-sdk for a chat over a chat-mode flow or a saved agent — never hand-roll a runnable that runs it and polls its stream; a saved agent is an agent runnable passed as { wmill, runnable }.まだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
Windmillに新しいTriggerCrud型のトリガーを追加する際、バックエンドからCLI、画面、同期やキャプチャ機能まで必要な変更箇所を確認できる開発チェックリストです。
WindmillのAIチャットを変更するときに、ツールやプロンプト、結果の返し方、コンテキスト量を整理し、評価で変更前後の挙動を比べるための指針です。
WindmillのAI生成モード向けに、実際の利用要求に沿ったベンチマークケースを作成・実行します。生成結果を決定的な検証とLLM評価で確認し、AIチャットやcopilotの変更前後を比較できます。
Windmill CLIのコマンドを使い、スクリプトやフローなどの管理、ジョブの実行履歴確認、失敗原因の調査を進めるためのリファレンスです。
モジュールのインターフェースを小さく保ちながら、内部に複雑な処理をまとめる設計を支援します。テストしやすさや変更のしやすさを考え、処理の境目を整理できます。
変更内容を確認し、対象ファイルだけを選んで、形式や長さの規則に沿ったConventional Commits形式のGitコミットを作成するスキルです。