windmill-cli 1.700.1 → 1.701.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/esm/main.js +188 -126
  2. package/package.json +1 -1
package/esm/main.js CHANGED
@@ -16752,7 +16752,7 @@ var init_OpenAPI = __esm(() => {
16752
16752
  PASSWORD: undefined,
16753
16753
  TOKEN: getEnv3("WM_TOKEN"),
16754
16754
  USERNAME: undefined,
16755
- VERSION: "1.700.1",
16755
+ VERSION: "1.701.0",
16756
16756
  WITH_CREDENTIALS: true,
16757
16757
  interceptors: {
16758
16758
  request: new Interceptors,
@@ -44417,7 +44417,7 @@ var require_zipEntries = __commonJS((exports, module) => {
44417
44417
  if (this.centralDirRecords !== this.files.length) {
44418
44418
  if (this.centralDirRecords !== 0 && this.files.length === 0) {
44419
44419
  throw new Error("Corrupted zip or bug: expected " + this.centralDirRecords + " records in central dir, got " + this.files.length);
44420
- } else {}
44420
+ }
44421
44421
  }
44422
44422
  },
44423
44423
  readEndOfCentral: function() {
@@ -55347,7 +55347,7 @@ var require_loader = __commonJS((exports, module) => {
55347
55347
  var error2 = generateError(state, message);
55348
55348
  if (state.onWarning) {
55349
55349
  state.onWarning.call(null, error2);
55350
- } else {}
55350
+ }
55351
55351
  }
55352
55352
  var directiveHandlers = {
55353
55353
  YAML: function handleYamlDirective(state, name, args) {
@@ -55885,7 +55885,7 @@ var require_loader = __commonJS((exports, module) => {
55885
55885
  } else if (detectedIndent) {
55886
55886
  sc.value += common3.repeat(`
55887
55887
  `, emptyLines + 1);
55888
- } else {}
55888
+ }
55889
55889
  detectedIndent = true;
55890
55890
  emptyLines = 0;
55891
55891
  captureStart = state.position;
@@ -64370,12 +64370,16 @@ var init_path_assigner = __esm(() => {
64370
64370
  });
64371
64371
 
64372
64372
  // windmill-utils-internal/src/inline-scripts/extractor.ts
64373
- function extractRawscriptInline(id, summary, rawscript, mapping, separator, assigner) {
64373
+ function extractRawscriptInline(id, summary, rawscript, mapping, separator, assigner, failOnInlineDirective) {
64374
64374
  const [basePath, ext2] = assigner.assignPath(summary ?? id, rawscript.language);
64375
64375
  const mappedPath = mapping[id];
64376
64376
  const path10 = mappedPath ?? basePath + ext2;
64377
64377
  const language = rawscript.language;
64378
64378
  const content = rawscript.content;
64379
+ if (failOnInlineDirective && typeof content === "string" && content.startsWith("!inline ")) {
64380
+ throw new Error(`Refusing to extract corrupted inline script for module '${id}': ` + `rawscript.content is the literal string \`${content.split(`
64381
+ `)[0]}\` ` + `instead of script source. The backend's flow_version.value is corrupt — ` + `re-push from a known-good local copy to repair it.`);
64382
+ }
64379
64383
  const r = [{ path: path10, content, language, is_lock: false }];
64380
64384
  rawscript.content = "!inline " + path10.replaceAll(separator, "/");
64381
64385
  const lock = rawscript.lock;
@@ -64390,19 +64394,20 @@ function extractRawscriptInline(id, summary, rawscript, mapping, separator, assi
64390
64394
  }
64391
64395
  function extractInlineScripts(modules, mapping = {}, separator = "/", defaultTs, pathAssigner, options) {
64392
64396
  const assigner = pathAssigner ?? newPathAssigner(defaultTs ?? "bun", { skipInlineScriptSuffix: options?.skipInlineScriptSuffix });
64397
+ const failOnInlineDirective = options?.failOnInlineDirective ?? false;
64393
64398
  return modules.flatMap((m) => {
64394
64399
  if (m.value.type == "rawscript") {
64395
- return extractRawscriptInline(m.id, m.summary, m.value, mapping, separator, assigner);
64400
+ return extractRawscriptInline(m.id, m.summary, m.value, mapping, separator, assigner, failOnInlineDirective);
64396
64401
  } else if (m.value.type == "forloopflow") {
64397
- return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner);
64402
+ return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner, options);
64398
64403
  } else if (m.value.type == "branchall") {
64399
- return m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner));
64404
+ return m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner, options));
64400
64405
  } else if (m.value.type == "whileloopflow") {
64401
- return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner);
64406
+ return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner, options);
64402
64407
  } else if (m.value.type == "branchone") {
64403
64408
  return [
64404
- ...m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner)),
64405
- ...extractInlineScripts(m.value.default, mapping, separator, defaultTs, assigner)
64409
+ ...m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner, options)),
64410
+ ...extractInlineScripts(m.value.default, mapping, separator, defaultTs, assigner, options)
64406
64411
  ];
64407
64412
  } else if (m.value.type == "aiagent") {
64408
64413
  return (m.value.tools ?? []).flatMap((tool) => {
@@ -64410,7 +64415,7 @@ function extractInlineScripts(modules, mapping = {}, separator = "/", defaultTs,
64410
64415
  if (!toolValue || toolValue.tool_type !== "flowmodule" || toolValue.type !== "rawscript") {
64411
64416
  return [];
64412
64417
  }
64413
- return extractRawscriptInline(tool.id, tool.summary, toolValue, mapping, separator, assigner);
64418
+ return extractRawscriptInline(tool.id, tool.summary, toolValue, mapping, separator, assigner, failOnInlineDirective);
64414
64419
  });
64415
64420
  } else {
64416
64421
  return [];
@@ -64786,12 +64791,16 @@ async function generateFlowLockInternal(folder, dryRun, workspace, opts, justUpd
64786
64791
  const treePath = fileToTreePath.get(k) ?? folderNormalized + "/" + path10.basename(k, path10.extname(k));
64787
64792
  return tree.isStale(treePath);
64788
64793
  }) : changedScripts;
64789
- await replaceInlineScripts(flowValue.value.modules, fileReader, exports_log, folder + SEP7, SEP7, locksToRemove);
64794
+ const missingFiles = [];
64795
+ await replaceInlineScripts(flowValue.value.modules, fileReader, exports_log, folder + SEP7, SEP7, locksToRemove, missingFiles);
64790
64796
  if (flowValue.value.failure_module) {
64791
- await replaceInlineScripts([flowValue.value.failure_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove);
64797
+ await replaceInlineScripts([flowValue.value.failure_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove, missingFiles);
64792
64798
  }
64793
64799
  if (flowValue.value.preprocessor_module) {
64794
- await replaceInlineScripts([flowValue.value.preprocessor_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove);
64800
+ await replaceInlineScripts([flowValue.value.preprocessor_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove, missingFiles);
64801
+ }
64802
+ if (missingFiles.length > 0) {
64803
+ throw new Error(`Cannot regenerate lock for flow ${remote_path}: missing inline script file(s): ${missingFiles.join(", ")}. ` + `Either restore the file(s) or remove the !inline reference(s) from flow.yaml before retrying.`);
64795
64804
  }
64796
64805
  const tempScriptRefs = tree?.getTempScriptRefs(folderNormalized);
64797
64806
  const savedNotes = flowValue.value.notes;
@@ -64804,12 +64813,13 @@ async function generateFlowLockInternal(folder, dryRun, workspace, opts, justUpd
64804
64813
  const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun", {
64805
64814
  skipInlineScriptSuffix: getNonDottedPaths()
64806
64815
  });
64807
- const inlineScripts = extractInlineScripts(flowValue.value.modules, currentMapping, SEP7, opts.defaultTs, lockAssigner);
64816
+ const extractOpts = { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true };
64817
+ const inlineScripts = extractInlineScripts(flowValue.value.modules, currentMapping, SEP7, opts.defaultTs, lockAssigner, extractOpts);
64808
64818
  if (flowValue.value.failure_module) {
64809
- inlineScripts.push(...extractInlineScripts([flowValue.value.failure_module], currentMapping, SEP7, opts.defaultTs, lockAssigner));
64819
+ inlineScripts.push(...extractInlineScripts([flowValue.value.failure_module], currentMapping, SEP7, opts.defaultTs, lockAssigner, extractOpts));
64810
64820
  }
64811
64821
  if (flowValue.value.preprocessor_module) {
64812
- inlineScripts.push(...extractInlineScripts([flowValue.value.preprocessor_module], currentMapping, SEP7, opts.defaultTs, lockAssigner));
64822
+ inlineScripts.push(...extractInlineScripts([flowValue.value.preprocessor_module], currentMapping, SEP7, opts.defaultTs, lockAssigner, extractOpts));
64813
64823
  }
64814
64824
  inlineScripts.forEach((s) => {
64815
64825
  writeIfChanged(process.cwd() + SEP7 + folder + SEP7 + s.path, s.content);
@@ -65931,12 +65941,12 @@ function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, re
65931
65941
  try {
65932
65942
  const assigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
65933
65943
  const inlineMapping = extractCurrentMapping(flow.value.modules, {}, flow.value.failure_module, flow.value.preprocessor_module);
65934
- inlineScripts = extractInlineScripts(flow.value.modules, inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths() });
65944
+ inlineScripts = extractInlineScripts(flow.value.modules, inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true });
65935
65945
  if (flow.value.failure_module) {
65936
- inlineScripts.push(...extractInlineScripts([flow.value.failure_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths() }));
65946
+ inlineScripts.push(...extractInlineScripts([flow.value.failure_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true }));
65937
65947
  }
65938
65948
  if (flow.value.preprocessor_module) {
65939
- inlineScripts.push(...extractInlineScripts([flow.value.preprocessor_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths() }));
65949
+ inlineScripts.push(...extractInlineScripts([flow.value.preprocessor_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true }));
65940
65950
  }
65941
65951
  } catch (error2) {
65942
65952
  error(`Failed to extract inline scripts for flow at path: ${p}`);
@@ -75684,7 +75694,7 @@ async function pushFlow(workspace, remotePath, localPath, message, permissionedA
75684
75694
  await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, exports_log, localPath, SEP20, undefined, missingFiles);
75685
75695
  }
75686
75696
  if (missingFiles.length > 0) {
75687
- warn(colors.yellow(`Warning: missing inline script file(s): ${missingFiles.join(", ")}. ` + `The flow will be pushed with unresolved !inline references.`));
75697
+ throw new Error(`Cannot push flow ${remotePath}: missing inline script file(s): ${missingFiles.join(", ")}. ` + `Either restore the file(s) or remove the !inline reference(s) from flow.yaml before pushing.`);
75688
75698
  }
75689
75699
  const hasOnBehalfOf = localFlow.has_on_behalf_of ?? !!localFlow.on_behalf_of_email;
75690
75700
  delete localFlow.has_on_behalf_of;
@@ -84945,9 +84955,9 @@ name: raw-app
84945
84955
  description: MUST use when creating raw apps.
84946
84956
  ---
84947
84957
 
84948
- # Windmill Raw Apps
84958
+ # Windmill Raw Apps — CLI workflow
84949
84959
 
84950
- Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
84960
+ 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.
84951
84961
 
84952
84962
  ## Creating a Raw App
84953
84963
 
@@ -85016,7 +85026,7 @@ wmill app new
85016
85026
 
85017
85027
  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.
85018
85028
 
85019
- ## App Structure
85029
+ ## On-disk app layout
85020
85030
 
85021
85031
  \`\`\`
85022
85032
  my_app{{RAW_APP_SUFFIX}}/
@@ -85036,11 +85046,7 @@ my_app{{RAW_APP_SUFFIX}}/
85036
85046
  └── *.sql # SQL files to apply via dev server
85037
85047
  \`\`\`
85038
85048
 
85039
- ## Backend Runnables
85040
-
85041
- Backend runnables are server-side scripts that your frontend can call. They live in the \`backend/\` folder.
85042
-
85043
- ### Creating a Backend Runnable
85049
+ ## Backend runnables on disk
85044
85050
 
85045
85051
  Add a code file to the \`backend/\` folder:
85046
85052
 
@@ -85050,7 +85056,7 @@ backend/<id>.<ext>
85050
85056
 
85051
85057
  The runnable ID is the filename without extension. For example, \`get_user.ts\` creates a runnable with ID \`get_user\`.
85052
85058
 
85053
- ### Supported Languages
85059
+ ### Supported languages (extension-driven)
85054
85060
 
85055
85061
  | Language | Extension | Example |
85056
85062
  |------------------|--------------|------------------|
@@ -85072,27 +85078,14 @@ The runnable ID is the filename without extension. For example, \`get_user.ts\`
85072
85078
  | C# | \`.cs\` | \`myFunc.cs\` |
85073
85079
  | Java | \`.java\` | \`myFunc.java\` |
85074
85080
 
85075
- ### Example Backend Runnable
85076
-
85077
- **backend/get_user.ts:**
85078
- \`\`\`typescript
85079
- import * as wmill from 'windmill-client';
85080
-
85081
- export async function main(user_id: string) {
85082
- const sql = wmill.datatable();
85083
- const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
85084
- return user;
85085
- }
85086
- \`\`\`
85087
-
85088
- After creating, tell the user they can generate lock files by running:
85081
+ After creating a runnable, tell the user they can generate lock files by running:
85089
85082
  \`\`\`bash
85090
85083
  wmill generate-metadata
85091
85084
  \`\`\`
85092
85085
 
85093
- ### Optional YAML Configuration
85086
+ ### Optional YAML configuration
85094
85087
 
85095
- Add a \`<id>.yaml\` file to configure fields or static values:
85088
+ Add a \`<id>.yaml\` file alongside the code to configure fields or static values:
85096
85089
 
85097
85090
  **backend/get_user.yaml:**
85098
85091
  \`\`\`yaml
@@ -85103,7 +85096,7 @@ fields:
85103
85096
  value: "default_user"
85104
85097
  \`\`\`
85105
85098
 
85106
- ### Referencing Existing Scripts
85099
+ ### Referencing existing scripts
85107
85100
 
85108
85101
  To use an existing Windmill script instead of inline code:
85109
85102
 
@@ -85119,32 +85112,9 @@ type: flow
85119
85112
  path: f/my_folder/my_flow
85120
85113
  \`\`\`
85121
85114
 
85122
- ### Calling Backend from Frontend
85123
-
85124
- Import from the auto-generated \`wmill.ts\`:
85125
-
85126
- \`\`\`typescript
85127
- import { backend } from './wmill';
85128
-
85129
- // Call a backend runnable
85130
- const user = await backend.get_user({ user_id: '123' });
85131
- \`\`\`
85132
-
85133
- The \`wmill.ts\` file provides type-safe access to all backend runnables.
85134
-
85135
- ## Data Tables
85136
-
85137
- Raw apps can query Windmill datatables (PostgreSQL databases managed by Windmill).
85138
-
85139
- ### Critical Rules
85115
+ ## Data tables \`raw_app.yaml\` config
85140
85116
 
85141
- 1. **ONLY USE WHITELISTED TABLES**: You can ONLY query tables listed in \`raw_app.yaml\` \`data.tables\`. Tables not in this list are NOT accessible.
85142
-
85143
- 2. **ADD TABLES BEFORE USING**: To use a new table, first add it to \`data.tables\` in \`raw_app.yaml\`.
85144
-
85145
- 3. **USE CONFIGURED DATATABLE/SCHEMA**: Check the app's \`raw_app.yaml\` for the default datatable and schema.
85146
-
85147
- ### Configuration in raw_app.yaml
85117
+ The \`data\` block in \`raw_app.yaml\` controls which tables the app can query.
85148
85118
 
85149
85119
  \`\`\`yaml
85150
85120
  data:
@@ -85156,48 +85126,9 @@ data:
85156
85126
  \`\`\`
85157
85127
 
85158
85128
  **Table reference formats:**
85159
- - \`<datatable>\` - All tables in the datatable
85160
- - \`<datatable>/<table>\` - Specific table in public schema
85161
- - \`<datatable>/<schema>:<table>\` - Table in specific schema
85162
-
85163
- ### Querying in TypeScript (Bun/Deno)
85164
-
85165
- \`\`\`typescript
85166
- import * as wmill from 'windmill-client';
85167
-
85168
- export async function main(user_id: string) {
85169
- const sql = wmill.datatable(); // Or: wmill.datatable('other_datatable')
85170
-
85171
- // Parameterized queries (safe from SQL injection)
85172
- const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
85173
- const users = await sql\`SELECT * FROM users WHERE active = \${true}\`.fetch();
85174
-
85175
- // Insert/Update
85176
- await sql\`INSERT INTO users (name, email) VALUES (\${name}, \${email})\`;
85177
- await sql\`UPDATE users SET name = \${newName} WHERE id = \${user_id}\`;
85178
-
85179
- return user;
85180
- }
85181
- \`\`\`
85182
-
85183
- ### Querying in Python
85184
-
85185
- \`\`\`python
85186
- import wmill
85187
-
85188
- def main(user_id: str):
85189
- db = wmill.datatable() # Or: wmill.datatable('other_datatable')
85190
-
85191
- # Use $1, $2, etc. for parameters
85192
- user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
85193
- users = db.query('SELECT * FROM users WHERE active = $1', True).fetch()
85194
-
85195
- # Insert/Update
85196
- db.query('INSERT INTO users (name, email) VALUES ($1, $2)', name, email)
85197
- db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id)
85198
-
85199
- return user
85200
- \`\`\`
85129
+ - \`<datatable>\` All tables in the datatable
85130
+ - \`<datatable>/<table>\` Specific table in public schema
85131
+ - \`<datatable>/<schema>:<table>\` Table in specific schema
85201
85132
 
85202
85133
  ## SQL Migrations (sql_to_apply/)
85203
85134
 
@@ -85206,11 +85137,11 @@ The \`sql_to_apply/\` folder is for creating/modifying database tables during de
85206
85137
  ### Workflow
85207
85138
 
85208
85139
  1. Create \`.sql\` files in \`sql_to_apply/\`
85209
- 2. Run \`wmill app dev\` - the dev server watches this folder
85140
+ 2. Run \`wmill app dev\` the dev server watches this folder
85210
85141
  3. When SQL files change, a modal appears in the browser to confirm execution
85211
85142
  4. After creating tables, **add them to \`data.tables\`** in \`raw_app.yaml\`
85212
85143
 
85213
- ### Example Migration
85144
+ ### Example migration
85214
85145
 
85215
85146
  **sql_to_apply/001_create_users.sql:**
85216
85147
  \`\`\`sql
@@ -85229,12 +85160,12 @@ data:
85229
85160
  - main/users
85230
85161
  \`\`\`
85231
85162
 
85232
- ### Migration Best Practices
85163
+ ### Migration best practices
85233
85164
 
85234
85165
  - **Use idempotent SQL**: \`CREATE TABLE IF NOT EXISTS\`, etc.
85235
85166
  - **Number files**: \`001_\`, \`002_\` for ordering
85236
85167
  - **Always whitelist tables** after creation
85237
- - This folder is NOT synced - it's for local development only
85168
+ - This folder is NOT synced it's for local development only
85238
85169
 
85239
85170
  ## CLI Commands
85240
85171
 
@@ -85250,14 +85181,145 @@ For everything else, tell the user which command fits their intent and let them
85250
85181
  | \`wmill sync push\` | Deploy app to Windmill |
85251
85182
  | \`wmill sync pull\` | Pull latest from Windmill |
85252
85183
 
85184
+
85185
+
85186
+ # Windmill Raw Apps
85187
+
85188
+ Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
85189
+
85190
+ ## App shape
85191
+
85192
+ A raw app has three logical parts:
85193
+
85194
+ - **Frontend** — bundled with esbuild from \`index.tsx\` as the entrypoint. Files include the entrypoint, components (\`App.tsx\`), styles, etc.
85195
+ - **Backend runnables** — server-side scripts the frontend calls, each addressed by a unique key.
85196
+ - **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.
85197
+
85198
+ ## Frontend
85199
+
85200
+ ### Entrypoint
85201
+
85202
+ \`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
85203
+
85204
+ ### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
85205
+
85206
+ 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.
85207
+
85208
+ ### Calling backend runnables
85209
+
85210
+ Import the generated bindings and call the runnable like a function:
85211
+
85212
+ \`\`\`typescript
85213
+ import { backend } from './wmill';
85214
+
85215
+ // Call a backend runnable
85216
+ const user = await backend.get_user({ user_id: '123' });
85217
+ \`\`\`
85218
+
85219
+ The frontend cannot reach datatables, workspace items, or external services on its own — it goes through \`backend.<key>(args)\` for everything server-side.
85220
+
85221
+ ## Backend runnables
85222
+
85223
+ Each runnable has a unique key (used to call it from the frontend) and one of four types:
85224
+
85225
+ | Type | What it is |
85226
+ |---|---|
85227
+ | \`inline\` | Custom code stored on the app itself. Most common for app-specific logic. |
85228
+ | \`script\` | Reference to an existing workspace script by path. |
85229
+ | \`flow\` | Reference to an existing workspace flow by path. |
85230
+ | \`hubscript\` | Reference to a hub script by path. |
85231
+
85232
+ ### Inline runnables
85233
+
85234
+ 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.
85235
+
85236
+ **TypeScript example** (\`backend/get_user.ts\`):
85237
+
85238
+ \`\`\`typescript
85239
+ import * as wmill from 'windmill-client';
85240
+
85241
+ export async function main(user_id: string) {
85242
+ const sql = wmill.datatable();
85243
+ const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
85244
+ return user;
85245
+ }
85246
+ \`\`\`
85247
+
85248
+ **Python example** (\`backend/get_user.py\`):
85249
+
85250
+ \`\`\`python
85251
+ import wmill
85252
+
85253
+ def main(user_id: str):
85254
+ db = wmill.datatable()
85255
+ user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
85256
+ return user
85257
+ \`\`\`
85258
+
85259
+ ### Path runnables (script / flow / hubscript)
85260
+
85261
+ 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.
85262
+
85263
+ ### Static inputs
85264
+
85265
+ \`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.
85266
+
85267
+ ## Data Tables
85268
+
85269
+ Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the \`wmill\` client; the frontend never queries them directly.
85270
+
85271
+ ### Critical rules
85272
+
85273
+ 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.
85274
+ 2. **Add tables before using**: queries against unlisted tables fail at runtime. When you introduce a new table, register it in \`data.tables\` first.
85275
+ 3. **Use the configured datatable/schema**: the app's \`data\` config sets the default datatable and schema; reference them consistently across runnables.
85276
+
85277
+ ### Querying in TypeScript (Bun/Deno)
85278
+
85279
+ \`\`\`typescript
85280
+ import * as wmill from 'windmill-client';
85281
+
85282
+ export async function main(user_id: string) {
85283
+ const sql = wmill.datatable(); // Or: wmill.datatable('other_datatable')
85284
+
85285
+ // Parameterized queries (safe from SQL injection)
85286
+ const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
85287
+ const users = await sql\`SELECT * FROM users WHERE active = \${true}\`.fetch();
85288
+
85289
+ // Insert/Update
85290
+ await sql\`INSERT INTO users (name, email) VALUES (\${name}, \${email})\`;
85291
+ await sql\`UPDATE users SET name = \${newName} WHERE id = \${user_id}\`;
85292
+
85293
+ return user;
85294
+ }
85295
+ \`\`\`
85296
+
85297
+ ### Querying in Python
85298
+
85299
+ \`\`\`python
85300
+ import wmill
85301
+
85302
+ def main(user_id: str):
85303
+ db = wmill.datatable() # Or: wmill.datatable('other_datatable')
85304
+
85305
+ # Use $1, $2, etc. for parameters
85306
+ user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
85307
+ users = db.query('SELECT * FROM users WHERE active = $1', True).fetch()
85308
+
85309
+ # Insert/Update
85310
+ db.query('INSERT INTO users (name, email) VALUES ($1, $2)', name, email)
85311
+ db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id)
85312
+
85313
+ return user
85314
+ \`\`\`
85315
+
85253
85316
  ## Best Practices
85254
85317
 
85255
- 1. **Check DATATABLES.md** for existing tables before creating new ones
85256
- 2. **Use parameterized queries** - never concatenate user input into SQL
85257
- 3. **Keep runnables focused** - one function per file
85258
- 4. **Use descriptive IDs** - \`get_user.ts\` not \`a.ts\`
85259
- 5. **Always whitelist tables** - add to \`data.tables\` before querying
85260
- 6. **Generate locks** - tell the user to run \`wmill generate-metadata\` after adding/modifying backend runnables
85318
+ 1. **Check existing tables** before creating new ones — reuse beats schema growth.
85319
+ 2. **Use parameterized queries** never concatenate user input into SQL.
85320
+ 3. **Keep runnables focused** one function per runnable; small surface area.
85321
+ 4. **Use descriptive keys** \`get_user\`, not \`a\`.
85322
+ 5. **Always whitelist tables** adding a runnable that queries a new table requires the table to be in \`data.tables\` first.
85261
85323
  `,
85262
85324
  triggers: `---
85263
85325
  name: triggers
@@ -89624,7 +89686,7 @@ var config_default = command35;
89624
89686
 
89625
89687
  // src/main.ts
89626
89688
  await init_context();
89627
- var VERSION = "1.700.1";
89689
+ var VERSION = "1.701.0";
89628
89690
  async function checkVersionSafe(cmd) {
89629
89691
  const mainCommand = cmd.getMainCommand();
89630
89692
  const upgradeCommand = mainCommand.getCommand("upgrade");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windmill-cli",
3
- "version": "1.700.1",
3
+ "version": "1.701.0",
4
4
  "description": "CLI for Windmill",
5
5
  "license": "Apache 2.0",
6
6
  "type": "module",