mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
merge
This commit is contained in:
@@ -98,6 +98,21 @@ jobs:
|
||||
vcpkg.exe install openssl:x64-windows-static
|
||||
vcpkg.exe integrate install
|
||||
|
||||
- name: Free disk space (post-vcpkg)
|
||||
shell: pwsh
|
||||
run: |
|
||||
# vcpkg leaves multi-GB of buildtrees/downloads after installing openssl;
|
||||
# we only need the installed/ dir for linking.
|
||||
$vcpkgRoot = $env:VCPKG_INSTALLATION_ROOT
|
||||
foreach ($sub in @("buildtrees", "downloads", "packages")) {
|
||||
$path = Join-Path $vcpkgRoot $sub
|
||||
if (Test-Path $path) {
|
||||
Write-Host "Removing $path"
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $path
|
||||
}
|
||||
}
|
||||
Get-PSDrive C | Select-Object Used,Free | Format-Table -AutoSize
|
||||
|
||||
- name: Get runtime paths
|
||||
id: runtime-paths
|
||||
shell: pwsh
|
||||
@@ -119,6 +134,10 @@ jobs:
|
||||
cargo build --release -p windmill_duckdb_ffi_internal
|
||||
New-Item -ItemType Directory -Path ..\target\debug -Force
|
||||
Copy-Item target\release\windmill_duckdb_ffi_internal.dll ..\target\debug\
|
||||
# duckdb is bundled (~2GB of build artifacts); the DLL is the only
|
||||
# thing we need from this excluded-crate target dir.
|
||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue target
|
||||
Get-PSDrive C | Select-Object Used,Free | Format-Table -AutoSize
|
||||
|
||||
- name: Print runtime versions and env
|
||||
shell: pwsh
|
||||
@@ -136,6 +155,10 @@ jobs:
|
||||
echo "USERPROFILE=$env:USERPROFILE"
|
||||
echo "HOME=$env:HOME"
|
||||
|
||||
- name: Disk space before cargo test
|
||||
shell: pwsh
|
||||
run: Get-PSDrive C | Select-Object Used,Free | Format-Table -AutoSize
|
||||
|
||||
- name: cargo test
|
||||
working-directory: backend
|
||||
timeout-minutes: 60
|
||||
@@ -144,13 +167,16 @@ jobs:
|
||||
RUST_LOG: "off"
|
||||
RUST_LOG_STYLE: never
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
CARGO_BUILD_JOBS: 12
|
||||
# 16-vcpu runners with disabled PDB still hit LNK1180 ("insufficient
|
||||
# disk space") at link time with 12 parallel link jobs: each test
|
||||
# binary link spikes several hundred MB of transient I/O. Capping at
|
||||
# 8 trades ~25% wall time for headroom on the ~75GB runner disk.
|
||||
CARGO_BUILD_JOBS: 8
|
||||
# backend/Cargo.toml sets split-debuginfo = "unpacked", which on
|
||||
# windows-msvc is coerced to "packed": every test-binary link spawns
|
||||
# the mspdbsrv.exe PDB type server and writes a large .pdb. With 12
|
||||
# parallel link jobs this races the type-server cap (LNK1318 "LIMIT
|
||||
# (12)") and exhausts the runner disk (LNK1180). CI needs no debug
|
||||
# info, so disable PDB generation for the dev/test profiles here.
|
||||
# the mspdbsrv.exe PDB type server and writes a large .pdb. CI needs
|
||||
# no debug info, so disable PDB generation for the dev/test profiles
|
||||
# here (avoids both LNK1318 type-server limit and PDB disk usage).
|
||||
CARGO_PROFILE_DEV_SPLIT_DEBUGINFO: "off"
|
||||
CARGO_PROFILE_TEST_SPLIT_DEBUGINFO: "off"
|
||||
# Tests' poll-time stack frames (deep nested async fn chains in
|
||||
|
||||
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [1.711.0](https://github.com/windmill-labs/windmill/compare/v1.710.1...v1.711.0) (2026-05-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** add object-storage commands and flow test-step ([#9326](https://github.com/windmill-labs/windmill/issues/9326)) ([36f574f](https://github.com/windmill-labs/windmill/commit/36f574ff951198a4d40ee068a27d74c41ce32154))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** handle __flow suffix when deriving the flow's Windmill path ([#9333](https://github.com/windmill-labs/windmill/issues/9333)) ([6f77034](https://github.com/windmill-labs/windmill/commit/6f770346fb330997a836c39fba347df4c088a83c))
|
||||
* **queue:** duration-weighted workspace fairness signal ([#9329](https://github.com/windmill-labs/windmill/issues/9329)) ([42d2121](https://github.com/windmill-labs/windmill/commit/42d2121af925de50f549ecb72ffb5132f5c41079))
|
||||
|
||||
## [1.710.1](https://github.com/windmill-labs/windmill/compare/v1.710.0...v1.710.1) (2026-05-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve workspace fairness ([896add0](https://github.com/windmill-labs/windmill/commit/896add0350f4de31f5674d6be0907a582c5ec17e))
|
||||
|
||||
## [1.710.0](https://github.com/windmill-labs/windmill/compare/v1.709.0...v1.710.0) (2026-05-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **queue:** stochastic admission + EE availability of workspace fairness algorithm ([#9321](https://github.com/windmill-labs/windmill/issues/9321)) ([8bf7fd2](https://github.com/windmill-labs/windmill/commit/8bf7fd2c921c48861b71731a085b18ea8f72fb68))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **websocket-trigger:** honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY ([#9324](https://github.com/windmill-labs/windmill/issues/9324)) ([6f36316](https://github.com/windmill-labs/windmill/commit/6f363163df9cd15f5af7d56cf34a01b70d236830))
|
||||
|
||||
## [1.709.0](https://github.com/windmill-labs/windmill/compare/v1.708.0...v1.709.0) (2026-05-25)
|
||||
|
||||
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM background_task_state\n WHERE name LIKE $1\n AND updated_at < NOW() - INTERVAL '7 days'",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8597cd40f80e69edbf1bc7d7402baca32e33e871be454acb5175c11361fe1b0a"
|
||||
}
|
||||
Generated
+154
-154
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -87,7 +87,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2de9dc793360764dc81b9593d72cb50347656a52
|
||||
327d23f7438968a21bac9fd42e7f6f027c61477c
|
||||
|
||||
+24
-24
@@ -6183,7 +6183,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -6263,7 +6263,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6275,7 +6275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6284,7 +6284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6296,7 +6296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6308,7 +6308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6320,7 +6320,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6332,7 +6332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6344,7 +6344,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6355,7 +6355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6366,7 +6366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6378,7 +6378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6389,7 +6389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6411,7 +6411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6423,7 +6423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6437,7 +6437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6454,7 +6454,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6467,7 +6467,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6479,7 +6479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6497,7 +6497,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6513,7 +6513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -6529,7 +6529,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6561,7 +6561,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6572,7 +6572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
@@ -2709,6 +2709,26 @@ pub async fn monitor_db(
|
||||
}
|
||||
};
|
||||
|
||||
// run every hour (120 iterations * 30s = 3600s)
|
||||
let cleanup_stale_server_heartbeats_f = async {
|
||||
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(120) {
|
||||
if let Some(db) = conn.as_sql() {
|
||||
match windmill_api::cleanup_stale_server_heartbeats(db).await {
|
||||
Ok(count) if count > 0 => {
|
||||
tracing::info!(
|
||||
"Deleted {} stale server_heartbeat background_task_state rows",
|
||||
count
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Error cleaning up stale server_heartbeat rows: {:?}", e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// run every hour (120 iterations * 30s = 3600s)
|
||||
let manage_audit_partitions_f = async {
|
||||
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(120) {
|
||||
@@ -2767,6 +2787,7 @@ pub async fn monitor_db(
|
||||
native_triggers_sync_f,
|
||||
cleanup_notify_events_f,
|
||||
check_expiring_tokens_f,
|
||||
cleanup_stale_server_heartbeats_f,
|
||||
manage_audit_partitions_f,
|
||||
export_audit_logs_to_object_store_f,
|
||||
cleanup_scheduled_job_deletions_f,
|
||||
|
||||
@@ -79,15 +79,28 @@ async fn create_workspace(db: &Pool<Postgres>, id: &str) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Insert `n` completed jobs for `workspace_id`, each ending `secs_ago`
|
||||
/// seconds in the past with a 1-second wall-clock duration. The fairness
|
||||
/// algorithm weights contributions by `duration_ms` (clamped to the window),
|
||||
/// so each job contributes ~1 worker-second when fully inside the window.
|
||||
async fn insert_completed(db: &Pool<Postgres>, workspace_id: &str, n: usize, secs_ago: i32) {
|
||||
for _ in 0..n {
|
||||
let id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO v2_job (id, workspace_id, kind)
|
||||
VALUES (gen_random_uuid(), $1, 'script'::job_kind) RETURNING id",
|
||||
)
|
||||
.bind(workspace_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job_completed (id, workspace_id, duration_ms, status,
|
||||
started_at, completed_at)
|
||||
VALUES (gen_random_uuid(), $1, 1, 'success'::job_status,
|
||||
NOW() - make_interval(secs => $2::int),
|
||||
NOW() - make_interval(secs => $2::int))",
|
||||
VALUES ($1, $2, 1000, 'success'::job_status,
|
||||
NOW() - make_interval(secs => ($3::int + 1)),
|
||||
NOW() - make_interval(secs => $3::int))",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(workspace_id)
|
||||
.bind(secs_ago)
|
||||
.execute(db)
|
||||
@@ -106,12 +119,98 @@ async fn insert_queued(
|
||||
let mut ids = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag)
|
||||
VALUES (gen_random_uuid(), $1, NOW(), $2, $3) RETURNING id",
|
||||
"INSERT INTO v2_job (id, workspace_id, kind, tag)
|
||||
VALUES (gen_random_uuid(), $1, 'script'::job_kind, $2) RETURNING id",
|
||||
)
|
||||
.bind(workspace_id)
|
||||
.bind(tag)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
// Running jobs need a `started_at` for the fairness algorithm to
|
||||
// compute a positive elapsed-time contribution. Backdate by 1s so
|
||||
// each running row contributes ~1 worker-second by the time the
|
||||
// refresh runs, matching the `insert_completed` scale.
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag, started_at)
|
||||
VALUES ($1, $2, NOW(), $3, $4,
|
||||
CASE WHEN $3 THEN NOW() - interval '1 second' ELSE NULL END)",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(workspace_id)
|
||||
.bind(running)
|
||||
.bind(tag)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
if running {
|
||||
// The fairness algorithm bounds the running contribution by the
|
||||
// per-job `v2_job_runtime.ping`. Insert a fresh ping so each
|
||||
// running row accrues real-time worker-seconds.
|
||||
sqlx::query(
|
||||
"INSERT INTO v2_job_runtime (id, ping) VALUES ($1, NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET ping = NOW()",
|
||||
)
|
||||
.bind(id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_live_worker_ping(db, workspace_id, id).await;
|
||||
}
|
||||
ids.push(id);
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// Insert a live `worker_ping` row claiming the given job. Each insert uses
|
||||
/// a fresh randomly-named worker so callers can stack multiple pings without
|
||||
/// PK collisions on `worker`.
|
||||
async fn insert_live_worker_ping(db: &Pool<Postgres>, workspace_id: &str, job_id: Uuid) {
|
||||
let worker_name = format!("test-worker-{}", Uuid::new_v4());
|
||||
sqlx::query(
|
||||
"INSERT INTO worker_ping (worker, worker_instance, ping_at, ip, current_job_id, current_job_workspace_id)
|
||||
VALUES ($1, 'test', NOW(), '127.0.0.1', $2, $3)",
|
||||
)
|
||||
.bind(&worker_name)
|
||||
.bind(job_id)
|
||||
.bind(workspace_id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Insert a "zombie" running row: a row in `v2_job_queue` with `running=true`
|
||||
/// but **no** live `worker_ping` claiming it (no paired worker, or the worker
|
||||
/// has stopped pinging). The fairness algorithm must NOT count these — they
|
||||
/// don't consume any worker slot.
|
||||
async fn insert_zombie_running(db: &Pool<Postgres>, workspace_id: &str, n: usize) -> Vec<Uuid> {
|
||||
let mut ids = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, tag, started_at)
|
||||
VALUES (gen_random_uuid(), $1, NOW() - interval '1 hour', true, 'deno',
|
||||
NOW() - interval '1 hour') RETURNING id",
|
||||
)
|
||||
.bind(workspace_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
ids.push(id);
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// Insert a concurrency-suspended row: `running=true` AND `suspend > 0`. These
|
||||
/// rows are not being processed by any worker (the flow is paused), so the
|
||||
/// algorithm must not count them as slot occupancy.
|
||||
async fn insert_suspended_running(db: &Pool<Postgres>, workspace_id: &str, n: usize) -> Vec<Uuid> {
|
||||
let mut ids = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let id: Uuid = sqlx::query_scalar(
|
||||
"INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, running, suspend, tag)
|
||||
VALUES (gen_random_uuid(), $1, NOW(), true, 1, 'deno') RETURNING id",
|
||||
)
|
||||
.bind(workspace_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -334,6 +433,76 @@ async fn fairness_catches_slot_hoggers(db: Pool<Postgres>) {
|
||||
assert_eq!(overloaded_set(), vec!["hogger".to_string()]);
|
||||
}
|
||||
|
||||
/// Regression: a workspace with a large backlog of `running = true` rows that
|
||||
/// have **no live worker** claiming them (worker died, ping went stale, etc.)
|
||||
/// must not be counted as "active". A previous version of the algorithm
|
||||
/// counted `v2_job_queue.running = true` directly and was perpetually pinned
|
||||
/// on the workspace with the most zombie rows, masking every other workspace.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial]
|
||||
#[ignore = "flaky in CI"]
|
||||
async fn fairness_ignores_zombie_running_rows(db: Pool<Postgres>) {
|
||||
reset_fairness_state();
|
||||
create_workspace(&db, "stuck_backlog").await;
|
||||
create_workspace(&db, "real_noisy").await;
|
||||
create_workspace(&db, "victim").await;
|
||||
|
||||
// 100 zombie running rows for `stuck_backlog`. No paired worker_ping ⇒
|
||||
// no live worker is processing them. Old algorithm: 100 units of fake
|
||||
// activity. New algorithm: 0 units.
|
||||
insert_zombie_running(&db, "stuck_backlog", 100).await;
|
||||
// `real_noisy` is genuinely flooding the cluster.
|
||||
insert_completed(&db, "real_noisy", 60, 2).await;
|
||||
insert_completed(&db, "victim", 5, 3).await;
|
||||
|
||||
refresh_overloaded(&db).await.expect("refresh ok");
|
||||
|
||||
let set = overloaded_set();
|
||||
assert!(
|
||||
!set.contains(&"stuck_backlog".to_string()),
|
||||
"zombie running rows must not flag a workspace as overloaded; got {set:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
set,
|
||||
vec!["real_noisy".to_string()],
|
||||
"the actually noisy workspace must surface even when another workspace \
|
||||
has a large backlog of zombie running rows; got {set:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: concurrency-suspended rows (`running = true AND suspend > 0`)
|
||||
/// are not consuming worker slots — the flow is paused at a suspend step —
|
||||
/// and must not contribute to the activity share.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
#[serial]
|
||||
#[ignore = "flaky in CI"]
|
||||
async fn fairness_ignores_concurrency_suspended_rows(db: Pool<Postgres>) {
|
||||
reset_fairness_state();
|
||||
create_workspace(&db, "concurrency_capped").await;
|
||||
create_workspace(&db, "real_noisy").await;
|
||||
create_workspace(&db, "victim").await;
|
||||
|
||||
// 100 concurrency-suspended rows. Each has `running = true` (the legacy
|
||||
// signal) but `suspend > 0` (not actually on a worker).
|
||||
insert_suspended_running(&db, "concurrency_capped", 100).await;
|
||||
insert_completed(&db, "real_noisy", 60, 2).await;
|
||||
insert_completed(&db, "victim", 5, 3).await;
|
||||
|
||||
refresh_overloaded(&db).await.expect("refresh ok");
|
||||
|
||||
let set = overloaded_set();
|
||||
assert!(
|
||||
!set.contains(&"concurrency_capped".to_string()),
|
||||
"concurrency-suspended rows must not flag a workspace as overloaded; got {set:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
set,
|
||||
vec!["real_noisy".to_string()],
|
||||
"noisy workspace must still surface despite another workspace's large \
|
||||
suspended backlog; got {set:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Simulation test
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -449,7 +618,21 @@ async fn mock_worker(
|
||||
stats: Arc<Mutex<Stats>>,
|
||||
completed_counter: Arc<AtomicU64>,
|
||||
) {
|
||||
let _ = worker_id;
|
||||
let worker_name = format!("mock-worker-{worker_id}");
|
||||
// Each mock worker maintains its own `worker_ping` row, the way a real
|
||||
// worker would: `current_job_*` set on pick-up, cleared on completion.
|
||||
// The fairness algorithm now reads slot occupancy from `worker_ping` (so
|
||||
// that concurrency-suspended rows and zombies with no live ping do not
|
||||
// inflate the denominator), so the simulation must keep this in sync.
|
||||
sqlx::query(
|
||||
"INSERT INTO worker_ping (worker, worker_instance, ping_at) VALUES ($1, 'sim', NOW())
|
||||
ON CONFLICT (worker) DO UPDATE SET ping_at = NOW(),
|
||||
current_job_id = NULL, current_job_workspace_id = NULL",
|
||||
)
|
||||
.bind(&worker_name)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
let standard_sql = "WITH picked AS (
|
||||
SELECT id FROM v2_job_queue
|
||||
WHERE running = false AND scheduled_for <= now()
|
||||
@@ -516,6 +699,20 @@ async fn mock_worker(
|
||||
|
||||
match row {
|
||||
Some((id, ws, dur_ms, created_at)) => {
|
||||
// Claim the slot on this worker's ping so the fairness
|
||||
// algorithm counts this workspace's slot occupancy.
|
||||
sqlx::query(
|
||||
"UPDATE worker_ping SET ping_at = NOW(),
|
||||
current_job_id = $1, current_job_workspace_id = $2
|
||||
WHERE worker = $3",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&ws)
|
||||
.bind(&worker_name)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(dur_ms as u64)).await;
|
||||
|
||||
// Move to completed atomically: insert + delete in one query.
|
||||
@@ -534,6 +731,17 @@ async fn mock_worker(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Release the slot.
|
||||
sqlx::query(
|
||||
"UPDATE worker_ping SET ping_at = NOW(),
|
||||
current_job_id = NULL, current_job_workspace_id = NULL
|
||||
WHERE worker = $1",
|
||||
)
|
||||
.bind(&worker_name)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let latency_ms = (completed_at - created_at).num_milliseconds().max(0) as u64;
|
||||
{
|
||||
let mut s = stats.lock().await;
|
||||
@@ -543,7 +751,13 @@ async fn mock_worker(
|
||||
}
|
||||
None => {
|
||||
// Empty queue (or every queued workspace is capped). Back off
|
||||
// briefly so we don't hammer the DB.
|
||||
// briefly so we don't hammer the DB. Refresh the heartbeat so
|
||||
// this worker's ping doesn't go stale during long idle gaps.
|
||||
sqlx::query("UPDATE worker_ping SET ping_at = NOW() WHERE worker = $1")
|
||||
.bind(&worker_name)
|
||||
.execute(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.709.0
|
||||
version: 1.711.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -1261,3 +1261,26 @@ pub async fn check_any_server_started(db: &DB, not_before: chrono::DateTime<chro
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Delete `server_heartbeat:*` rows that have not been refreshed in a long
|
||||
/// time. Each server startup generates a fresh random `INSTANCE_NAME` and
|
||||
/// inserts a new row keyed by `server_heartbeat:{instance}`; because that
|
||||
/// row is only written once (on startup) and never updated thereafter, the
|
||||
/// table grows by one row per server restart and is otherwise never pruned.
|
||||
///
|
||||
/// The row is only consulted by `check_any_server_started`, which itself
|
||||
/// filters on `updated_at > not_before` (the moment a restart was initiated),
|
||||
/// so rows older than the cutoff cannot influence any restart decision and
|
||||
/// are safe to delete.
|
||||
pub async fn cleanup_stale_server_heartbeats(db: &DB) -> anyhow::Result<u64> {
|
||||
let prefix = format!("{SERVER_HEARTBEAT_TASK}:");
|
||||
let res = sqlx::query!(
|
||||
"DELETE FROM background_task_state
|
||||
WHERE name LIKE $1
|
||||
AND updated_at < NOW() - INTERVAL '7 days'",
|
||||
format!("{prefix}%"),
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(res.rows_affected())
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.709.0";
|
||||
export const VERSION = "v1.711.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import * as log from "../../core/log.ts";
|
||||
import { sep as SEP } from "node:path";
|
||||
import { dirname, sep as SEP } from "node:path";
|
||||
import { stringify as yamlStringify } from "yaml";
|
||||
import { yamlParseFile } from "../../utils/yaml.ts";
|
||||
import { readTextFile, validateRequiredArgs } from "../../utils/utils.ts";
|
||||
@@ -542,6 +542,7 @@ async function preview(
|
||||
data?: string;
|
||||
silent: boolean;
|
||||
remote?: boolean;
|
||||
step?: string;
|
||||
} & SyncOptions,
|
||||
flowPath: string
|
||||
) {
|
||||
@@ -562,7 +563,10 @@ async function preview(
|
||||
if (!isFlowDir) {
|
||||
// Check if it's a flow.yaml file
|
||||
if (flowPath.endsWith("flow.yaml") || flowPath.endsWith("flow.json")) {
|
||||
flowPath = flowPath.substring(0, flowPath.lastIndexOf(SEP));
|
||||
// Use dirname so a bare "flow.yaml" (no parent dir) becomes "."
|
||||
// instead of "" — the latter, after appending SEP below, becomes "/"
|
||||
// and silently reads from filesystem root.
|
||||
flowPath = dirname(flowPath);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Flow path must be a .flow/__flow directory or a flow.yaml file"
|
||||
@@ -636,18 +640,35 @@ async function preview(
|
||||
|
||||
const input = opts.data ? await resolve(opts.data) : {};
|
||||
|
||||
log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`);
|
||||
|
||||
// Single-step mode: run only the named module's runnable.
|
||||
// The full-flow prep above (inline-script replacement, local PathScript
|
||||
// substitution, tempScriptRefs build) is exactly what the single step needs
|
||||
// too — PathScript modules have already been rewritten to inline rawscript
|
||||
// when `useLocalPathScripts` is set, and tempScriptRefs covers relative
|
||||
// imports in inline scripts.
|
||||
// Compute the flow's windmill path (e.g. "f/cli_smoke/myrelflow"). Used as
|
||||
// the anchor for relative-import resolution: inline scripts in this flow are
|
||||
// treated as living at "<flow_wm_path>/<step_id>", so "./util" resolves to
|
||||
// "<flow_wm_path_parent>/util" — matching the keys in temp_script_refs.
|
||||
const flowWmPath = stripFlowSuffix(flowPath).replaceAll(SEP, "/");
|
||||
|
||||
if (opts.step) {
|
||||
await previewStep(opts.step, localFlow, flowWmPath, workspace, input, tempScriptRefs, opts.silent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.silent) {
|
||||
log.info(colors.yellow(`Running flow preview for ${flowPath}...`));
|
||||
}
|
||||
|
||||
log.debug(`Flow value: ${JSON.stringify(localFlow.value, null, 2)}`);
|
||||
|
||||
// Run the flow preview — start the job, then poll for completion
|
||||
const jobId = await wmill.runFlowPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
value: localFlow.value,
|
||||
path: flowPath.substring(0, flowPath.indexOf(".flow")).replaceAll(SEP, "/"),
|
||||
path: flowWmPath,
|
||||
args: input,
|
||||
temp_script_refs: tempScriptRefs,
|
||||
},
|
||||
@@ -674,6 +695,176 @@ async function preview(
|
||||
}
|
||||
}
|
||||
|
||||
async function previewStep(
|
||||
stepId: string,
|
||||
localFlow: FlowFile,
|
||||
flowWmPath: string,
|
||||
workspace: { workspaceId: string },
|
||||
baseArgs: Record<string, unknown>,
|
||||
tempScriptRefs: Record<string, string> | undefined,
|
||||
silent: boolean,
|
||||
) {
|
||||
const module = findStepInFlowValue(localFlow.value, stepId);
|
||||
if (!module) {
|
||||
const available = collectStepIds(localFlow.value).join(", ") || "(none)";
|
||||
throw new Error(`Step '${stepId}' not found in flow. Available steps: ${available}`);
|
||||
}
|
||||
|
||||
// The preprocessor module receives args via _ENTRYPOINT_OVERRIDE so the
|
||||
// runner picks the preprocessor entrypoint (matches frontend behavior in
|
||||
// copilot/chat/flow/core.ts).
|
||||
const args =
|
||||
stepId === "preprocessor"
|
||||
? { _ENTRYPOINT_OVERRIDE: "preprocessor", ...baseArgs }
|
||||
: baseArgs;
|
||||
|
||||
const moduleValue = module.value;
|
||||
let jobId: string;
|
||||
if (moduleValue?.type === "rawscript") {
|
||||
log.info(colors.yellow(`Previewing step '${stepId}' (rawscript, ${moduleValue.language})...`));
|
||||
jobId = await wmill.runScriptPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
content: moduleValue.content ?? "",
|
||||
language: moduleValue.language,
|
||||
// Anchor relative imports to "<flow_wm_path>/<step_id>" so
|
||||
// temp_script_refs (keyed by Windmill paths) resolve correctly.
|
||||
// Without `path`, the worker defaults to "tmp/main" and "../foo"
|
||||
// resolves to "tmp/foo", missing every entry in temp_script_refs.
|
||||
path: `${flowWmPath}/${stepId}`,
|
||||
flow_path: flowWmPath,
|
||||
args,
|
||||
temp_script_refs: tempScriptRefs,
|
||||
},
|
||||
});
|
||||
} else if (moduleValue?.type === "script") {
|
||||
// Falls through here only when the deployed PathScript is what we want —
|
||||
// either --remote was passed, or no local file exists for this path.
|
||||
log.info(colors.yellow(`Previewing step '${stepId}' (script ${moduleValue.path})...`));
|
||||
const script = moduleValue.hash
|
||||
? await wmill.getScriptByHash({
|
||||
workspace: workspace.workspaceId,
|
||||
hash: moduleValue.hash,
|
||||
})
|
||||
: await wmill.getScriptByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path: moduleValue.path,
|
||||
});
|
||||
jobId = await wmill.runScriptPreview({
|
||||
workspace: workspace.workspaceId,
|
||||
requestBody: {
|
||||
content: script.content,
|
||||
language: script.language as any,
|
||||
// Anchor to the script's own deployed path so its relative imports
|
||||
// resolve against the workspace tree (or temp_script_refs).
|
||||
path: moduleValue.path,
|
||||
flow_path: flowWmPath,
|
||||
args,
|
||||
temp_script_refs: tempScriptRefs,
|
||||
},
|
||||
});
|
||||
} else if (moduleValue?.type === "flow") {
|
||||
log.info(colors.yellow(`Previewing step '${stepId}' (flow ${moduleValue.path})...`));
|
||||
jobId = await wmill.runFlowByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path: moduleValue.path,
|
||||
requestBody: args,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot preview step of type '${moduleValue?.type ?? "unknown"}'. Supported types: rawscript, script, flow.`
|
||||
);
|
||||
}
|
||||
|
||||
const { result, success } = await pollForJobResult(workspace.workspaceId, jobId);
|
||||
|
||||
if (!success) {
|
||||
if (silent) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.red.bold(`Step '${stepId}' failed:`));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
console.log(JSON.stringify(result));
|
||||
} else {
|
||||
log.info(colors.bold.underline.green(`Step '${stepId}' completed`));
|
||||
log.info(JSON.stringify(result, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
// Strip the `.flow`/`__flow` directory suffix to recover the flow's logical
|
||||
// Windmill path. Workspaces with nonDottedPaths use `__flow`; the default
|
||||
// uses `.flow`. A previous version used `indexOf(".flow")` which returned -1
|
||||
// (and thus `substring(0, -1) === ""`) for `__flow` folders and for the
|
||||
// `dirname("flow.yaml") === "."` fallback — producing an empty path that
|
||||
// broke relative-import resolution downstream.
|
||||
function stripFlowSuffix(flowPath: string): string {
|
||||
const stripped = flowPath.endsWith(SEP) ? flowPath.slice(0, -SEP.length) : flowPath;
|
||||
if (stripped.endsWith(".flow")) return stripped.slice(0, -".flow".length);
|
||||
if (stripped.endsWith("__flow")) return stripped.slice(0, -"__flow".length);
|
||||
return stripped;
|
||||
}
|
||||
|
||||
function findStepInFlowValue(flowValue: any, stepId: string): any | undefined {
|
||||
if (!flowValue) return undefined;
|
||||
if (flowValue.failure_module?.id === stepId) return flowValue.failure_module;
|
||||
if (flowValue.preprocessor_module?.id === stepId) return flowValue.preprocessor_module;
|
||||
return findStepInModules(flowValue.modules ?? [], stepId);
|
||||
}
|
||||
|
||||
function findStepInModules(modules: any[], stepId: string): any | undefined {
|
||||
for (const m of modules) {
|
||||
if (m?.id === stepId) return m;
|
||||
const v = m?.value;
|
||||
if (!v) continue;
|
||||
if (v.type === "forloopflow" || v.type === "whileloopflow") {
|
||||
const found = findStepInModules(v.modules ?? [], stepId);
|
||||
if (found) return found;
|
||||
} else if (v.type === "branchone") {
|
||||
for (const b of v.branches ?? []) {
|
||||
const found = findStepInModules(b.modules ?? [], stepId);
|
||||
if (found) return found;
|
||||
}
|
||||
const found = findStepInModules(v.default ?? [], stepId);
|
||||
if (found) return found;
|
||||
} else if (v.type === "branchall") {
|
||||
for (const b of v.branches ?? []) {
|
||||
const found = findStepInModules(b.modules ?? [], stepId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectStepIds(flowValue: any): string[] {
|
||||
const ids: string[] = [];
|
||||
const walkModules = (modules: any[]) => {
|
||||
for (const m of modules) {
|
||||
if (m?.id) ids.push(m.id);
|
||||
const v = m?.value;
|
||||
if (!v) continue;
|
||||
if (v.type === "forloopflow" || v.type === "whileloopflow") {
|
||||
walkModules(v.modules ?? []);
|
||||
} else if (v.type === "branchone") {
|
||||
for (const b of v.branches ?? []) walkModules(b.modules ?? []);
|
||||
walkModules(v.default ?? []);
|
||||
} else if (v.type === "branchall") {
|
||||
for (const b of v.branches ?? []) walkModules(b.modules ?? []);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (flowValue?.preprocessor_module?.id) ids.push(flowValue.preprocessor_module.id);
|
||||
if (flowValue?.failure_module?.id) ids.push(flowValue.failure_module.id);
|
||||
walkModules(flowValue?.modules ?? []);
|
||||
return ids;
|
||||
}
|
||||
|
||||
export async function generateLocks(
|
||||
opts: GlobalOptions & {
|
||||
yes?: boolean;
|
||||
@@ -890,7 +1081,7 @@ const command = new Command()
|
||||
.action(run as any)
|
||||
.command(
|
||||
"preview",
|
||||
"preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default."
|
||||
"preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step <id> to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow)."
|
||||
)
|
||||
.arguments("<flow_path:string>")
|
||||
.option(
|
||||
@@ -905,6 +1096,10 @@ const command = new Command()
|
||||
"--remote",
|
||||
"Use deployed workspace scripts for PathScript steps instead of local files."
|
||||
)
|
||||
.option(
|
||||
"--step <step_id:string>",
|
||||
"Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does."
|
||||
)
|
||||
.action(preview as any)
|
||||
.command(
|
||||
"generate-locks",
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import { GlobalOptions } from "../../types.ts";
|
||||
import { requireLogin } from "../../core/auth.ts";
|
||||
import { resolveWorkspace } from "../../core/context.ts";
|
||||
import { Command } from "@cliffy/command";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "../../core/log.ts";
|
||||
import * as wmill from "../../../gen/services.gen.ts";
|
||||
import { formatTimestamp } from "../../utils/utils.ts";
|
||||
|
||||
function formatBytes(n: number | undefined): string {
|
||||
if (n == null) return "-";
|
||||
if (n < 1024) return `${n}B`;
|
||||
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`;
|
||||
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}M`;
|
||||
return `${(n / (1024 * 1024 * 1024)).toFixed(2)}G`;
|
||||
}
|
||||
|
||||
async function listStorages(
|
||||
opts: GlobalOptions & { json?: boolean }
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const names = await wmill.getSecondaryStorageNames({
|
||||
workspace: workspace.workspaceId,
|
||||
includeDefault: true,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(names));
|
||||
return;
|
||||
}
|
||||
if (names.length === 0) {
|
||||
log.info("No object storage configured for this workspace.");
|
||||
return;
|
||||
}
|
||||
for (const name of names) {
|
||||
console.log(name === "_default_" ? `${name} ${colors.dim("(default)")}` : name);
|
||||
}
|
||||
}
|
||||
|
||||
async function listFiles(
|
||||
opts: GlobalOptions & {
|
||||
json?: boolean;
|
||||
maxKeys?: number;
|
||||
marker?: string;
|
||||
storage?: string;
|
||||
},
|
||||
prefix?: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const result = await wmill.listStoredFiles({
|
||||
workspace: workspace.workspaceId,
|
||||
maxKeys: opts.maxKeys ?? 100,
|
||||
marker: opts.marker,
|
||||
prefix,
|
||||
storage: opts.storage,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(result));
|
||||
return;
|
||||
}
|
||||
const files = result.windmill_large_files ?? [];
|
||||
if (files.length === 0) {
|
||||
log.info("No files found.");
|
||||
return;
|
||||
}
|
||||
new Table()
|
||||
.header(["Key"])
|
||||
.padding(2)
|
||||
.border(true)
|
||||
.body(files.map((f) => [f.s3]))
|
||||
.render();
|
||||
if (result.next_marker) {
|
||||
log.info(`\nMore results available. Use --marker '${result.next_marker}' to paginate.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(
|
||||
opts: GlobalOptions & {
|
||||
storage?: string;
|
||||
contentType?: string;
|
||||
contentDisposition?: string;
|
||||
},
|
||||
localPath: string,
|
||||
fileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const buf = await readFile(localPath);
|
||||
// Wrap Node Buffer in a Blob for the SDK request body.
|
||||
const blob = new Blob([buf], { type: opts.contentType ?? "application/octet-stream" });
|
||||
|
||||
await wmill.fileUpload({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
contentType: opts.contentType,
|
||||
contentDisposition: opts.contentDisposition,
|
||||
requestBody: blob,
|
||||
});
|
||||
log.info(colors.green(`Uploaded ${localPath} -> ${fileKey}`));
|
||||
}
|
||||
|
||||
async function download(
|
||||
opts: GlobalOptions & { storage?: string; stdout?: boolean },
|
||||
fileKey: string,
|
||||
outputPath?: string
|
||||
) {
|
||||
if (opts.stdout) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// The generated request layer (cli/gen/core/request.ts:getResponseBody)
|
||||
// routes by Content-Type: binary types → Blob, text/* → string, JSON → object.
|
||||
// The generated return type is `Blob | File`, which is wrong for non-binary
|
||||
// responses, so widen to unknown before normalizing.
|
||||
const body: unknown = await wmill.fileDownload({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
let buf: Buffer;
|
||||
if (typeof body === "string") {
|
||||
buf = Buffer.from(body, "utf-8");
|
||||
} else if (body instanceof Blob) {
|
||||
buf = Buffer.from(await body.arrayBuffer());
|
||||
} else if (body instanceof ArrayBuffer) {
|
||||
buf = Buffer.from(body);
|
||||
} else if (body == null) {
|
||||
buf = Buffer.alloc(0);
|
||||
} else {
|
||||
buf = Buffer.from(JSON.stringify(body), "utf-8");
|
||||
}
|
||||
|
||||
if (opts.stdout) {
|
||||
process.stdout.write(buf);
|
||||
return;
|
||||
}
|
||||
const dest = outputPath ?? basename(fileKey);
|
||||
await writeFile(dest, buf);
|
||||
log.info(colors.green(`Downloaded ${fileKey} -> ${dest}`));
|
||||
}
|
||||
|
||||
async function del(
|
||||
opts: GlobalOptions & { storage?: string; yes?: boolean },
|
||||
fileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
if (!opts.yes) {
|
||||
const confirmed = await Confirm.prompt({
|
||||
message: `Delete '${fileKey}' from object storage${opts.storage ? ` (storage: ${opts.storage})` : ""}?`,
|
||||
default: false,
|
||||
});
|
||||
if (!confirmed) {
|
||||
log.info("Aborted.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await wmill.deleteS3File({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
log.info(colors.green(`Deleted ${fileKey}`));
|
||||
}
|
||||
|
||||
async function move(
|
||||
opts: GlobalOptions & { storage?: string },
|
||||
srcFileKey: string,
|
||||
destFileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
await wmill.moveS3File({
|
||||
workspace: workspace.workspaceId,
|
||||
srcFileKey,
|
||||
destFileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
log.info(colors.green(`Moved ${srcFileKey} -> ${destFileKey}`));
|
||||
}
|
||||
|
||||
async function info(
|
||||
opts: GlobalOptions & { json?: boolean; storage?: string },
|
||||
fileKey: string
|
||||
) {
|
||||
if (opts.json) log.setSilent(true);
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const metadata = await wmill.loadFileMetadata({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(metadata));
|
||||
return;
|
||||
}
|
||||
console.log(colors.bold("Key:") + " " + fileKey);
|
||||
console.log(colors.bold("Size:") + " " + formatBytes(metadata.size_in_bytes));
|
||||
console.log(colors.bold("Mime:") + " " + (metadata.mime_type ?? "-"));
|
||||
console.log(
|
||||
colors.bold("Last Modified:") + " " +
|
||||
(metadata.last_modified ? formatTimestamp(metadata.last_modified) : "-")
|
||||
);
|
||||
if (metadata.expires) {
|
||||
console.log(colors.bold("Expires:") + " " + formatTimestamp(metadata.expires));
|
||||
}
|
||||
if (metadata.version_id) {
|
||||
console.log(colors.bold("Version Id:") + " " + metadata.version_id);
|
||||
}
|
||||
}
|
||||
|
||||
async function preview(
|
||||
opts: GlobalOptions & {
|
||||
storage?: string;
|
||||
bytesFrom?: number;
|
||||
bytesLength?: number;
|
||||
csvSeparator?: string;
|
||||
csvHeader?: boolean;
|
||||
mime?: string;
|
||||
},
|
||||
fileKey: string
|
||||
) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
// Backend requires both byte fields; mirror the frontend's defaults
|
||||
// (frontend/src/lib/components/S3FilePickerInner.svelte) for an interactive
|
||||
// peek so the user gets useful output without passing flags.
|
||||
const result = await wmill.loadFilePreview({
|
||||
workspace: workspace.workspaceId,
|
||||
fileKey,
|
||||
storage: opts.storage,
|
||||
fileMimeType: opts.mime,
|
||||
readBytesFrom: opts.bytesFrom ?? 0,
|
||||
readBytesLength: opts.bytesLength ?? 128 * 1024,
|
||||
csvSeparator: opts.csvSeparator,
|
||||
csvHasHeader: opts.csvHeader,
|
||||
});
|
||||
|
||||
if (result.msg) {
|
||||
log.info(colors.yellow(result.msg));
|
||||
}
|
||||
if (result.content != null) {
|
||||
process.stdout.write(result.content);
|
||||
if (!result.content.endsWith("\n")) process.stdout.write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.alias("s3")
|
||||
.description("Object storage (S3) related commands. Operates on the workspace's default object storage; use --storage to target a configured secondary storage.")
|
||||
.action(listStorages as any)
|
||||
.command(
|
||||
"list",
|
||||
"List configured object storages for the workspace (default + secondary)."
|
||||
)
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.action(listStorages as any)
|
||||
.command(
|
||||
"files",
|
||||
"List files in an object storage. Optionally filter by prefix."
|
||||
)
|
||||
.alias("ls")
|
||||
.arguments("[prefix:string]")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--max-keys <maxKeys:number>", "Page size (default 100)")
|
||||
.option("--marker <marker:string>", "Pagination marker from a previous response")
|
||||
.option("--storage <storage:string>", "Secondary storage name (omit for the workspace default)")
|
||||
.action(listFiles as any)
|
||||
.command(
|
||||
"upload",
|
||||
"Upload a local file to object storage at the given file key."
|
||||
)
|
||||
.arguments("<local_path:string> <file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--content-type <contentType:string>", "Content-Type header to set on the object")
|
||||
.option("--content-disposition <contentDisposition:string>", "Content-Disposition header to set on the object")
|
||||
.action(upload as any)
|
||||
.command(
|
||||
"download",
|
||||
"Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory."
|
||||
)
|
||||
.arguments("<file_key:string> [output_path:string]")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--stdout", "Write file contents to stdout instead of a file")
|
||||
.action(download as any)
|
||||
.command(
|
||||
"delete",
|
||||
"Delete an object from object storage. Prompts for confirmation unless --yes is set."
|
||||
)
|
||||
.arguments("<file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--yes", "Skip the confirmation prompt")
|
||||
.action(del as any)
|
||||
.command(
|
||||
"move",
|
||||
"Move an object within the same storage (rename or relocate by key)."
|
||||
)
|
||||
.arguments("<src_file_key:string> <dest_file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.action(move as any)
|
||||
.command(
|
||||
"info",
|
||||
"Show metadata (size, mime, last-modified) for an object."
|
||||
)
|
||||
.arguments("<file_key:string>")
|
||||
.option("--json", "Output as JSON (for piping to jq)")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.action(info as any)
|
||||
.command(
|
||||
"preview",
|
||||
"Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files."
|
||||
)
|
||||
.arguments("<file_key:string>")
|
||||
.option("--storage <storage:string>", "Secondary storage name")
|
||||
.option("--mime <mime:string>", "Override the detected mime type (e.g. text/csv)")
|
||||
.option("--bytes-from <bytesFrom:number>", "Start offset in bytes")
|
||||
.option("--bytes-length <bytesLength:number>", "Number of bytes to read")
|
||||
.option("--csv-separator <csvSeparator:string>", "CSV column separator (default ,)")
|
||||
.option("--csv-header", "Treat the first CSV row as a header")
|
||||
.action(preview as any);
|
||||
|
||||
export default command;
|
||||
@@ -5167,7 +5167,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- \`wmill flow preview <flow_path>\` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`wmill flow preview <flow_path>\` — **default when iterating on a local flow.** Runs the local \`flow.yaml\` against local inline scripts without deploying. Add \`--remote\` to use deployed workspace scripts for PathScript steps instead of local files. Add \`--step <step_id>\` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
|
||||
- \`wmill flow run <path>\` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- \`wmill generate-metadata\` — regenerate stale \`.lock\` and \`.script.yaml\` files. By default it scans **scripts, flows, and apps** across the workspace; pass \`--skip-flows --skip-apps\` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- \`wmill sync push\` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
@@ -5184,6 +5184,12 @@ Only use \`sync push\` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### Single-step vs whole-flow preview
|
||||
|
||||
Use \`flow preview <flow_path> --step <step_id>\` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript, locally if available; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive. The step id is resolved by walking nested branchone/branchall/forloopflow/whileloopflow modules and includes the special \`preprocessor\` and \`failure\` modules.
|
||||
|
||||
Use \`flow preview <flow_path>\` (no \`--step\`) when steps depend on each other's outputs, when the user is validating the overall control flow, or when \`--step\` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (\`wmill flow preview -d '<args>'\`), which actually runs the flow and has side effects. Visual preview (the \`preview\` skill) is offered separately — see "Visual preview" below.
|
||||
@@ -6838,10 +6844,11 @@ flow related commands
|
||||
- \`flow run <path:string>\` - run a flow by path.
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step <id> to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow).
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`--step <step_id:string>\` - Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does.
|
||||
- \`flow new <flow_path:string>\` - create a new empty flow
|
||||
- \`--summary <summary:string>\` - flow summary
|
||||
- \`--description <description:string>\` - flow description
|
||||
@@ -7057,6 +7064,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- \`-w, --watch\` - Watch for file changes and re-lint automatically
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** \`s3\`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`object-storage list\` - List configured object storages for the workspace (default + secondary).
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`object-storage files [prefix:string]\` - List files in an object storage. Optionally filter by prefix.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--max-keys <maxKeys:number>\` - Page size (default 100)
|
||||
- \`--marker <marker:string>\` - Pagination marker from a previous response
|
||||
- \`--storage <storage:string>\` - Secondary storage name (omit for the workspace default)
|
||||
- \`object-storage upload <local_path:string> <file_key:string>\` - Upload a local file to object storage at the given file key.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--content-type <contentType:string>\` - Content-Type header to set on the object
|
||||
- \`--content-disposition <contentDisposition:string>\` - Content-Disposition header to set on the object
|
||||
- \`object-storage download <file_key:string> [output_path:string]\` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--stdout\` - Write file contents to stdout instead of a file
|
||||
- \`object-storage delete <file_key:string>\` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--yes\` - Skip the confirmation prompt
|
||||
- \`object-storage move <src_file_key:string> <dest_file_key:string>\` - Move an object within the same storage (rename or relocate by key).
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage info <file_key:string>\` - Show metadata (size, mime, last-modified) for an object.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage preview <file_key:string>\` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--mime <mime:string>\` - Override the detected mime type (e.g. text/csv)
|
||||
- \`--bytes-from <bytesFrom:number>\` - Start offset in bytes
|
||||
- \`--bytes-length <bytesLength:number>\` - Number of bytes to read
|
||||
- \`--csv-separator <csvSeparator:string>\` - CSV column separator (default ,)
|
||||
- \`--csv-header\` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -7393,6 +7436,26 @@ workspace related commands
|
||||
- \`--team-name <team_name:string>\` - Slack team name
|
||||
- \`workspace disconnect-slack\`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
\`wmill object-storage\` (alias \`wmill s3\`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace \`/job_helpers/*\` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **\`file_key\` is the path inside the bucket** (e.g. \`reports/2026-05/orders.csv\`), not a Windmill path. Do NOT pass \`u/...\` or \`f/...\` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **\`--storage <name>\` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use \`wmill object-storage list\` to discover configured storages.
|
||||
- **\`preview\` vs \`download\`**: \`preview\` returns a peek (CSV first rows, text content, or a byte slice via \`--bytes-from\`/\`--bytes-length\`) without writing to disk. Use \`download\` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: \`wmill object-storage files [prefix]\` (alias \`ls\`) — paginated, use \`--marker\` to continue.
|
||||
- Inspect one file: \`wmill object-storage info <file_key>\` for size/mime/last-modified, \`wmill object-storage preview <file_key>\` for content peek.
|
||||
- Move data in: \`wmill object-storage upload <local_path> <file_key>\` — set \`--content-type\` if the receiver cares (e.g. \`text/csv\`).
|
||||
- Move data out: \`wmill object-storage download <file_key> [output_path]\` — \`--stdout\` to pipe.
|
||||
- Reorganize: \`wmill object-storage move <src> <dest>\` (same storage), \`wmill object-storage delete <file_key>\` (interactive confirm unless \`--yes\`).
|
||||
`,
|
||||
"preview": `---
|
||||
name: preview
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@ import docs from "./commands/docs/docs.ts";
|
||||
import config from "./commands/config/config.ts";
|
||||
import datatable from "./commands/datatable/datatable.ts";
|
||||
import ducklake from "./commands/ducklake/ducklake.ts";
|
||||
import objectStorage from "./commands/object-storage/object-storage.ts";
|
||||
import { fetchVersion } from "./core/context.ts";
|
||||
|
||||
export {
|
||||
@@ -77,6 +78,7 @@ export {
|
||||
config,
|
||||
datatable,
|
||||
ducklake,
|
||||
objectStorage,
|
||||
hubPull,
|
||||
pull,
|
||||
push,
|
||||
@@ -87,7 +89,7 @@ export {
|
||||
token,
|
||||
};
|
||||
|
||||
export const VERSION = "1.709.0";
|
||||
export const VERSION = "1.711.0";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
@@ -210,6 +212,7 @@ const command = new Command()
|
||||
.command("config", config)
|
||||
.command("datatable", datatable)
|
||||
.command("ducklake", ducklake)
|
||||
.command("object-storage", objectStorage)
|
||||
.command("version --version", "Show version information")
|
||||
.action(async (opts: any) => {
|
||||
console.log("CLI version: " + VERSION);
|
||||
|
||||
+31
-337
@@ -1,354 +1,48 @@
|
||||
# App Mode AI Chat Review
|
||||
|
||||
## Purpose
|
||||
This note only tracks the highest-value next steps for making app-mode AI chat
|
||||
safer and more efficient.
|
||||
|
||||
This document reviews the current app-mode AI chat design with a focus on:
|
||||
## Recommended Next Steps
|
||||
|
||||
- keeping prompts and context as small as possible;
|
||||
- requiring user confirmation for important actions;
|
||||
- making datatable integration smooth and safe for users.
|
||||
1. Add confirmation for dangerous app tools.
|
||||
|
||||
## Short verdict
|
||||
Require explicit user confirmation before file writes, file deletes, backend
|
||||
runnable writes, backend runnable deletes, and datatable SQL execution. Show a
|
||||
useful diff or exact SQL before applying the action.
|
||||
|
||||
The app-mode AI chat has a solid foundation: mode-specific helpers, explicit `@` context, app snapshots/revert, datatable whitelisting, and generic confirmation UI already exist.
|
||||
2. Enforce datatable SQL safety in code.
|
||||
|
||||
However, it is not yet optimal for minimal context and user-safe automation:
|
||||
Do not rely on prompt instructions for SQL safety. Classify statements before
|
||||
execution, block DDL unless table creation is allowed, and require
|
||||
confirmation for DDL, DML, and row-returning reads that would expose data back
|
||||
to the model.
|
||||
|
||||
1. **Context is still too large by default**, especially the app system prompt, broad file-discovery guidance, full datatable schemas, and persistent `@` context. (`get_files()` has since been replaced by metadata-only `list_files()`.)
|
||||
2. **Important app/datatable actions are not consistently confirmed**. The confirmation infrastructure exists, but app tools mostly bypass it.
|
||||
3. **Datatables UX is promising but has rough edges**: stale cached table context, weak SQL safety, policy persistence issues, and too-heavy full-schema fetching.
|
||||
3. Keep default context demand-driven.
|
||||
|
||||
## Relevant files
|
||||
Prefer selected context and targeted reads before broad discovery. Keep file
|
||||
listings metadata-only, avoid sending full datatable schemas by default, and
|
||||
keep SDK/reference material out of the base prompt unless it is requested or
|
||||
needed for the task.
|
||||
|
||||
### AI chat orchestration
|
||||
4. Improve app context lifecycle.
|
||||
|
||||
- `frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts`
|
||||
- `frontend/src/lib/components/copilot/chat/chatLoop.ts`
|
||||
- `frontend/src/lib/components/copilot/chat/shared.ts`
|
||||
- `frontend/src/lib/components/copilot/chat/AIChat.svelte`
|
||||
- `frontend/src/lib/components/copilot/chat/AIChatDisplay.svelte`
|
||||
- `frontend/src/lib/components/copilot/chat/AIChatInput.svelte`
|
||||
- `frontend/src/lib/components/copilot/chat/ToolExecutionDisplay.svelte`
|
||||
Treat `@` context as per-message by default, with an explicit pinning affordance
|
||||
for context that should persist. Lazy-load file and runnable contents, and add
|
||||
a visible approximate context-size indicator so users can spot prompt bloat.
|
||||
|
||||
### App mode
|
||||
5. Refresh datatable context after mutations.
|
||||
|
||||
- `frontend/src/lib/components/copilot/chat/app/core.ts`
|
||||
- `frontend/src/lib/components/copilot/chat/AppAvailableContextList.svelte`
|
||||
- `frontend/src/lib/components/copilot/chat/ContextElementBadge.svelte`
|
||||
- `frontend/src/lib/components/copilot/chat/DatatableCreationPolicy.svelte`
|
||||
Refresh table metadata after data-panel changes and after AI-created tables so
|
||||
follow-up tool calls and user-visible context do not use stale schema data.
|
||||
|
||||
### Raw app editor and datatables
|
||||
6. Persist table creation policy explicitly.
|
||||
|
||||
- `frontend/src/lib/components/raw_apps/RawAppEditor.svelte`
|
||||
- `frontend/src/lib/components/raw_apps/RawAppDataTableList.svelte`
|
||||
- `frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte`
|
||||
- `frontend/src/lib/components/raw_apps/DefaultDatabaseSelector.svelte`
|
||||
- `frontend/src/lib/components/raw_apps/dataTableRefUtils.ts`
|
||||
- `frontend/src/lib/components/raw_apps/datatableUtils.svelte.ts`
|
||||
- `frontend/src/routes/(root)/(logged)/apps_raw/add/+page.svelte`
|
||||
- `frontend/src/routes/(root)/(logged)/apps_raw/edit/[...path]/+page.svelte`
|
||||
Store whether AI table creation is enabled as an explicit app setting instead
|
||||
of inferring it from the presence of datatable configuration.
|
||||
|
||||
### Backend datatable APIs
|
||||
7. Add focused eval coverage for these behaviors.
|
||||
|
||||
- `backend/windmill-api-workspaces/src/workspaces.rs`
|
||||
- `list_datatables`
|
||||
- `list_datatable_schemas`
|
||||
- `get_datatable_schema`
|
||||
- `edit_datatable_config`
|
||||
|
||||
### System prompts
|
||||
|
||||
- `system_prompts/README.md`
|
||||
- `system_prompts/auto-generated/index.ts`
|
||||
- `system_prompts/auto-generated/sdks/datatable-typescript.md`
|
||||
- `system_prompts/auto-generated/sdks/datatable-python.md`
|
||||
|
||||
## How app mode works today
|
||||
|
||||
In the raw app editor, `RawAppEditor.svelte` initializes app-mode AI chat on mount:
|
||||
|
||||
- calls `aiChatManager.saveAndClear()`;
|
||||
- calls `aiChatManager.changeMode(AIMode.APP)`;
|
||||
- registers app helpers through `aiChatManager.setAppHelpers(...)`.
|
||||
|
||||
Those app helpers expose operations for:
|
||||
|
||||
- frontend files;
|
||||
- backend runnables;
|
||||
- current selected editor context;
|
||||
- linting;
|
||||
- app snapshots and revert;
|
||||
- datatable schema loading;
|
||||
- SQL execution;
|
||||
- app table whitelisting.
|
||||
|
||||
When app mode is active, `AIChatManager.changeMode(AIMode.APP)` sets:
|
||||
|
||||
- system prompt: `prepareAppSystemMessage(...)`;
|
||||
- tools: `getAppTools()`;
|
||||
- helpers: `appAiChatHelpers`.
|
||||
|
||||
When the user sends a message, `prepareAppUserMessage(...)` builds the user prompt from:
|
||||
|
||||
- current frontend/backend file selection, unless excluded;
|
||||
- inspector-selected DOM element;
|
||||
- editor code selection;
|
||||
- additional `@`-mentioned context;
|
||||
- the user instructions.
|
||||
|
||||
`runChatLoop(...)` then sends the system message, history, user message, and tool definitions to the selected model. Tool calls go through `processToolCall(...)`, which supports confirmation only when a tool opts into `requiresConfirmation`.
|
||||
|
||||
## Current app tools
|
||||
|
||||
### Read and discovery tools
|
||||
|
||||
These are generally safe without confirmation:
|
||||
|
||||
- `list_files`
|
||||
- `get_frontend_file`
|
||||
- `get_backend_runnable`
|
||||
- `get_selected_context`
|
||||
- `lint`
|
||||
- `search_workspace`
|
||||
- `get_runnable_details`
|
||||
- `search_hub_scripts`
|
||||
- `list_datatables`
|
||||
- `get_datatable_table_schema`
|
||||
|
||||
### Mutating tools
|
||||
|
||||
These currently execute directly in app mode:
|
||||
|
||||
- `set_frontend_file`
|
||||
- `patch_file`
|
||||
- `delete_frontend_file`
|
||||
- `set_backend_runnable`
|
||||
- `delete_backend_runnable`
|
||||
- `exec_datatable_sql`
|
||||
|
||||
This is the biggest mismatch with the requirement that every important action should be confirmed by the user.
|
||||
|
||||
## System prompt assessment
|
||||
|
||||
The app system prompt is useful but heavier than ideal.
|
||||
|
||||
### Strengths
|
||||
|
||||
- Clearly explains raw app structure.
|
||||
- Explains the frontend/backend runnable split.
|
||||
- Encourages `patch_file` for small edits.
|
||||
- Pushes datatables for persisted app storage.
|
||||
- Explains that datatable DDL should go through `exec_datatable_sql`.
|
||||
- Includes table creation policy context.
|
||||
|
||||
### Concerns
|
||||
|
||||
1. It always includes broad app-building instructions, even for small localized edits.
|
||||
2. The previous prompt included the datatable SDK reference for both TypeScript and Python every time. This has since been removed; concise examples remain in the prompt.
|
||||
3. The previous prompt told the model to start with `get_files()`, which encouraged loading all files even when selected context was sufficient. This is now improved by `list_files()`, but the prompt still needs to stay demand-driven.
|
||||
4. It relies heavily on prompt instructions for datatable safety instead of enforcing safety in tools.
|
||||
5. Custom workspace/user prompts are appended as `USER GIVEN INSTRUCTIONS`, which is flexible but can further increase context.
|
||||
|
||||
### Recommendation
|
||||
|
||||
The base app prompt should be shorter and more demand-driven:
|
||||
|
||||
- Keep file discovery demand-driven: use selected and explicitly provided context first; call `list_files()` only when a broader metadata overview is needed.
|
||||
- Keep full SDK details out of the default prompt; concise examples are usually enough. Add an on-demand SDK reference only if it does not cause unnecessary extra tool turns.
|
||||
- Keep only minimal datatable rules in the base prompt:
|
||||
- use datatables for persistence;
|
||||
- call `list_datatables()` before schema work;
|
||||
- DDL must use `exec_datatable_sql`;
|
||||
- non-read SQL requires confirmation.
|
||||
|
||||
## Additional context assessment
|
||||
|
||||
The `@` context system is a good UX foundation.
|
||||
|
||||
App mode exposes categories for:
|
||||
|
||||
- frontend files;
|
||||
- backend runnables;
|
||||
- datatables.
|
||||
|
||||
Selecting a datatable context includes its columns and also calls `addTableToWhitelist(...)`, adding the table to the app data panel.
|
||||
|
||||
### Strengths
|
||||
|
||||
- Context is explicit and user-controllable.
|
||||
- Datatable table selection is naturally integrated into the chat input.
|
||||
- Selected app file/runnable chips are visible and can be excluded.
|
||||
- Inspector and code-selection context are compact and useful.
|
||||
|
||||
### Concerns
|
||||
|
||||
1. `@` context persists across messages until manually removed, which can silently bloat follow-up prompts.
|
||||
2. Available app context currently includes file contents/runnable configs in memory before selection.
|
||||
3. Each selected context item is truncated, but there is no overall context budget indicator.
|
||||
4. Current file/runnable selection is included by default unless excluded, which is convenient but not minimal.
|
||||
|
||||
### Recommendation
|
||||
|
||||
- Make app `@` context per-message by default.
|
||||
- Add an explicit “pin” option for context that should persist across messages.
|
||||
- Lazy-load file/runnable content when selected or when a message is sent.
|
||||
- Show an approximate context-size/token budget indicator.
|
||||
- Prefer sending path/name and selected code first; fetch full files only when necessary.
|
||||
|
||||
## Confirmation assessment
|
||||
|
||||
The generic confirmation mechanism already exists:
|
||||
|
||||
- `processToolCall(...)` checks `tool.requiresConfirmation`.
|
||||
- `ToolExecutionDisplay.svelte` renders Run/Cancel controls.
|
||||
- Script test runs, flow test runs, and mutating API calls already use confirmation.
|
||||
|
||||
App mode should use the same infrastructure for important actions.
|
||||
|
||||
### Suggested confirmation policy
|
||||
|
||||
#### No confirmation required
|
||||
|
||||
- `list_files`, as a metadata-only response;
|
||||
- `get_frontend_file`;
|
||||
- `get_backend_runnable`;
|
||||
- `get_selected_context`;
|
||||
- `list_datatables`, as table-name metadata only;
|
||||
- `get_datatable_table_schema`, as a targeted schema read;
|
||||
- `lint`;
|
||||
- search tools.
|
||||
|
||||
#### Confirmation required
|
||||
|
||||
- `set_frontend_file`;
|
||||
- `patch_file`;
|
||||
- `delete_frontend_file`;
|
||||
- `set_backend_runnable`;
|
||||
- `delete_backend_runnable`;
|
||||
- `exec_datatable_sql` for any DDL or DML;
|
||||
- `exec_datatable_sql` for `SELECT` if it returns real row data that will be sent back to the model.
|
||||
|
||||
### Recommended UX
|
||||
|
||||
For files/runnables:
|
||||
|
||||
- Prefer batched proposed edits.
|
||||
- Show a diff.
|
||||
- Let the user click “Apply changes”.
|
||||
- Run lint after applying.
|
||||
|
||||
For SQL:
|
||||
|
||||
- Show the exact SQL.
|
||||
- Classify the query as:
|
||||
- schema read;
|
||||
- data read;
|
||||
- insert/update/delete;
|
||||
- DDL.
|
||||
- Require confirmation before data reads and all mutations.
|
||||
- For table creation, require both:
|
||||
- table creation policy enabled;
|
||||
- explicit confirmation of the `CREATE TABLE` SQL.
|
||||
|
||||
## Datatables integration assessment
|
||||
|
||||
The datatable integration is directionally good and already has several strong user-facing pieces.
|
||||
|
||||
### Current strengths
|
||||
|
||||
The new app setup lets the user choose:
|
||||
|
||||
- default datatable;
|
||||
- schema mode: none, new, existing;
|
||||
- whether AI can create tables;
|
||||
- pre-whitelisted existing tables.
|
||||
|
||||
The raw app data panel lets users:
|
||||
|
||||
- add datatable table references;
|
||||
- inspect tables through the DB manager drawer;
|
||||
- configure the default datatable/schema for new tables.
|
||||
|
||||
The AI chat integration lets users:
|
||||
|
||||
- mention datatable tables through `@` context;
|
||||
- add mentioned tables to the app whitelist;
|
||||
- list datatable/schema/table names with `list_datatables()`;
|
||||
- retrieve one table's columns with `get_datatable_table_schema()`;
|
||||
- create tables through `exec_datatable_sql(..., new_table)`.
|
||||
|
||||
### Concerns
|
||||
|
||||
1. **`exec_datatable_sql` is too powerful without confirmation.**
|
||||
It can run `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`, etc.
|
||||
|
||||
2. **Table creation policy is not fully enforced in code.**
|
||||
The tool blocks `new_table` when policy is disabled, but it does not block DDL if the model omits `new_table`.
|
||||
|
||||
3. **Table creation disabled state may not persist cleanly.**
|
||||
`RawAppData` stores `datatable` and `schema`, but not an explicit `enabled` value. `RawAppEditor` infers enabled from `data.datatable !== undefined`, which can re-enable table creation after reopening.
|
||||
|
||||
4. **Datatable context cache can become stale.**
|
||||
`AIChatManager.refreshDatatables()` runs when app helpers are set, but may not refresh immediately after data panel changes or after AI creates a new table.
|
||||
|
||||
5. **Full schema loading can still be too expensive internally.**
|
||||
`list_datatables()` and `get_datatable_table_schema()` reduce what is sent to the model, but they still currently rely on app helpers that fetch full schema data before filtering.
|
||||
|
||||
6. **Auto-whitelisting from `@table` is convenient but silent.**
|
||||
It mutates app data without an obvious confirmation or undo affordance.
|
||||
|
||||
### Recommended datatable tool design
|
||||
|
||||
Instead of one broad schema tool and one unrestricted SQL tool, prefer smaller tools:
|
||||
|
||||
- `list_datatables()`
|
||||
- `list_datatable_tables(datatable, schema?, search?)` (optional backend/API optimization if table lists need server-side filtering)
|
||||
- `get_datatable_table_schema(datatable, schema, table)`
|
||||
- `preview_datatable_rows(datatable, schema, table, limit)` with confirmation
|
||||
- `execute_datatable_sql(datatable, sql)` with query classification and confirmation
|
||||
- `create_datatable_table(datatable, schema, table, columns)` as a structured safe path for table creation
|
||||
|
||||
## Priority recommendations
|
||||
|
||||
1. **Add confirmation to dangerous app tools**
|
||||
- file/runnable writes;
|
||||
- file/runnable deletes;
|
||||
- datatable SQL;
|
||||
- especially DDL/DML.
|
||||
|
||||
2. **Enforce SQL safety in code, not only in prompts**
|
||||
- block DDL unless `new_table` is provided and policy allows it;
|
||||
- confirm all non-`SELECT` statements;
|
||||
- consider confirming `SELECT` row reads too.
|
||||
|
||||
3. **Reduce default prompt/tool context**
|
||||
- keep `list_files()` metadata-only and demand-driven;
|
||||
- use selected context first;
|
||||
- keep full SDK references out of the default prompt;
|
||||
- keep datatable tools split into smaller schema/table lookups.
|
||||
|
||||
4. **Refresh datatable context reliably**
|
||||
- refresh after data panel changes;
|
||||
- refresh after `exec_datatable_sql(..., new_table)`;
|
||||
- remove debug logging from datatable refresh.
|
||||
|
||||
5. **Persist table creation policy explicitly**
|
||||
- store a boolean such as `tableCreationEnabled` in raw app data;
|
||||
- do not infer enabled solely from `data.datatable`.
|
||||
|
||||
6. **Improve `@` context lifecycle**
|
||||
- make app `@` context per-message by default;
|
||||
- add pinning for persistent context;
|
||||
- lazy-load file/runnable contents;
|
||||
- show approximate context size.
|
||||
|
||||
## Overall opinion
|
||||
|
||||
The current architecture is good and extensible, but it should become more demand-driven and safer before being considered efficient and user-safe.
|
||||
|
||||
The highest-impact changes are:
|
||||
|
||||
- add confirmation for app mutations and datatable SQL;
|
||||
- enforce datatable SQL policy programmatically;
|
||||
- reduce the app system prompt and avoid automatic broad context loading;
|
||||
- split datatable schema access into smaller, targeted tools.
|
||||
Cover confirmation requirements, datatable SQL policy enforcement, selected
|
||||
context minimization, and stale-schema refresh behavior with targeted app-mode
|
||||
evals or lower-level tests where practical.
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
# App Mode AI Chat Token Baseline
|
||||
|
||||
This baseline was collected before optimizing app-mode context/prompt/datatable behavior.
|
||||
|
||||
> Note: The historical commands/results below include `app-token-selected-large-frontend-context` and `app-token-selected-large-backend-context`. Those cases were removed from the active eval suite because `runtime.appContext.selected` only verified that the file/runnable existed and did not serialize a selected file/runnable hint to the model. Future selected-file/runnable coverage should be reintroduced through the app context manager path.
|
||||
|
||||
## Command
|
||||
|
||||
Secrets were loaded from `~/windmill/ai_evals/.env` without printing them.
|
||||
|
||||
```bash
|
||||
cd ai_evals
|
||||
set -a
|
||||
source ~/windmill/ai_evals/.env
|
||||
set +a
|
||||
bun run cli -- run app \
|
||||
app-token-baseline-large-app-small-edit \
|
||||
app-token-selected-large-frontend-context \
|
||||
app-token-selected-large-backend-context \
|
||||
app-token-many-datatable-context \
|
||||
app-token-large-datatable-discovery \
|
||||
--model haiku \
|
||||
--runs 1 \
|
||||
--output results/app-token-baseline-current-max8.json
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
- Mode: `app`
|
||||
- Model under test: `anthropic:claude-haiku-4-5-20251001`
|
||||
- Transport: `direct`
|
||||
- Judge model: `claude-sonnet-4-6`
|
||||
- Runs per case: `1`
|
||||
- Token-heavy app cases use `runtime.maxTurns: 8`
|
||||
|
||||
## Results
|
||||
|
||||
Pass rate: **100% (5/5)**
|
||||
|
||||
| Case | Prompt tokens | Completion tokens | Total tokens | Tool calls | Tools used |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| `app-token-baseline-large-app-small-edit` | 73,682 | 519 | 74,201 | 4 | `get_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-selected-large-frontend-context` | 36,305 | 348 | 36,653 | 2 | `get_frontend_file`, `patch_file` |
|
||||
| `app-token-selected-large-backend-context` | 95,232 | 19,633 | 114,865 | 4 | `set_backend_runnable`, `get_backend_runnable` |
|
||||
| `app-token-many-datatable-context` | 35,204 | 404 | 35,608 | 2 | `get_files`, `patch_file` |
|
||||
| `app-token-large-datatable-discovery` | 114,964 | 4,047 | 119,011 | 7 | `get_files`, `get_datatables`, `set_backend_runnable`, `set_frontend_file`, `patch_file`, `lint` |
|
||||
|
||||
Aggregate token usage:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalTokenUsage": {
|
||||
"prompt": 355387,
|
||||
"completion": 24951,
|
||||
"total": 380338
|
||||
},
|
||||
"averageTokenUsagePerAttempt": {
|
||||
"prompt": 71077.4,
|
||||
"completion": 4990.2,
|
||||
"total": 76067.6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
The highest-token cases are:
|
||||
|
||||
1. `app-token-large-datatable-discovery` — full datatable discovery with `get_datatables()` and app edits reached **119,011** total tokens.
|
||||
2. `app-token-selected-large-backend-context` — selected large backend runnable plus a rewrite-style tool call reached **114,865** total tokens.
|
||||
3. `app-token-baseline-large-app-small-edit` — a trivial heading edit still reached **74,201** total tokens, largely due broad file discovery.
|
||||
|
||||
These cases should be rerun after prompt/context/tool changes to compare total and prompt-token reductions.
|
||||
|
||||
## Follow-up: metadata-only `list_files`
|
||||
|
||||
The contentful `get_files` app-mode tool was replaced with `list_files` to make broad app discovery cheaper and less sticky in chat history.
|
||||
|
||||
Changes:
|
||||
|
||||
- Renamed the overview tool from `get_files` to `list_files`.
|
||||
- Changed the overview response from truncated source/config contents to metadata only.
|
||||
- `list_files` returns:
|
||||
- frontend files: `path`, character `size`, and file `kind`;
|
||||
- backend runnables: `key`, `name`, `type`, and lightweight optional metadata such as `path`, `language`, `contentSize`, and `staticInputKeys`.
|
||||
- Updated app-mode prompt guidance so the model no longer starts every task with broad file discovery.
|
||||
- Kept targeted content tools as the path for inspection:
|
||||
- `get_frontend_file(path)` for frontend source;
|
||||
- `get_backend_runnable(key)` for runnable configuration/source.
|
||||
|
||||
The same five cases were rerun with:
|
||||
|
||||
```bash
|
||||
cd ai_evals
|
||||
set -a
|
||||
source ~/windmill/ai_evals/.env
|
||||
set +a
|
||||
bun run cli -- run app \
|
||||
app-token-baseline-large-app-small-edit \
|
||||
app-token-selected-large-frontend-context \
|
||||
app-token-selected-large-backend-context \
|
||||
app-token-many-datatable-context \
|
||||
app-token-large-datatable-discovery \
|
||||
--model haiku \
|
||||
--runs 1 \
|
||||
--output results/app-token-after-list-files.json
|
||||
```
|
||||
|
||||
Pass rate: **100% (5/5)**
|
||||
|
||||
| Case | Prompt tokens | Completion tokens | Total tokens | Tool calls | Tools used |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| `app-token-baseline-large-app-small-edit` | 41,020 | 422 | 41,442 | 3 | `list_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-selected-large-frontend-context` | 41,020 | 422 | 41,442 | 3 | `list_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-selected-large-backend-context` | 53,511 | 9,714 | 63,225 | 3 | `list_files`, `get_backend_runnable`, `set_backend_runnable` |
|
||||
| `app-token-many-datatable-context` | 46,990 | 475 | 47,465 | 3 | `list_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-large-datatable-discovery` | 131,607 | 5,084 | 136,691 | 8 | `get_datatables`, `list_files`, `set_backend_runnable`, `set_frontend_file`, `patch_file`, `lint` |
|
||||
|
||||
Aggregate token usage:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalTokenUsage": {
|
||||
"prompt": 314148,
|
||||
"completion": 16117,
|
||||
"total": 330265
|
||||
},
|
||||
"averageTokenUsagePerAttempt": {
|
||||
"prompt": 62829.6,
|
||||
"completion": 3223.4,
|
||||
"total": 66053
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Comparison against the post-rebase / PR #8922 run (`results/app-token-after-origin-main-pr8922.json`):
|
||||
|
||||
| Case | PR #8922 total | `list_files` total | Delta | Delta % | Prompt delta |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| `app-token-baseline-large-app-small-edit` | 74,061 | 41,442 | -32,619 | -44.0% | -32,522 |
|
||||
| `app-token-selected-large-frontend-context` | 74,061 | 41,442 | -32,619 | -44.0% | -32,522 |
|
||||
| `app-token-selected-large-backend-context` | 71,050 | 63,225 | -7,825 | -11.0% | -7,787 |
|
||||
| `app-token-many-datatable-context` | 35,497 | 47,465 | +11,968 | +33.7% | +11,886 |
|
||||
| `app-token-large-datatable-discovery` | 97,128 | 136,691 | +39,563 | +40.7% | +38,295 |
|
||||
|
||||
Aggregate comparison against the post-rebase / PR #8922 run:
|
||||
|
||||
| Metric | PR #8922 | `list_files` | Delta | Delta % |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Prompt tokens | 336,798 | 314,148 | -22,650 | -6.7% |
|
||||
| Completion tokens | 14,999 | 16,117 | +1,118 | +7.5% |
|
||||
| Total tokens | 351,797 | 330,265 | -21,532 | -6.1% |
|
||||
|
||||
Compared to the original baseline above, the `list_files` run is **-50,073 total tokens** (**-13.2% total**).
|
||||
|
||||
Interpretation:
|
||||
|
||||
- The small edit and selected-frontend cases improved substantially because broad discovery no longer injects truncated contents for the whole app.
|
||||
- The selected-backend case also improved, despite still needing targeted runnable inspection.
|
||||
- The datatable-context cases can require an extra `get_frontend_file` after `list_files`, so the small datatable edit regressed in this single-run sample.
|
||||
- The large datatable case remains dominated by datatable/schema prompt bloat and model variability; moving datatable SDK/reference and schema discovery behind smaller on-demand tools is still the next likely high-impact optimization.
|
||||
|
||||
## Follow-up: targeted datatable tools and shorter datatable prompt
|
||||
|
||||
The next pass reduced default datatable context by making datatable discovery metadata-first and removing the full datatable SDK reference from the system prompt.
|
||||
|
||||
Changes:
|
||||
|
||||
- Replaced the broad schema discovery tool with `list_datatables()` for datatable/schema/table names only.
|
||||
- Added `get_datatable_table_schema(datatable_name, schema_name, table_name)` for targeted column lookup when column names/types are actually needed.
|
||||
- Removed the full TypeScript + Python datatable SDK reference from the default app system prompt.
|
||||
- Kept concise TypeScript and Python datatable examples in the prompt, which were enough for the benchmark cases.
|
||||
- Strengthened prompt/tool guidance so table-list dashboards use `list_datatables()` directly and avoid schema/SDK lookups unless needed.
|
||||
|
||||
The same five cases were rerun with:
|
||||
|
||||
```bash
|
||||
cd ai_evals
|
||||
set -a
|
||||
source ~/windmill/ai_evals/.env
|
||||
set +a
|
||||
bun run cli -- run app \
|
||||
app-token-baseline-large-app-small-edit \
|
||||
app-token-selected-large-frontend-context \
|
||||
app-token-selected-large-backend-context \
|
||||
app-token-many-datatable-context \
|
||||
app-token-large-datatable-discovery \
|
||||
--model haiku \
|
||||
--runs 1 \
|
||||
--output results/app-token-after-datatable-tools-v3.json
|
||||
```
|
||||
|
||||
Pass rate: **100% (5/5)**
|
||||
|
||||
| Case | Prompt tokens | Completion tokens | Total tokens | Tool calls | Tools used |
|
||||
|---|---:|---:|---:|---:|---|
|
||||
| `app-token-baseline-large-app-small-edit` | 37,516 | 425 | 37,941 | 3 | `list_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-selected-large-frontend-context` | 37,516 | 358 | 37,874 | 3 | `list_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-selected-large-backend-context` | 49,995 | 9,708 | 59,703 | 3 | `list_files`, `get_backend_runnable`, `set_backend_runnable` |
|
||||
| `app-token-many-datatable-context` | 43,493 | 536 | 44,029 | 3 | `list_files`, `get_frontend_file`, `patch_file` |
|
||||
| `app-token-large-datatable-discovery` | 24,193 | 2,043 | 26,236 | 4 | `list_datatables`, `list_files`, `get_frontend_file`, `set_frontend_file` |
|
||||
|
||||
Aggregate token usage:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalTokenUsage": {
|
||||
"prompt": 192713,
|
||||
"completion": 13070,
|
||||
"total": 205783
|
||||
},
|
||||
"averageTokenUsagePerAttempt": {
|
||||
"prompt": 38542.6,
|
||||
"completion": 2614,
|
||||
"total": 41156.6
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Comparison against the metadata-only `list_files` run (`results/app-token-after-list-files.json`):
|
||||
|
||||
| Case | `list_files` total | Datatable-tools total | Delta | Delta % | Prompt delta |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| `app-token-baseline-large-app-small-edit` | 41,442 | 37,941 | -3,501 | -8.4% | -3,504 |
|
||||
| `app-token-selected-large-frontend-context` | 41,442 | 37,874 | -3,568 | -8.6% | -3,504 |
|
||||
| `app-token-selected-large-backend-context` | 63,225 | 59,703 | -3,522 | -5.6% | -3,516 |
|
||||
| `app-token-many-datatable-context` | 47,465 | 44,029 | -3,436 | -7.2% | -3,497 |
|
||||
| `app-token-large-datatable-discovery` | 136,691 | 26,236 | -110,455 | -80.8% | -107,414 |
|
||||
|
||||
Aggregate comparison:
|
||||
|
||||
| Metric | `list_files` | Datatable tools | Delta | Delta % |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Prompt tokens | 314,148 | 192,713 | -121,435 | -38.7% |
|
||||
| Completion tokens | 16,117 | 13,070 | -3,047 | -18.9% |
|
||||
| Total tokens | 330,265 | 205,783 | -124,482 | -37.7% |
|
||||
|
||||
Compared to the post-rebase / PR #8922 run, the datatable-tools run is **-146,014 total tokens** (**-41.5% total**). Compared to the original baseline above, it is **-174,555 total tokens** (**-45.9% total**).
|
||||
|
||||
Interpretation:
|
||||
|
||||
- Removing the full datatable SDK reference from the default prompt saved about 3.5k prompt tokens in every case.
|
||||
- The large datatable discovery case improved dramatically because the model used `list_datatables()` table-name metadata instead of loading full schemas.
|
||||
- The small datatable-context edit is still higher than the post-rebase / PR #8922 run because selected file identifiers are not yet injected, so the model still discovers and reads `/index.tsx` before patching.
|
||||
- A future context-manager-backed selected file/runnable flow should add cheap selected identifiers when that UX is ready, so selected-file tasks can skip `list_files()` without reintroducing implicit source-content bloat.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Failing Tests
|
||||
|
||||
This file tracks benchmark cases that still fail or need follow-up validation.
|
||||
|
||||
## Flow
|
||||
|
||||
- `flow-test6-ai-agent-tools`
|
||||
Latest failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow`
|
||||
Issues:
|
||||
final output does not include the actions or tool-result details the prompt asks for
|
||||
`open_support_ticket` contains a syntax bug
|
||||
|
||||
- `flow-test7-simple-modification`
|
||||
Latest failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow`
|
||||
Issues:
|
||||
`validate_data` was added, but the failure behavior still does not match the requested contract
|
||||
`save_results` throws instead of returning a graceful structured result
|
||||
|
||||
- `flow-test11-preprocessor-and-failure-handler`
|
||||
Latest failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow`
|
||||
Issues:
|
||||
the model creates regular `preprocessor` and `failure` modules
|
||||
it does not use Windmill's special top-level `preprocessor_module` and `failure_module`
|
||||
|
||||
## Needs Reconfirmation
|
||||
|
||||
- `flow-test4-order-processing-loop`
|
||||
Full-suite failing run: `ai_evals/results/2026-04-09T11-25-24.107Z__flow`
|
||||
Follow-up passing run after prompt improvement: `ai_evals/results/2026-04-09T13-29-15.877Z__flow`
|
||||
Note:
|
||||
this case failed on invalid `branchone` downstream result access
|
||||
it passed after adding explicit branch-output guidance to the flow prompt
|
||||
rerun the full flow suite to confirm the fix holds in the broader benchmark
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,140 +0,0 @@
|
||||
# System Prompt Testing Status
|
||||
|
||||
This document describes the benchmark tool that exists today. It is the current
|
||||
truth for `ai_evals/`.
|
||||
|
||||
The longer planning document in
|
||||
[system-prompt-testing-plan.md](/home/farhad/windmill__worktrees/prompt-testing-plan/docs/system-prompt-testing-plan.md)
|
||||
still contains useful background, but parts of its workflow are now historical
|
||||
because the old variants/history system was removed.
|
||||
|
||||
## Current Tool
|
||||
|
||||
There is one repo-level benchmark CLI under `ai_evals/` with three commands:
|
||||
|
||||
- `bun run cli -- models`
|
||||
- `bun run cli -- cases [mode]`
|
||||
- `bun run cli -- run <mode> [caseIds...]`
|
||||
|
||||
Supported modes:
|
||||
|
||||
- `cli`
|
||||
- `flow`
|
||||
- `script`
|
||||
- `app`
|
||||
|
||||
Public `run` options:
|
||||
|
||||
- `--runs <n>`
|
||||
- `--output <path>`
|
||||
- `--model <alias>`
|
||||
- `--verbose`
|
||||
- `--record`
|
||||
|
||||
There is no variant workflow and no compare command in the current tool.
|
||||
Tracked history is intentionally minimal: `run --record` appends one compact
|
||||
summary line to `ai_evals/history/<mode>.jsonl`. This is only allowed for
|
||||
full-suite runs, not selected case ids. History lines include average token
|
||||
usage when the benchmark mode reports it, plus average judge score and per-case
|
||||
duration/judge/token usage summaries.
|
||||
|
||||
## How It Works
|
||||
|
||||
Each attempt runs:
|
||||
|
||||
1. the current production prompts, tools, and guidance from this checkout
|
||||
2. deterministic validation
|
||||
3. LLM judging
|
||||
|
||||
Results are written locally under `ai_evals/results/` as:
|
||||
|
||||
- a summary JSON file
|
||||
- a sibling artifacts directory containing the generated flow/script/app/workspace
|
||||
|
||||
If `--record` is used, the CLI also appends a compact JSONL summary line to the
|
||||
tracked file for that mode under `ai_evals/history/`.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
- `ai_evals/cases/`: one YAML manifest per mode
|
||||
- `ai_evals/fixtures/`: initial and expected fixtures
|
||||
- `ai_evals/core/`: shared case loading, model resolution, validation, judging, and result writing
|
||||
- `ai_evals/history/`: optional tracked pass-rate history written by `run --record`, one JSONL file per mode
|
||||
- `ai_evals/modes/`: one runner per mode
|
||||
|
||||
Execution model:
|
||||
|
||||
- `flow`, `script`, and `app` reuse the production frontend chat loop and production tool definitions through the frontend Vitest bridge
|
||||
- `cli` creates a temp workspace, writes the current checkout guidance into it, and runs the Anthropic agent SDK against that workspace
|
||||
|
||||
## Case Model
|
||||
|
||||
Each case is intentionally small:
|
||||
|
||||
- `prompt`
|
||||
- optional `initial`
|
||||
- optional `expected`
|
||||
- optional `validate`
|
||||
- optional `cliExpect`
|
||||
|
||||
`validate` is mainly used for stronger deterministic checks where exact fixture
|
||||
matching would be too strict, especially for `flow` creation cases.
|
||||
|
||||
`cliExpect` is used by CLI-mode cases to assert agent behavior deterministically,
|
||||
including:
|
||||
|
||||
- required or forbidden skills
|
||||
- skills invoked before the first file mutation
|
||||
- ordered `wmill` command proposals in the assistant response
|
||||
- forbidden attempted `wmill` executions
|
||||
- read-only guidance cases where the workspace must stay unchanged
|
||||
|
||||
Examples of current deterministic checks:
|
||||
|
||||
- schema contains one of several accepted input shapes
|
||||
- `results.*` references resolve
|
||||
- required code/input characteristics exist in some module
|
||||
- expected workspace files are created in `cli` mode
|
||||
- expected CLI skills and proposed `wmill` commands are observed in `cli` mode
|
||||
|
||||
## Model Selection
|
||||
|
||||
Model aliases are resolved through a shared registry in `ai_evals/core/models.ts`.
|
||||
|
||||
Current aliases:
|
||||
|
||||
- `haiku`
|
||||
- `sonnet`
|
||||
- `opus`
|
||||
- `4o`
|
||||
|
||||
Notes:
|
||||
|
||||
- the `models` command also shows accepted alias spellings such as `gpt-4o` and `claude-opus-4.6`
|
||||
- frontend modes can use Anthropic and OpenAI-backed aliases
|
||||
- `cli` mode is Anthropic-only because it runs through the Anthropic agent SDK
|
||||
- the judge model is separate and currently defaults to `claude-sonnet-4-6`
|
||||
|
||||
## What Is Working Well
|
||||
|
||||
- one simple local benchmark CLI
|
||||
- real production execution paths instead of synthetic prompt variants
|
||||
- local result and artifact persistence by default
|
||||
- live frontend progress output
|
||||
- reusable flow/script/app/cli runners under one tool
|
||||
- deterministic validation can now catch real runtime-invalid flow wiring
|
||||
|
||||
## What Still Needs Work
|
||||
|
||||
- broader case coverage across all four modes
|
||||
- stronger deterministic validators for more cases, especially app/script semantics
|
||||
- clearer per-case validation metadata as the corpus grows
|
||||
- CI automation for smoke and nightly runs
|
||||
|
||||
## Recommended Next Focus
|
||||
|
||||
The next high-value work is:
|
||||
|
||||
1. add more realistic benchmark cases
|
||||
2. keep simplifying deterministic validators so they check correctness, not one exact implementation
|
||||
3. add CI only after the local benchmark signal is trustworthy
|
||||
Generated
+50
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.709.0",
|
||||
"version": "1.711.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.709.0",
|
||||
"version": "1.711.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -846,6 +846,7 @@
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -857,6 +858,7 @@
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -867,6 +869,7 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1504,6 +1508,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1520,6 +1525,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1536,6 +1542,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1552,6 +1559,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1568,6 +1576,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1584,6 +1593,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1600,6 +1610,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1616,6 +1627,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1632,6 +1644,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1648,6 +1661,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1664,6 +1678,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1680,6 +1695,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1696,6 +1712,7 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1714,6 +1731,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1730,6 +1748,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2035,6 +2054,7 @@
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6810,7 +6830,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -7309,6 +7329,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7329,6 +7350,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7349,6 +7371,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7369,6 +7392,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7389,6 +7413,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7409,6 +7434,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7429,6 +7455,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7449,6 +7476,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7469,6 +7497,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7489,6 +7518,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7509,6 +7539,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12077,6 +12108,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -12807,7 +12853,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.709.0",
|
||||
"version": "1.711.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
|
||||
|
||||
@@ -428,7 +428,7 @@
|
||||
inputBaseClass,
|
||||
inputBorderClass({ error: !!error }),
|
||||
inputSizeClasses[size],
|
||||
'relative flex gap-0 pb-0 mb-1 flex-col flex-wrap sm:flex-row sm:items-center',
|
||||
'relative flex gap-0 pb-0 mb-1 flex-wrap flex-row items-center',
|
||||
disabled && '!bg-surface-disabled cursor-not-allowed border-none'
|
||||
)}
|
||||
>
|
||||
@@ -486,7 +486,7 @@
|
||||
/>
|
||||
</label>
|
||||
{:else if meta.ownerKind === 'folder'}
|
||||
<label class="block grow w-42">
|
||||
<label class="block grow">
|
||||
<FolderPicker
|
||||
bind:folderName={meta.owner}
|
||||
{initialPath}
|
||||
@@ -500,7 +500,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-sm text-secondary">/</div>
|
||||
<label class="block grow min-w-32 mr-3">
|
||||
<label class="block grow mr-3">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<PathNameAutocomplete
|
||||
bind:this={inputP}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { buildWsUrl } from '$lib/wsUrl'
|
||||
import { paneMinPercent } from '$lib/utils/splitpaneSizing'
|
||||
import { processSecretArgs } from './secretArgUtils'
|
||||
import type { Schema, SupportedLanguage } from '$lib/common'
|
||||
import {
|
||||
@@ -1432,9 +1433,7 @@
|
||||
const splitAxisExtent = $derived(
|
||||
previewLayout === 'bottom' ? splitContainerHeight : splitContainerWidth
|
||||
)
|
||||
const testPaneMinPercent = $derived(
|
||||
splitAxisExtent > 0 ? Math.min(80, (testPaneMinPx / splitAxisExtent) * 100) : 0
|
||||
)
|
||||
const testPaneMinPercent = $derived(paneMinPercent(splitAxisExtent, testPaneMinPx))
|
||||
|
||||
// Raw user-controlled test size (what the splitter wrote, or what the
|
||||
// toggle set). The size we actually pass to <Pane> is clamped to the
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.709.0"
|
||||
wmill = ">=1.711.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.709.0
|
||||
version: 1.711.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.709.0'
|
||||
ModuleVersion = '1.711.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.709.0"
|
||||
version = "1.711.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -148,10 +148,11 @@ flow related commands
|
||||
- `flow run <path:string>` - run a flow by path.
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step <id> to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow).
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `--step <step_id:string>` - Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does.
|
||||
- `flow new <flow_path:string>` - create a new empty flow
|
||||
- `--summary <summary:string>` - flow summary
|
||||
- `--description <description:string>` - flow description
|
||||
@@ -367,6 +368,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- `-w, --watch` - Watch for file changes and re-lint automatically
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** `s3`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- `object-storage list` - List configured object storages for the workspace (default + secondary).
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `object-storage files [prefix:string]` - List files in an object storage. Optionally filter by prefix.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--max-keys <maxKeys:number>` - Page size (default 100)
|
||||
- `--marker <marker:string>` - Pagination marker from a previous response
|
||||
- `--storage <storage:string>` - Secondary storage name (omit for the workspace default)
|
||||
- `object-storage upload <local_path:string> <file_key:string>` - Upload a local file to object storage at the given file key.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--content-type <contentType:string>` - Content-Type header to set on the object
|
||||
- `--content-disposition <contentDisposition:string>` - Content-Disposition header to set on the object
|
||||
- `object-storage download <file_key:string> [output_path:string]` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--stdout` - Write file contents to stdout instead of a file
|
||||
- `object-storage delete <file_key:string>` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--yes` - Skip the confirmation prompt
|
||||
- `object-storage move <src_file_key:string> <dest_file_key:string>` - Move an object within the same storage (rename or relocate by key).
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage info <file_key:string>` - Show metadata (size, mime, last-modified) for an object.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage preview <file_key:string>` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--mime <mime:string>` - Override the detected mime type (e.g. text/csv)
|
||||
- `--bytes-from <bytesFrom:number>` - Start offset in bytes
|
||||
- `--bytes-length <bytesLength:number>` - Number of bytes to read
|
||||
- `--csv-separator <csvSeparator:string>` - CSV column separator (default ,)
|
||||
- `--csv-header` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -703,3 +740,23 @@ workspace related commands
|
||||
- `--team-name <team_name:string>` - Slack team name
|
||||
- `workspace disconnect-slack`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
`wmill object-storage` (alias `wmill s3`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace `/job_helpers/*` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **`file_key` is the path inside the bucket** (e.g. `reports/2026-05/orders.csv`), not a Windmill path. Do NOT pass `u/...` or `f/...` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **`--storage <name>` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use `wmill object-storage list` to discover configured storages.
|
||||
- **`preview` vs `download`**: `preview` returns a peek (CSV first rows, text content, or a byte slice via `--bytes-from`/`--bytes-length`) without writing to disk. Use `download` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: `wmill object-storage files [prefix]` (alias `ls`) — paginated, use `--marker` to continue.
|
||||
- Inspect one file: `wmill object-storage info <file_key>` for size/mime/last-modified, `wmill object-storage preview <file_key>` for content peek.
|
||||
- Move data in: `wmill object-storage upload <local_path> <file_key>` — set `--content-type` if the receiver cares (e.g. `text/csv`).
|
||||
- Move data out: `wmill object-storage download <file_key> [output_path]` — `--stdout` to pipe.
|
||||
- Reorganize: `wmill object-storage move <src> <dest>` (same storage), `wmill object-storage delete <file_key>` (interactive confirm unless `--yes`).
|
||||
|
||||
@@ -2698,10 +2698,11 @@ flow related commands
|
||||
- \`flow run <path:string>\` - run a flow by path.
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- \`flow preview <flow_path:string>\` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step <id> to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow).
|
||||
- \`-d --data <data:string>\` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- \`-s --silent\` - Do not output anything other then the final output. Useful for scripting.
|
||||
- \`--remote\` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- \`--step <step_id:string>\` - Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does.
|
||||
- \`flow new <flow_path:string>\` - create a new empty flow
|
||||
- \`--summary <summary:string>\` - flow summary
|
||||
- \`--description <description:string>\` - flow description
|
||||
@@ -2917,6 +2918,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- \`--locks-required\` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- \`-w, --watch\` - Watch for file changes and re-lint automatically
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** \`s3\`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- \`object-storage list\` - List configured object storages for the workspace (default + secondary).
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`object-storage files [prefix:string]\` - List files in an object storage. Optionally filter by prefix.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--max-keys <maxKeys:number>\` - Page size (default 100)
|
||||
- \`--marker <marker:string>\` - Pagination marker from a previous response
|
||||
- \`--storage <storage:string>\` - Secondary storage name (omit for the workspace default)
|
||||
- \`object-storage upload <local_path:string> <file_key:string>\` - Upload a local file to object storage at the given file key.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--content-type <contentType:string>\` - Content-Type header to set on the object
|
||||
- \`--content-disposition <contentDisposition:string>\` - Content-Disposition header to set on the object
|
||||
- \`object-storage download <file_key:string> [output_path:string]\` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--stdout\` - Write file contents to stdout instead of a file
|
||||
- \`object-storage delete <file_key:string>\` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--yes\` - Skip the confirmation prompt
|
||||
- \`object-storage move <src_file_key:string> <dest_file_key:string>\` - Move an object within the same storage (rename or relocate by key).
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage info <file_key:string>\` - Show metadata (size, mime, last-modified) for an object.
|
||||
- \`--json\` - Output as JSON (for piping to jq)
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`object-storage preview <file_key:string>\` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- \`--storage <storage:string>\` - Secondary storage name
|
||||
- \`--mime <mime:string>\` - Override the detected mime type (e.g. text/csv)
|
||||
- \`--bytes-from <bytesFrom:number>\` - Start offset in bytes
|
||||
- \`--bytes-length <bytesLength:number>\` - Number of bytes to read
|
||||
- \`--csv-separator <csvSeparator:string>\` - CSV column separator (default ,)
|
||||
- \`--csv-header\` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -3253,6 +3290,26 @@ workspace related commands
|
||||
- \`--team-name <team_name:string>\` - Slack team name
|
||||
- \`workspace disconnect-slack\`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
\`wmill object-storage\` (alias \`wmill s3\`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace \`/job_helpers/*\` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **\`file_key\` is the path inside the bucket** (e.g. \`reports/2026-05/orders.csv\`), not a Windmill path. Do NOT pass \`u/...\` or \`f/...\` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **\`--storage <name>\` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use \`wmill object-storage list\` to discover configured storages.
|
||||
- **\`preview\` vs \`download\`**: \`preview\` returns a peek (CSV first rows, text content, or a byte slice via \`--bytes-from\`/\`--bytes-length\`) without writing to disk. Use \`download\` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: \`wmill object-storage files [prefix]\` (alias \`ls\`) — paginated, use \`--marker\` to continue.
|
||||
- Inspect one file: \`wmill object-storage info <file_key>\` for size/mime/last-modified, \`wmill object-storage preview <file_key>\` for content peek.
|
||||
- Move data in: \`wmill object-storage upload <local_path> <file_key>\` — set \`--content-type\` if the receiver cares (e.g. \`text/csv\`).
|
||||
- Move data out: \`wmill object-storage download <file_key> [output_path]\` — \`--stdout\` to pipe.
|
||||
- Reorganize: \`wmill object-storage move <src> <dest>\` (same storage), \`wmill object-storage delete <file_key>\` (interactive confirm unless \`--yes\`).
|
||||
`;
|
||||
|
||||
export const LANG_BASH = `# Bash
|
||||
|
||||
@@ -153,10 +153,11 @@ flow related commands
|
||||
- `flow run <path:string>` - run a flow by path.
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not ouput anything other then the final output. Useful for scripting.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default.
|
||||
- `flow preview <flow_path:string>` - preview a local flow without deploying it. Runs the flow definition from local files and uses local PathScripts by default. Pass --step <id> to run only one module in isolation (resolves nested steps inside branchone/branchall/forloopflow/whileloopflow plus the special preprocessor/failure modules; supported step types: rawscript, script, flow).
|
||||
- `-d --data <data:string>` - Inputs specified as a JSON string or a file using @<filename> or stdin using @-.
|
||||
- `-s --silent` - Do not output anything other then the final output. Useful for scripting.
|
||||
- `--remote` - Use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `--step <step_id:string>` - Run only the named step instead of the whole flow. Honors --data as the step's args and --remote / local-PathScript resolution the same way the full-flow preview does.
|
||||
- `flow new <flow_path:string>` - create a new empty flow
|
||||
- `--summary <summary:string>` - flow summary
|
||||
- `--description <description:string>` - flow description
|
||||
@@ -372,6 +373,42 @@ Validate Windmill flow, schedule, and trigger YAML files in a directory
|
||||
- `--locks-required` - Fail if scripts or flow inline scripts that need locks have no locks
|
||||
- `-w, --watch` - Watch for file changes and re-lint automatically
|
||||
|
||||
### object-storage
|
||||
|
||||
**Alias:** `s3`
|
||||
|
||||
**Subcommands:**
|
||||
|
||||
- `object-storage list` - List configured object storages for the workspace (default + secondary).
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `object-storage files [prefix:string]` - List files in an object storage. Optionally filter by prefix.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--max-keys <maxKeys:number>` - Page size (default 100)
|
||||
- `--marker <marker:string>` - Pagination marker from a previous response
|
||||
- `--storage <storage:string>` - Secondary storage name (omit for the workspace default)
|
||||
- `object-storage upload <local_path:string> <file_key:string>` - Upload a local file to object storage at the given file key.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--content-type <contentType:string>` - Content-Type header to set on the object
|
||||
- `--content-disposition <contentDisposition:string>` - Content-Disposition header to set on the object
|
||||
- `object-storage download <file_key:string> [output_path:string]` - Download an object to a local file (or stdout). Default output path is the basename of the file key in the current directory.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--stdout` - Write file contents to stdout instead of a file
|
||||
- `object-storage delete <file_key:string>` - Delete an object from object storage. Prompts for confirmation unless --yes is set.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--yes` - Skip the confirmation prompt
|
||||
- `object-storage move <src_file_key:string> <dest_file_key:string>` - Move an object within the same storage (rename or relocate by key).
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage info <file_key:string>` - Show metadata (size, mime, last-modified) for an object.
|
||||
- `--json` - Output as JSON (for piping to jq)
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `object-storage preview <file_key:string>` - Preview the contents of an object (text/CSV). Use --bytes-from / --bytes-length to peek at a slice of binary files.
|
||||
- `--storage <storage:string>` - Secondary storage name
|
||||
- `--mime <mime:string>` - Override the detected mime type (e.g. text/csv)
|
||||
- `--bytes-from <bytesFrom:number>` - Start offset in bytes
|
||||
- `--bytes-length <bytesLength:number>` - Number of bytes to read
|
||||
- `--csv-separator <csvSeparator:string>` - CSV column separator (default ,)
|
||||
- `--csv-header` - Treat the first CSV row as a header
|
||||
|
||||
### protection-rules
|
||||
|
||||
**Subcommands:**
|
||||
@@ -708,3 +745,23 @@ workspace related commands
|
||||
- `--team-name <team_name:string>` - Slack team name
|
||||
- `workspace disconnect-slack`
|
||||
|
||||
|
||||
|
||||
# Object Storage CLI
|
||||
|
||||
`wmill object-storage` (alias `wmill s3`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace `/job_helpers/*` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **`file_key` is the path inside the bucket** (e.g. `reports/2026-05/orders.csv`), not a Windmill path. Do NOT pass `u/...` or `f/...` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **`--storage <name>` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use `wmill object-storage list` to discover configured storages.
|
||||
- **`preview` vs `download`**: `preview` returns a peek (CSV first rows, text content, or a byte slice via `--bytes-from`/`--bytes-length`) without writing to disk. Use `download` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: `wmill object-storage files [prefix]` (alias `ls`) — paginated, use `--marker` to continue.
|
||||
- Inspect one file: `wmill object-storage info <file_key>` for size/mime/last-modified, `wmill object-storage preview <file_key>` for content peek.
|
||||
- Move data in: `wmill object-storage upload <local_path> <file_key>` — set `--content-type` if the receiver cares (e.g. `text/csv`).
|
||||
- Move data out: `wmill object-storage download <file_key> [output_path]` — `--stdout` to pipe.
|
||||
- Reorganize: `wmill object-storage move <src> <dest>` (same storage), `wmill object-storage delete <file_key>` (interactive confirm unless `--yes`).
|
||||
|
||||
@@ -46,7 +46,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
@@ -63,6 +63,12 @@ Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### Single-step vs whole-flow preview
|
||||
|
||||
Use `flow preview <flow_path> --step <step_id>` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript, locally if available; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive. The step id is resolved by walking nested branchone/branchall/forloopflow/whileloopflow modules and includes the special `preprocessor` and `failure` modules.
|
||||
|
||||
Use `flow preview <flow_path>` (no `--step`) when steps depend on each other's outputs, when the user is validating the overall control flow, or when `--step` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
@@ -41,7 +41,7 @@ Once the flow has real content, **offer** to open the visual preview as a one-se
|
||||
|
||||
After writing, tell the user which command fits what they want to do:
|
||||
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files.
|
||||
- `wmill flow preview <flow_path>` — **default when iterating on a local flow.** Runs the local `flow.yaml` against local inline scripts without deploying. Add `--remote` to use deployed workspace scripts for PathScript steps instead of local files. Add `--step <step_id>` to run only one module in isolation (see "Single-step vs whole-flow preview" below).
|
||||
- `wmill flow run <path>` — runs the flow **already deployed** in the workspace. Use only when the user explicitly wants to test the deployed version, not local edits.
|
||||
- `wmill generate-metadata` — regenerate stale `.lock` and `.script.yaml` files. By default it scans **scripts, flows, and apps** across the workspace; pass `--skip-flows --skip-apps` (or run from a subdirectory) to limit the scope when you only care about the flow you edited.
|
||||
- `wmill sync push` — deploy local changes to the workspace. Only suggest/run this when the user explicitly asks to deploy/publish/push — not when they say "run", "try", or "test".
|
||||
@@ -58,6 +58,12 @@ Only use `sync push` when:
|
||||
- The user explicitly asks to deploy, publish, push, or ship.
|
||||
- The preview has already validated the change and the user wants it in the workspace.
|
||||
|
||||
### Single-step vs whole-flow preview
|
||||
|
||||
Use `flow preview <flow_path> --step <step_id>` when the user is iterating on one module and the flow's upstream steps aren't part of what they're trying to validate. It runs only that step's runnable (rawscript: the inline script; script: the PathScript, locally if available; flow: the subflow by path) and is much faster than running the whole flow when previous steps are slow or expensive. The step id is resolved by walking nested branchone/branchall/forloopflow/whileloopflow modules and includes the special `preprocessor` and `failure` modules.
|
||||
|
||||
Use `flow preview <flow_path>` (no `--step`) when steps depend on each other's outputs, when the user is validating the overall control flow, or when `--step` doesn't apply (branchone, branchall, forloopflow, whileloopflow, identity, and AI agent steps cannot themselves be tested in isolation — for branchone/branchall/forloopflow/whileloopflow, the *contained* steps can, by passing the inner step's id).
|
||||
|
||||
### After writing — offer to run, don't wait passively
|
||||
|
||||
This is about **programmatic execution** (`wmill flow preview -d '<args>'`), which actually runs the flow and has side effects. Visual preview (the `preview` skill) is offered separately — see "Visual preview" below.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Object Storage CLI
|
||||
|
||||
`wmill object-storage` (alias `wmill s3`) exposes the workspace's object storage (S3-compatible: AWS S3, MinIO, GCS, R2, Azure Blob) over the per-workspace `/job_helpers/*` endpoints.
|
||||
|
||||
## Key concepts (not obvious from per-command --help)
|
||||
|
||||
- **`file_key` is the path inside the bucket** (e.g. `reports/2026-05/orders.csv`), not a Windmill path. Do NOT pass `u/...` or `f/...` here — those are Windmill paths to scripts/flows/resources, unrelated to objects in the bucket.
|
||||
- **Scope is the active workspace.** Object storage is configured per-workspace (default storage + optional secondary storages). Switching workspaces switches which bucket the commands target.
|
||||
- **`--storage <name>` targets a secondary storage** configured on the workspace. Omit it to use the workspace's default object storage. Use `wmill object-storage list` to discover configured storages.
|
||||
- **`preview` vs `download`**: `preview` returns a peek (CSV first rows, text content, or a byte slice via `--bytes-from`/`--bytes-length`) without writing to disk. Use `download` when you want the full file on disk.
|
||||
|
||||
## Choosing a subcommand
|
||||
|
||||
- Look at what's there: `wmill object-storage files [prefix]` (alias `ls`) — paginated, use `--marker` to continue.
|
||||
- Inspect one file: `wmill object-storage info <file_key>` for size/mime/last-modified, `wmill object-storage preview <file_key>` for content peek.
|
||||
- Move data in: `wmill object-storage upload <local_path> <file_key>` — set `--content-type` if the receiver cares (e.g. `text/csv`).
|
||||
- Move data out: `wmill object-storage download <file_key> [output_path]` — `--stdout` to pipe.
|
||||
- Reorganize: `wmill object-storage move <src> <dest>` (same storage), `wmill object-storage delete <file_key>` (interactive confirm unless `--yes`).
|
||||
@@ -2312,6 +2312,13 @@ def main():
|
||||
print("Extracting CLI commands...")
|
||||
cli_data = extract_cli_commands()
|
||||
cli_commands = generate_cli_commands_markdown(cli_data)
|
||||
# Append hand-written CLI guidance covering bits that aren't obvious from
|
||||
# the auto-generated per-command --help (file_key semantics, --storage,
|
||||
# workspace scope). The cli-commands skill is the entry point agents read
|
||||
# to learn about `wmill`, so non-obvious usage notes belong here.
|
||||
object_storage_cli = read_markdown_file(base_dir / "object-storage-cli.md")
|
||||
if object_storage_cli:
|
||||
cli_commands = f"{cli_commands}\n\n{object_storage_cli}"
|
||||
OUTPUT_CLI_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(OUTPUT_CLI_DIR / "cli-commands.md").write_text(cli_commands)
|
||||
print(f" Found {len(cli_data['commands'])} commands, {len(cli_data['global_options'])} global options")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.709.0",
|
||||
"version": "1.711.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.709.0",
|
||||
"version": "1.711.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"sideEffects": false,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.709.0
|
||||
1.711.0
|
||||
|
||||
Reference in New Issue
Block a user